/src/mozilla-central/dom/animation/Animation.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
4 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
5 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | |
7 | | #include "Animation.h" |
8 | | #include "AnimationUtils.h" |
9 | | #include "mozilla/dom/AnimationBinding.h" |
10 | | #include "mozilla/dom/AnimationPlaybackEvent.h" |
11 | | #include "mozilla/dom/DocumentTimeline.h" |
12 | | #include "mozilla/AnimationEventDispatcher.h" |
13 | | #include "mozilla/AnimationTarget.h" |
14 | | #include "mozilla/AutoRestore.h" |
15 | | #include "mozilla/Maybe.h" // For Maybe |
16 | | #include "mozilla/TypeTraits.h" // For std::forward<> |
17 | | #include "nsAnimationManager.h" // For CSSAnimation |
18 | | #include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch |
19 | | #include "nsIDocument.h" // For nsIDocument |
20 | | #include "nsIPresShell.h" // For nsIPresShell |
21 | | #include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr |
22 | | #include "nsTransitionManager.h" // For CSSTransition |
23 | | #include "PendingAnimationTracker.h" // For PendingAnimationTracker |
24 | | |
25 | | namespace mozilla { |
26 | | namespace dom { |
27 | | |
28 | | // Static members |
29 | | uint64_t Animation::sNextAnimationIndex = 0; |
30 | | |
31 | | NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper, |
32 | | mTimeline, |
33 | | mEffect, |
34 | | mReady, |
35 | | mFinished) |
36 | | |
37 | | NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper) |
38 | | NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper) |
39 | | |
40 | 0 | NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Animation) |
41 | 0 | NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) |
42 | | |
43 | | JSObject* |
44 | | Animation::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) |
45 | 0 | { |
46 | 0 | return dom::Animation_Binding::Wrap(aCx, this, aGivenProto); |
47 | 0 | } |
48 | | |
49 | | // --------------------------------------------------------------------------- |
50 | | // |
51 | | // Utility methods |
52 | | // |
53 | | // --------------------------------------------------------------------------- |
54 | | |
55 | | namespace { |
56 | | // A wrapper around nsAutoAnimationMutationBatch that looks up the |
57 | | // appropriate document from the supplied animation. |
58 | | class MOZ_RAII AutoMutationBatchForAnimation { |
59 | | public: |
60 | | explicit AutoMutationBatchForAnimation(const Animation& aAnimation |
61 | 0 | MOZ_GUARD_OBJECT_NOTIFIER_PARAM) { |
62 | 0 | MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
63 | 0 | Maybe<NonOwningAnimationTarget> target = |
64 | 0 | nsNodeUtils::GetTargetForAnimation(&aAnimation); |
65 | 0 | if (!target) { |
66 | 0 | return; |
67 | 0 | } |
68 | 0 | |
69 | 0 | // For mutation observers, we use the OwnerDoc. |
70 | 0 | nsIDocument* doc = target->mElement->OwnerDoc(); |
71 | 0 | if (!doc) { |
72 | 0 | return; |
73 | 0 | } |
74 | 0 | |
75 | 0 | mAutoBatch.emplace(doc); |
76 | 0 | } |
77 | | |
78 | | private: |
79 | | MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
80 | | Maybe<nsAutoAnimationMutationBatch> mAutoBatch; |
81 | | }; |
82 | | } |
83 | | |
84 | | // --------------------------------------------------------------------------- |
85 | | // |
86 | | // Animation interface: |
87 | | // |
88 | | // --------------------------------------------------------------------------- |
89 | | /* static */ already_AddRefed<Animation> |
90 | | Animation::Constructor(const GlobalObject& aGlobal, |
91 | | AnimationEffect* aEffect, |
92 | | const Optional<AnimationTimeline*>& aTimeline, |
93 | | ErrorResult& aRv) |
94 | 0 | { |
95 | 0 | nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); |
96 | 0 | RefPtr<Animation> animation = new Animation(global); |
97 | 0 |
|
98 | 0 | AnimationTimeline* timeline; |
99 | 0 | if (aTimeline.WasPassed()) { |
100 | 0 | timeline = aTimeline.Value(); |
101 | 0 | } else { |
102 | 0 | nsIDocument* document = |
103 | 0 | AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); |
104 | 0 | if (!document) { |
105 | 0 | aRv.Throw(NS_ERROR_FAILURE); |
106 | 0 | return nullptr; |
107 | 0 | } |
108 | 0 | timeline = document->Timeline(); |
109 | 0 | } |
110 | 0 |
|
111 | 0 | animation->SetTimelineNoUpdate(timeline); |
112 | 0 | animation->SetEffectNoUpdate(aEffect); |
113 | 0 |
|
114 | 0 | return animation.forget(); |
115 | 0 | } |
116 | | |
117 | | void |
118 | | Animation::SetId(const nsAString& aId) |
119 | 0 | { |
120 | 0 | if (mId == aId) { |
121 | 0 | return; |
122 | 0 | } |
123 | 0 | mId = aId; |
124 | 0 | nsNodeUtils::AnimationChanged(this); |
125 | 0 | } |
126 | | |
127 | | void |
128 | | Animation::SetEffect(AnimationEffect* aEffect) |
129 | 0 | { |
130 | 0 | SetEffectNoUpdate(aEffect); |
131 | 0 | PostUpdate(); |
132 | 0 | } |
133 | | |
134 | | // https://drafts.csswg.org/web-animations/#setting-the-target-effect |
135 | | void |
136 | | Animation::SetEffectNoUpdate(AnimationEffect* aEffect) |
137 | 0 | { |
138 | 0 | RefPtr<Animation> kungFuDeathGrip(this); |
139 | 0 |
|
140 | 0 | if (mEffect == aEffect) { |
141 | 0 | return; |
142 | 0 | } |
143 | 0 | |
144 | 0 | AutoMutationBatchForAnimation mb(*this); |
145 | 0 | bool wasRelevant = mIsRelevant; |
146 | 0 |
|
147 | 0 | if (mEffect) { |
148 | 0 | // We need to notify observers now because once we set mEffect to null |
149 | 0 | // we won't be able to find the target element to notify. |
150 | 0 | if (mIsRelevant) { |
151 | 0 | nsNodeUtils::AnimationRemoved(this); |
152 | 0 | } |
153 | 0 |
|
154 | 0 | // Break links with the old effect and then drop it. |
155 | 0 | RefPtr<AnimationEffect> oldEffect = mEffect; |
156 | 0 | mEffect = nullptr; |
157 | 0 | oldEffect->SetAnimation(nullptr); |
158 | 0 |
|
159 | 0 | // The following will not do any notification because mEffect is null. |
160 | 0 | UpdateRelevance(); |
161 | 0 | } |
162 | 0 |
|
163 | 0 | if (aEffect) { |
164 | 0 | // Break links from the new effect to its previous animation, if any. |
165 | 0 | RefPtr<AnimationEffect> newEffect = aEffect; |
166 | 0 | Animation* prevAnim = aEffect->GetAnimation(); |
167 | 0 | if (prevAnim) { |
168 | 0 | prevAnim->SetEffect(nullptr); |
169 | 0 | } |
170 | 0 |
|
171 | 0 | // Create links with the new effect. SetAnimation(this) will also update |
172 | 0 | // mIsRelevant of this animation, and then notify mutation observer if |
173 | 0 | // needed by calling Animation::UpdateRelevance(), so we don't need to |
174 | 0 | // call it again. |
175 | 0 | mEffect = newEffect; |
176 | 0 | mEffect->SetAnimation(this); |
177 | 0 |
|
178 | 0 | // Notify possible add or change. |
179 | 0 | // If the target is different, the change notification will be ignored by |
180 | 0 | // AutoMutationBatchForAnimation. |
181 | 0 | if (wasRelevant && mIsRelevant) { |
182 | 0 | nsNodeUtils::AnimationChanged(this); |
183 | 0 | } |
184 | 0 |
|
185 | 0 | ReschedulePendingTasks(); |
186 | 0 | } |
187 | 0 |
|
188 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
189 | 0 | } |
190 | | |
191 | | void |
192 | | Animation::SetTimeline(AnimationTimeline* aTimeline) |
193 | 0 | { |
194 | 0 | SetTimelineNoUpdate(aTimeline); |
195 | 0 | PostUpdate(); |
196 | 0 | } |
197 | | |
198 | | // https://drafts.csswg.org/web-animations/#setting-the-timeline |
199 | | void |
200 | | Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) |
201 | 0 | { |
202 | 0 | if (mTimeline == aTimeline) { |
203 | 0 | return; |
204 | 0 | } |
205 | 0 | |
206 | 0 | StickyTimeDuration activeTime = mEffect |
207 | 0 | ? mEffect->GetComputedTiming().mActiveTime |
208 | 0 | : StickyTimeDuration(); |
209 | 0 |
|
210 | 0 | RefPtr<AnimationTimeline> oldTimeline = mTimeline; |
211 | 0 | if (oldTimeline) { |
212 | 0 | oldTimeline->RemoveAnimation(this); |
213 | 0 | } |
214 | 0 |
|
215 | 0 | mTimeline = aTimeline; |
216 | 0 | if (!mStartTime.IsNull()) { |
217 | 0 | mHoldTime.SetNull(); |
218 | 0 | } |
219 | 0 |
|
220 | 0 | if (!aTimeline) { |
221 | 0 | MaybeQueueCancelEvent(activeTime); |
222 | 0 | } |
223 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
224 | 0 | } |
225 | | |
226 | | // https://drafts.csswg.org/web-animations/#set-the-animation-start-time |
227 | | void |
228 | | Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime) |
229 | 0 | { |
230 | 0 | // Return early if the start time will not change. However, if we |
231 | 0 | // are pending, then setting the start time to any value |
232 | 0 | // including the current value has the effect of aborting |
233 | 0 | // pending tasks so we should not return early in that case. |
234 | 0 | if (!Pending() && aNewStartTime == mStartTime) { |
235 | 0 | return; |
236 | 0 | } |
237 | 0 | |
238 | 0 | AutoMutationBatchForAnimation mb(*this); |
239 | 0 |
|
240 | 0 | Nullable<TimeDuration> timelineTime; |
241 | 0 | if (mTimeline) { |
242 | 0 | // The spec says to check if the timeline is active (has a resolved time) |
243 | 0 | // before using it here, but we don't need to since it's harmless to set |
244 | 0 | // the already null time to null. |
245 | 0 | timelineTime = mTimeline->GetCurrentTime(); |
246 | 0 | } |
247 | 0 | if (timelineTime.IsNull() && !aNewStartTime.IsNull()) { |
248 | 0 | mHoldTime.SetNull(); |
249 | 0 | } |
250 | 0 |
|
251 | 0 | Nullable<TimeDuration> previousCurrentTime = GetCurrentTime(); |
252 | 0 |
|
253 | 0 | ApplyPendingPlaybackRate(); |
254 | 0 | mStartTime = aNewStartTime; |
255 | 0 |
|
256 | 0 | if (!aNewStartTime.IsNull()) { |
257 | 0 | if (mPlaybackRate != 0.0) { |
258 | 0 | mHoldTime.SetNull(); |
259 | 0 | } |
260 | 0 | } else { |
261 | 0 | mHoldTime = previousCurrentTime; |
262 | 0 | } |
263 | 0 |
|
264 | 0 | CancelPendingTasks(); |
265 | 0 | if (mReady) { |
266 | 0 | // We may have already resolved mReady, but in that case calling |
267 | 0 | // MaybeResolve is a no-op, so that's okay. |
268 | 0 | mReady->MaybeResolve(this); |
269 | 0 | } |
270 | 0 |
|
271 | 0 | UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); |
272 | 0 | if (IsRelevant()) { |
273 | 0 | nsNodeUtils::AnimationChanged(this); |
274 | 0 | } |
275 | 0 | PostUpdate(); |
276 | 0 | } |
277 | | |
278 | | // https://drafts.csswg.org/web-animations/#current-time |
279 | | Nullable<TimeDuration> |
280 | | Animation::GetCurrentTimeForHoldTime( |
281 | | const Nullable<TimeDuration>& aHoldTime) const |
282 | 0 | { |
283 | 0 | Nullable<TimeDuration> result; |
284 | 0 | if (!aHoldTime.IsNull()) { |
285 | 0 | result = aHoldTime; |
286 | 0 | return result; |
287 | 0 | } |
288 | 0 | |
289 | 0 | if (mTimeline && !mStartTime.IsNull()) { |
290 | 0 | Nullable<TimeDuration> timelineTime = mTimeline->GetCurrentTime(); |
291 | 0 | if (!timelineTime.IsNull()) { |
292 | 0 | result = CurrentTimeFromTimelineTime( |
293 | 0 | timelineTime.Value(), mStartTime.Value(), mPlaybackRate); |
294 | 0 | } |
295 | 0 | } |
296 | 0 | return result; |
297 | 0 | } |
298 | | |
299 | | // https://drafts.csswg.org/web-animations/#set-the-current-time |
300 | | void |
301 | | Animation::SetCurrentTime(const TimeDuration& aSeekTime) |
302 | 0 | { |
303 | 0 | // Return early if the current time has not changed. However, if we |
304 | 0 | // are pause-pending, then setting the current time to any value |
305 | 0 | // including the current value has the effect of aborting the |
306 | 0 | // pause so we should not return early in that case. |
307 | 0 | if (mPendingState != PendingState::PausePending && |
308 | 0 | Nullable<TimeDuration>(aSeekTime) == GetCurrentTime()) { |
309 | 0 | return; |
310 | 0 | } |
311 | 0 | |
312 | 0 | AutoMutationBatchForAnimation mb(*this); |
313 | 0 |
|
314 | 0 | SilentlySetCurrentTime(aSeekTime); |
315 | 0 |
|
316 | 0 | if (mPendingState == PendingState::PausePending) { |
317 | 0 | // Finish the pause operation |
318 | 0 | mHoldTime.SetValue(aSeekTime); |
319 | 0 |
|
320 | 0 | ApplyPendingPlaybackRate(); |
321 | 0 | mStartTime.SetNull(); |
322 | 0 |
|
323 | 0 | if (mReady) { |
324 | 0 | mReady->MaybeResolve(this); |
325 | 0 | } |
326 | 0 | CancelPendingTasks(); |
327 | 0 | } |
328 | 0 |
|
329 | 0 | UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); |
330 | 0 | if (IsRelevant()) { |
331 | 0 | nsNodeUtils::AnimationChanged(this); |
332 | 0 | } |
333 | 0 | PostUpdate(); |
334 | 0 | } |
335 | | |
336 | | // https://drafts.csswg.org/web-animations/#set-the-playback-rate |
337 | | void |
338 | | Animation::SetPlaybackRate(double aPlaybackRate) |
339 | 0 | { |
340 | 0 | mPendingPlaybackRate.reset(); |
341 | 0 |
|
342 | 0 | if (aPlaybackRate == mPlaybackRate) { |
343 | 0 | return; |
344 | 0 | } |
345 | 0 | |
346 | 0 | AutoMutationBatchForAnimation mb(*this); |
347 | 0 |
|
348 | 0 | Nullable<TimeDuration> previousTime = GetCurrentTime(); |
349 | 0 | mPlaybackRate = aPlaybackRate; |
350 | 0 | if (!previousTime.IsNull()) { |
351 | 0 | SetCurrentTime(previousTime.Value()); |
352 | 0 | } |
353 | 0 |
|
354 | 0 | // In the case where GetCurrentTime() returns the same result before and |
355 | 0 | // after updating mPlaybackRate, SetCurrentTime will return early since, |
356 | 0 | // as far as it can tell, nothing has changed. |
357 | 0 | // As a result, we need to perform the following updates here: |
358 | 0 | // - update timing (since, if the sign of the playback rate has changed, our |
359 | 0 | // finished state may have changed), |
360 | 0 | // - dispatch a change notification for the changed playback rate, and |
361 | 0 | // - update the playback rate on animations on layers. |
362 | 0 | UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); |
363 | 0 | if (IsRelevant()) { |
364 | 0 | nsNodeUtils::AnimationChanged(this); |
365 | 0 | } |
366 | 0 | PostUpdate(); |
367 | 0 | } |
368 | | |
369 | | // https://drafts.csswg.org/web-animations/#seamlessly-update-the-playback-rate |
370 | | void |
371 | | Animation::UpdatePlaybackRate(double aPlaybackRate) |
372 | 0 | { |
373 | 0 | if (mPendingPlaybackRate && mPendingPlaybackRate.value() == aPlaybackRate) { |
374 | 0 | return; |
375 | 0 | } |
376 | 0 | |
377 | 0 | mPendingPlaybackRate = Some(aPlaybackRate); |
378 | 0 |
|
379 | 0 | // If we already have a pending task, there is nothing more to do since the |
380 | 0 | // playback rate will be applied then. |
381 | 0 | if (Pending()) { |
382 | 0 | return; |
383 | 0 | } |
384 | 0 | |
385 | 0 | AutoMutationBatchForAnimation mb(*this); |
386 | 0 |
|
387 | 0 | AnimationPlayState playState = PlayState(); |
388 | 0 | if (playState == AnimationPlayState::Idle || |
389 | 0 | playState == AnimationPlayState::Paused) { |
390 | 0 | // We are either idle or paused. In either case we can apply the pending |
391 | 0 | // playback rate immediately. |
392 | 0 | ApplyPendingPlaybackRate(); |
393 | 0 |
|
394 | 0 | // We don't need to update timing or post an update here because: |
395 | 0 | // |
396 | 0 | // * the current time hasn't changed -- it's either unresolved or fixed |
397 | 0 | // with a hold time -- so the output won't have changed |
398 | 0 | // * the finished state won't have changed even if the sign of the |
399 | 0 | // playback rate changed since we're not finished (we're paused or idle) |
400 | 0 | // * the playback rate on layers doesn't need to be updated since we're not |
401 | 0 | // moving. Once we get a start time etc. we'll update the playback rate |
402 | 0 | // then. |
403 | 0 | // |
404 | 0 | // All we need to do is update observers so that, e.g. DevTools, report the |
405 | 0 | // right information. |
406 | 0 | if (IsRelevant()) { |
407 | 0 | nsNodeUtils::AnimationChanged(this); |
408 | 0 | } |
409 | 0 | } else if (playState == AnimationPlayState::Finished) { |
410 | 0 | MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(), |
411 | 0 | "If we have no active timeline, we should be idle or paused"); |
412 | 0 | if (aPlaybackRate != 0) { |
413 | 0 | // The unconstrained current time can only be unresolved if either we |
414 | 0 | // don't have an active timeline (and we already asserted that is not |
415 | 0 | // true) or we have an unresolved start time (in which case we should be |
416 | 0 | // paused). |
417 | 0 | MOZ_ASSERT(!GetUnconstrainedCurrentTime().IsNull(), |
418 | 0 | "Unconstrained current time should be resolved"); |
419 | 0 | TimeDuration unconstrainedCurrentTime = |
420 | 0 | GetUnconstrainedCurrentTime().Value(); |
421 | 0 | TimeDuration timelineTime = mTimeline->GetCurrentTime().Value(); |
422 | 0 | mStartTime = StartTimeFromTimelineTime( |
423 | 0 | timelineTime, unconstrainedCurrentTime, aPlaybackRate); |
424 | 0 | } else { |
425 | 0 | mStartTime = mTimeline->GetCurrentTime(); |
426 | 0 | } |
427 | 0 |
|
428 | 0 | ApplyPendingPlaybackRate(); |
429 | 0 |
|
430 | 0 | // Even though we preserve the current time, we might now leave the finished |
431 | 0 | // state (e.g. if the playback rate changes sign) so we need to update |
432 | 0 | // timing. |
433 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
434 | 0 | if (IsRelevant()) { |
435 | 0 | nsNodeUtils::AnimationChanged(this); |
436 | 0 | } |
437 | 0 | PostUpdate(); |
438 | 0 | } else { |
439 | 0 | ErrorResult rv; |
440 | 0 | Play(rv, LimitBehavior::Continue); |
441 | 0 | MOZ_ASSERT(!rv.Failed(), |
442 | 0 | "We should only fail to play when using auto-rewind behavior"); |
443 | 0 | } |
444 | 0 | } |
445 | | |
446 | | // https://drafts.csswg.org/web-animations/#play-state |
447 | | AnimationPlayState |
448 | | Animation::PlayState() const |
449 | 0 | { |
450 | 0 | Nullable<TimeDuration> currentTime = GetCurrentTime(); |
451 | 0 | if (currentTime.IsNull() && !Pending()) { |
452 | 0 | return AnimationPlayState::Idle; |
453 | 0 | } |
454 | 0 | |
455 | 0 | if (mPendingState == PendingState::PausePending || |
456 | 0 | (mStartTime.IsNull() && !Pending())) { |
457 | 0 | return AnimationPlayState::Paused; |
458 | 0 | } |
459 | 0 | |
460 | 0 | if (!currentTime.IsNull() && |
461 | 0 | ((mPlaybackRate > 0.0 && currentTime.Value() >= EffectEnd()) || |
462 | 0 | (mPlaybackRate < 0.0 && currentTime.Value() <= TimeDuration()))) { |
463 | 0 | return AnimationPlayState::Finished; |
464 | 0 | } |
465 | 0 | |
466 | 0 | return AnimationPlayState::Running; |
467 | 0 | } |
468 | | |
469 | | Promise* |
470 | | Animation::GetReady(ErrorResult& aRv) |
471 | 0 | { |
472 | 0 | nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); |
473 | 0 | if (!mReady && global) { |
474 | 0 | mReady = Promise::Create(global, aRv); // Lazily create on demand |
475 | 0 | } |
476 | 0 | if (!mReady) { |
477 | 0 | aRv.Throw(NS_ERROR_FAILURE); |
478 | 0 | return nullptr; |
479 | 0 | } |
480 | 0 | if (!Pending()) { |
481 | 0 | mReady->MaybeResolve(this); |
482 | 0 | } |
483 | 0 | return mReady; |
484 | 0 | } |
485 | | |
486 | | Promise* |
487 | | Animation::GetFinished(ErrorResult& aRv) |
488 | 0 | { |
489 | 0 | nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); |
490 | 0 | if (!mFinished && global) { |
491 | 0 | mFinished = Promise::Create(global, aRv); // Lazily create on demand |
492 | 0 | } |
493 | 0 | if (!mFinished) { |
494 | 0 | aRv.Throw(NS_ERROR_FAILURE); |
495 | 0 | return nullptr; |
496 | 0 | } |
497 | 0 | if (mFinishedIsResolved) { |
498 | 0 | MaybeResolveFinishedPromise(); |
499 | 0 | } |
500 | 0 | return mFinished; |
501 | 0 | } |
502 | | |
503 | | void |
504 | | Animation::Cancel() |
505 | 0 | { |
506 | 0 | CancelNoUpdate(); |
507 | 0 | PostUpdate(); |
508 | 0 | } |
509 | | |
510 | | // https://drafts.csswg.org/web-animations/#finish-an-animation |
511 | | void |
512 | | Animation::Finish(ErrorResult& aRv) |
513 | 0 | { |
514 | 0 | double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); |
515 | 0 |
|
516 | 0 | if (effectivePlaybackRate == 0 || |
517 | 0 | (effectivePlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) { |
518 | 0 | aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
519 | 0 | return; |
520 | 0 | } |
521 | 0 | |
522 | 0 | AutoMutationBatchForAnimation mb(*this); |
523 | 0 |
|
524 | 0 | ApplyPendingPlaybackRate(); |
525 | 0 |
|
526 | 0 | // Seek to the end |
527 | 0 | TimeDuration limit = |
528 | 0 | mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0); |
529 | 0 | bool didChange = GetCurrentTime() != Nullable<TimeDuration>(limit); |
530 | 0 | SilentlySetCurrentTime(limit); |
531 | 0 |
|
532 | 0 | // If we are paused or play-pending we need to fill in the start time in |
533 | 0 | // order to transition to the finished state. |
534 | 0 | // |
535 | 0 | // We only do this, however, if we have an active timeline. If we have an |
536 | 0 | // inactive timeline we can't transition into the finished state just like |
537 | 0 | // we can't transition to the running state (this finished state is really |
538 | 0 | // a substate of the running state). |
539 | 0 | if (mStartTime.IsNull() && |
540 | 0 | mTimeline && |
541 | 0 | !mTimeline->GetCurrentTime().IsNull()) { |
542 | 0 | mStartTime = StartTimeFromTimelineTime( |
543 | 0 | mTimeline->GetCurrentTime().Value(), limit, mPlaybackRate); |
544 | 0 | didChange = true; |
545 | 0 | } |
546 | 0 |
|
547 | 0 | // If we just resolved the start time for a pause or play-pending |
548 | 0 | // animation, we need to clear the task. We don't do this as a branch of |
549 | 0 | // the above however since we can have a play-pending animation with a |
550 | 0 | // resolved start time if we aborted a pause operation. |
551 | 0 | if (!mStartTime.IsNull() && |
552 | 0 | (mPendingState == PendingState::PlayPending || |
553 | 0 | mPendingState == PendingState::PausePending)) { |
554 | 0 | if (mPendingState == PendingState::PausePending) { |
555 | 0 | mHoldTime.SetNull(); |
556 | 0 | } |
557 | 0 | CancelPendingTasks(); |
558 | 0 | didChange = true; |
559 | 0 | if (mReady) { |
560 | 0 | mReady->MaybeResolve(this); |
561 | 0 | } |
562 | 0 | } |
563 | 0 | UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Sync); |
564 | 0 | if (didChange && IsRelevant()) { |
565 | 0 | nsNodeUtils::AnimationChanged(this); |
566 | 0 | } |
567 | 0 | PostUpdate(); |
568 | 0 | } |
569 | | |
570 | | void |
571 | | Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior) |
572 | 0 | { |
573 | 0 | PlayNoUpdate(aRv, aLimitBehavior); |
574 | 0 | PostUpdate(); |
575 | 0 | } |
576 | | |
577 | | // https://drafts.csswg.org/web-animations/#reverse-an-animation |
578 | | void |
579 | | Animation::Reverse(ErrorResult& aRv) |
580 | 0 | { |
581 | 0 | if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { |
582 | 0 | aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
583 | 0 | return; |
584 | 0 | } |
585 | 0 | |
586 | 0 | double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); |
587 | 0 |
|
588 | 0 | if (effectivePlaybackRate == 0.0) { |
589 | 0 | return; |
590 | 0 | } |
591 | 0 | |
592 | 0 | Maybe<double> originalPendingPlaybackRate = mPendingPlaybackRate; |
593 | 0 |
|
594 | 0 | mPendingPlaybackRate = Some(-effectivePlaybackRate); |
595 | 0 |
|
596 | 0 | Play(aRv, LimitBehavior::AutoRewind); |
597 | 0 |
|
598 | 0 | // If Play() threw, restore state and don't report anything to mutation |
599 | 0 | // observers. |
600 | 0 | if (aRv.Failed()) { |
601 | 0 | mPendingPlaybackRate = originalPendingPlaybackRate; |
602 | 0 | } |
603 | 0 |
|
604 | 0 | // Play(), above, unconditionally calls PostUpdate so we don't need to do |
605 | 0 | // it here. |
606 | 0 | } |
607 | | |
608 | | // --------------------------------------------------------------------------- |
609 | | // |
610 | | // JS wrappers for Animation interface: |
611 | | // |
612 | | // --------------------------------------------------------------------------- |
613 | | |
614 | | Nullable<double> |
615 | | Animation::GetStartTimeAsDouble() const |
616 | 0 | { |
617 | 0 | return AnimationUtils::TimeDurationToDouble(mStartTime); |
618 | 0 | } |
619 | | |
620 | | void |
621 | | Animation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) |
622 | 0 | { |
623 | 0 | return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime)); |
624 | 0 | } |
625 | | |
626 | | Nullable<double> |
627 | | Animation::GetCurrentTimeAsDouble() const |
628 | 0 | { |
629 | 0 | return AnimationUtils::TimeDurationToDouble(GetCurrentTime()); |
630 | 0 | } |
631 | | |
632 | | void |
633 | | Animation::SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime, |
634 | | ErrorResult& aRv) |
635 | 0 | { |
636 | 0 | if (aCurrentTime.IsNull()) { |
637 | 0 | if (!GetCurrentTime().IsNull()) { |
638 | 0 | aRv.Throw(NS_ERROR_DOM_TYPE_ERR); |
639 | 0 | } |
640 | 0 | return; |
641 | 0 | } |
642 | 0 |
|
643 | 0 | return SetCurrentTime(TimeDuration::FromMilliseconds(aCurrentTime.Value())); |
644 | 0 | } |
645 | | |
646 | | // --------------------------------------------------------------------------- |
647 | | |
648 | | void |
649 | | Animation::Tick() |
650 | 0 | { |
651 | 0 | // Finish pending if we have a pending ready time, but only if we also |
652 | 0 | // have an active timeline. |
653 | 0 | if (mPendingState != PendingState::NotPending && |
654 | 0 | !mPendingReadyTime.IsNull() && |
655 | 0 | mTimeline && |
656 | 0 | !mTimeline->GetCurrentTime().IsNull()) { |
657 | 0 | // Even though mPendingReadyTime is initialized using TimeStamp::Now() |
658 | 0 | // during the *previous* tick of the refresh driver, it can still be |
659 | 0 | // ahead of the *current* timeline time when we are using the |
660 | 0 | // vsync timer so we need to clamp it to the timeline time. |
661 | 0 | TimeDuration currentTime = mTimeline->GetCurrentTime().Value(); |
662 | 0 | if (currentTime < mPendingReadyTime.Value()) { |
663 | 0 | mPendingReadyTime.SetValue(currentTime); |
664 | 0 | } |
665 | 0 | FinishPendingAt(mPendingReadyTime.Value()); |
666 | 0 | mPendingReadyTime.SetNull(); |
667 | 0 | } |
668 | 0 |
|
669 | 0 | if (IsPossiblyOrphanedPendingAnimation()) { |
670 | 0 | MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(), |
671 | 0 | "Orphaned pending animations should have an active timeline"); |
672 | 0 | FinishPendingAt(mTimeline->GetCurrentTime().Value()); |
673 | 0 | } |
674 | 0 |
|
675 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
676 | 0 |
|
677 | 0 | if (!mEffect) { |
678 | 0 | return; |
679 | 0 | } |
680 | 0 | |
681 | 0 | // Update layers if we are newly finished. |
682 | 0 | KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); |
683 | 0 | if (keyframeEffect && |
684 | 0 | !keyframeEffect->Properties().IsEmpty() && |
685 | 0 | !mFinishedAtLastComposeStyle && |
686 | 0 | PlayState() == AnimationPlayState::Finished) { |
687 | 0 | PostUpdate(); |
688 | 0 | } |
689 | 0 | } |
690 | | |
691 | | void |
692 | | Animation::TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime) |
693 | 0 | { |
694 | 0 | // Normally we expect the play state to be pending but it's possible that, |
695 | 0 | // due to the handling of possibly orphaned animations in Tick(), this |
696 | 0 | // animation got started whilst still being in another document's pending |
697 | 0 | // animation map. |
698 | 0 | if (!Pending()) { |
699 | 0 | return; |
700 | 0 | } |
701 | 0 | |
702 | 0 | // If aReadyTime.IsNull() we'll detect this in Tick() where we check for |
703 | 0 | // orphaned animations and trigger this animation anyway |
704 | 0 | mPendingReadyTime = aReadyTime; |
705 | 0 | } |
706 | | |
707 | | void |
708 | | Animation::TriggerNow() |
709 | 0 | { |
710 | 0 | // Normally we expect the play state to be pending but when an animation |
711 | 0 | // is cancelled and its rendered document can't be reached, we can end up |
712 | 0 | // with the animation still in a pending player tracker even after it is |
713 | 0 | // no longer pending. |
714 | 0 | if (!Pending()) { |
715 | 0 | return; |
716 | 0 | } |
717 | 0 | |
718 | 0 | // If we don't have an active timeline we can't trigger the animation. |
719 | 0 | // However, this is a test-only method that we don't expect to be used in |
720 | 0 | // conjunction with animations without an active timeline so generate |
721 | 0 | // a warning if we do find ourselves in that situation. |
722 | 0 | if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { |
723 | 0 | NS_WARNING("Failed to trigger an animation with an active timeline"); |
724 | 0 | return; |
725 | 0 | } |
726 | 0 |
|
727 | 0 | FinishPendingAt(mTimeline->GetCurrentTime().Value()); |
728 | 0 | } |
729 | | |
730 | | Nullable<TimeDuration> |
731 | | Animation::GetCurrentOrPendingStartTime() const |
732 | 0 | { |
733 | 0 | Nullable<TimeDuration> result; |
734 | 0 |
|
735 | 0 | // If we have a pending playback rate, work out what start time we will use |
736 | 0 | // when we come to updating that playback rate. |
737 | 0 | // |
738 | 0 | // This logic roughly shadows that in ResumeAt but is just different enough |
739 | 0 | // that it is difficult to extract out the common functionality (and |
740 | 0 | // extracting that functionality out would make it harder to match ResumeAt up |
741 | 0 | // against the spec). |
742 | 0 | if (mPendingPlaybackRate && !mPendingReadyTime.IsNull() && |
743 | 0 | !mStartTime.IsNull()) { |
744 | 0 | // If we have a hold time, use it as the current time to match. |
745 | 0 | TimeDuration currentTimeToMatch = |
746 | 0 | !mHoldTime.IsNull() |
747 | 0 | ? mHoldTime.Value() |
748 | 0 | : CurrentTimeFromTimelineTime( |
749 | 0 | mPendingReadyTime.Value(), mStartTime.Value(), mPlaybackRate); |
750 | 0 |
|
751 | 0 | result = StartTimeFromTimelineTime( |
752 | 0 | mPendingReadyTime.Value(), currentTimeToMatch, *mPendingPlaybackRate); |
753 | 0 | return result; |
754 | 0 | } |
755 | 0 |
|
756 | 0 | if (!mStartTime.IsNull()) { |
757 | 0 | result = mStartTime; |
758 | 0 | return result; |
759 | 0 | } |
760 | 0 | |
761 | 0 | if (mPendingReadyTime.IsNull() || mHoldTime.IsNull()) { |
762 | 0 | return result; |
763 | 0 | } |
764 | 0 | |
765 | 0 | // Calculate the equivalent start time from the pending ready time. |
766 | 0 | result = StartTimeFromTimelineTime( |
767 | 0 | mPendingReadyTime.Value(), mHoldTime.Value(), mPlaybackRate); |
768 | 0 |
|
769 | 0 | return result; |
770 | 0 | } |
771 | | |
772 | | TimeStamp |
773 | | Animation::AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const |
774 | 0 | { |
775 | 0 | // Initializes to null. Return the same object every time to benefit from |
776 | 0 | // return-value-optimization. |
777 | 0 | TimeStamp result; |
778 | 0 |
|
779 | 0 | // We *don't* check for mTimeline->TracksWallclockTime() here because that |
780 | 0 | // method only tells us if the timeline times can be converted to |
781 | 0 | // TimeStamps that can be compared to TimeStamp::Now() or not, *not* |
782 | 0 | // whether the timelines can be converted to TimeStamp values at all. |
783 | 0 | // |
784 | 0 | // Furthermore, we want to be able to use this method when the refresh driver |
785 | 0 | // is under test control (in which case TracksWallclockTime() will return |
786 | 0 | // false). |
787 | 0 | // |
788 | 0 | // Once we introduce timelines that are not time-based we will need to |
789 | 0 | // differentiate between them here and determine how to sort their events. |
790 | 0 | if (!mTimeline) { |
791 | 0 | return result; |
792 | 0 | } |
793 | 0 | |
794 | 0 | // Check the time is convertible to a timestamp |
795 | 0 | if (aTime == TimeDuration::Forever() || |
796 | 0 | mPlaybackRate == 0.0 || |
797 | 0 | mStartTime.IsNull()) { |
798 | 0 | return result; |
799 | 0 | } |
800 | 0 | |
801 | 0 | // Invert the standard relation: |
802 | 0 | // current time = (timeline time - start time) * playback rate |
803 | 0 | TimeDuration timelineTime = |
804 | 0 | TimeDuration(aTime).MultDouble(1.0 / mPlaybackRate) + mStartTime.Value(); |
805 | 0 |
|
806 | 0 | result = mTimeline->ToTimeStamp(timelineTime); |
807 | 0 | return result; |
808 | 0 | } |
809 | | |
810 | | TimeStamp |
811 | | Animation::ElapsedTimeToTimeStamp( |
812 | | const StickyTimeDuration& aElapsedTime) const |
813 | 0 | { |
814 | 0 | TimeDuration delay = mEffect |
815 | 0 | ? mEffect->SpecifiedTiming().Delay() |
816 | 0 | : TimeDuration(); |
817 | 0 | return AnimationTimeToTimeStamp(aElapsedTime + delay); |
818 | 0 | } |
819 | | |
820 | | // https://drafts.csswg.org/web-animations/#silently-set-the-current-time |
821 | | void |
822 | | Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) |
823 | 0 | { |
824 | 0 | if (!mHoldTime.IsNull() || |
825 | 0 | mStartTime.IsNull() || |
826 | 0 | !mTimeline || |
827 | 0 | mTimeline->GetCurrentTime().IsNull() || |
828 | 0 | mPlaybackRate == 0.0) { |
829 | 0 | mHoldTime.SetValue(aSeekTime); |
830 | 0 | if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { |
831 | 0 | mStartTime.SetNull(); |
832 | 0 | } |
833 | 0 | } else { |
834 | 0 | mStartTime = StartTimeFromTimelineTime( |
835 | 0 | mTimeline->GetCurrentTime().Value(), aSeekTime, mPlaybackRate); |
836 | 0 | } |
837 | 0 |
|
838 | 0 | mPreviousCurrentTime.SetNull(); |
839 | 0 | } |
840 | | |
841 | | // https://drafts.csswg.org/web-animations/#cancel-an-animation |
842 | | void |
843 | | Animation::CancelNoUpdate() |
844 | 0 | { |
845 | 0 | if (PlayState() != AnimationPlayState::Idle) { |
846 | 0 | ResetPendingTasks(); |
847 | 0 |
|
848 | 0 | if (mFinished) { |
849 | 0 | mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR); |
850 | 0 | } |
851 | 0 | ResetFinishedPromise(); |
852 | 0 |
|
853 | 0 | QueuePlaybackEvent(NS_LITERAL_STRING("cancel"), |
854 | 0 | GetTimelineCurrentTimeAsTimeStamp()); |
855 | 0 | } |
856 | 0 |
|
857 | 0 | StickyTimeDuration activeTime = mEffect |
858 | 0 | ? mEffect->GetComputedTiming().mActiveTime |
859 | 0 | : StickyTimeDuration(); |
860 | 0 |
|
861 | 0 | mHoldTime.SetNull(); |
862 | 0 | mStartTime.SetNull(); |
863 | 0 |
|
864 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
865 | 0 |
|
866 | 0 | if (mTimeline) { |
867 | 0 | mTimeline->RemoveAnimation(this); |
868 | 0 | } |
869 | 0 | MaybeQueueCancelEvent(activeTime); |
870 | 0 | } |
871 | | |
872 | | bool |
873 | | Animation::ShouldBeSynchronizedWithMainThread( |
874 | | nsCSSPropertyID aProperty, |
875 | | const nsIFrame* aFrame, |
876 | | AnimationPerformanceWarning::Type& aPerformanceWarning) const |
877 | 0 | { |
878 | 0 | // Only synchronize playing animations |
879 | 0 | if (!IsPlaying()) { |
880 | 0 | return false; |
881 | 0 | } |
882 | 0 | |
883 | 0 | // Currently only transform animations need to be synchronized |
884 | 0 | if (aProperty != eCSSProperty_transform) { |
885 | 0 | return false; |
886 | 0 | } |
887 | 0 | |
888 | 0 | KeyframeEffect* keyframeEffect = mEffect |
889 | 0 | ? mEffect->AsKeyframeEffect() |
890 | 0 | : nullptr; |
891 | 0 | if (!keyframeEffect) { |
892 | 0 | return false; |
893 | 0 | } |
894 | 0 | |
895 | 0 | // Are we starting at the same time as other geometric animations? |
896 | 0 | // We check this before calling ShouldBlockAsyncTransformAnimations, partly |
897 | 0 | // because it's cheaper, but also because it's often the most useful thing |
898 | 0 | // to know when you're debugging performance. |
899 | 0 | if (mSyncWithGeometricAnimations && |
900 | 0 | keyframeEffect->HasAnimationOfProperty(eCSSProperty_transform)) { |
901 | 0 | aPerformanceWarning = AnimationPerformanceWarning::Type:: |
902 | 0 | TransformWithSyncGeometricAnimations; |
903 | 0 | return true; |
904 | 0 | } |
905 | 0 | |
906 | 0 | return keyframeEffect-> |
907 | 0 | ShouldBlockAsyncTransformAnimations(aFrame, aPerformanceWarning); |
908 | 0 | } |
909 | | |
910 | | void |
911 | | Animation::UpdateRelevance() |
912 | 0 | { |
913 | 0 | bool wasRelevant = mIsRelevant; |
914 | 0 | mIsRelevant = HasCurrentEffect() || IsInEffect(); |
915 | 0 |
|
916 | 0 | // Notify animation observers. |
917 | 0 | if (wasRelevant && !mIsRelevant) { |
918 | 0 | nsNodeUtils::AnimationRemoved(this); |
919 | 0 | } else if (!wasRelevant && mIsRelevant) { |
920 | 0 | nsNodeUtils::AnimationAdded(this); |
921 | 0 | } |
922 | 0 | } |
923 | | |
924 | | bool |
925 | | Animation::HasLowerCompositeOrderThan(const Animation& aOther) const |
926 | 0 | { |
927 | 0 | // 0. Object-equality case |
928 | 0 | if (&aOther == this) { |
929 | 0 | return false; |
930 | 0 | } |
931 | 0 | |
932 | 0 | // 1. CSS Transitions sort lowest |
933 | 0 | { |
934 | 0 | auto asCSSTransitionForSorting = |
935 | 0 | [] (const Animation& anim) -> const CSSTransition* |
936 | 0 | { |
937 | 0 | const CSSTransition* transition = anim.AsCSSTransition(); |
938 | 0 | return transition && transition->IsTiedToMarkup() ? |
939 | 0 | transition : |
940 | 0 | nullptr; |
941 | 0 | }; |
942 | 0 | auto thisTransition = asCSSTransitionForSorting(*this); |
943 | 0 | auto otherTransition = asCSSTransitionForSorting(aOther); |
944 | 0 | if (thisTransition && otherTransition) { |
945 | 0 | return thisTransition->HasLowerCompositeOrderThan(*otherTransition); |
946 | 0 | } |
947 | 0 | if (thisTransition || otherTransition) { |
948 | 0 | // Cancelled transitions no longer have an owning element. To be strictly |
949 | 0 | // correct we should store a strong reference to the owning element |
950 | 0 | // so that if we arrive here while sorting cancel events, we can sort |
951 | 0 | // them in the correct order. |
952 | 0 | // |
953 | 0 | // However, given that cancel events are almost always queued |
954 | 0 | // synchronously in some deterministic manner, we can be fairly sure |
955 | 0 | // that cancel events will be dispatched in a deterministic order |
956 | 0 | // (which is our only hard requirement until specs say otherwise). |
957 | 0 | // Furthermore, we only reach here when we have events with equal |
958 | 0 | // timestamps so this is an edge case we can probably ignore for now. |
959 | 0 | return thisTransition; |
960 | 0 | } |
961 | 0 | } |
962 | 0 | |
963 | 0 | // 2. CSS Animations sort next |
964 | 0 | { |
965 | 0 | auto asCSSAnimationForSorting = |
966 | 0 | [] (const Animation& anim) -> const CSSAnimation* |
967 | 0 | { |
968 | 0 | const CSSAnimation* animation = anim.AsCSSAnimation(); |
969 | 0 | return animation && animation->IsTiedToMarkup() ? animation : nullptr; |
970 | 0 | }; |
971 | 0 | auto thisAnimation = asCSSAnimationForSorting(*this); |
972 | 0 | auto otherAnimation = asCSSAnimationForSorting(aOther); |
973 | 0 | if (thisAnimation && otherAnimation) { |
974 | 0 | return thisAnimation->HasLowerCompositeOrderThan(*otherAnimation); |
975 | 0 | } |
976 | 0 | if (thisAnimation || otherAnimation) { |
977 | 0 | return thisAnimation; |
978 | 0 | } |
979 | 0 | } |
980 | 0 | |
981 | 0 | // Subclasses of Animation repurpose mAnimationIndex to implement their |
982 | 0 | // own brand of composite ordering. However, by this point we should have |
983 | 0 | // handled any such custom composite ordering so we should now have unique |
984 | 0 | // animation indices. |
985 | 0 | MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex, |
986 | 0 | "Animation indices should be unique"); |
987 | 0 |
|
988 | 0 | // 3. Finally, generic animations sort by their position in the global |
989 | 0 | // animation array. |
990 | 0 | return mAnimationIndex < aOther.mAnimationIndex; |
991 | 0 | } |
992 | | |
993 | | void |
994 | | Animation::WillComposeStyle() |
995 | 0 | { |
996 | 0 | mFinishedAtLastComposeStyle = (PlayState() == AnimationPlayState::Finished); |
997 | 0 |
|
998 | 0 | MOZ_ASSERT(mEffect); |
999 | 0 |
|
1000 | 0 | KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); |
1001 | 0 | if (keyframeEffect) { |
1002 | 0 | keyframeEffect->WillComposeStyle(); |
1003 | 0 | } |
1004 | 0 | } |
1005 | | |
1006 | | void |
1007 | | Animation::ComposeStyle(RawServoAnimationValueMap& aComposeResult, |
1008 | | const nsCSSPropertyIDSet& aPropertiesToSkip) |
1009 | 0 | { |
1010 | 0 | if (!mEffect) { |
1011 | 0 | return; |
1012 | 0 | } |
1013 | 0 | |
1014 | 0 | // In order to prevent flicker, there are a few cases where we want to use |
1015 | 0 | // a different time for rendering that would otherwise be returned by |
1016 | 0 | // GetCurrentTime. These are: |
1017 | 0 | // |
1018 | 0 | // (a) For animations that are pausing but which are still running on the |
1019 | 0 | // compositor. In this case we send a layer transaction that removes the |
1020 | 0 | // animation but which also contains the animation values calculated on |
1021 | 0 | // the main thread. To prevent flicker when this occurs we want to ensure |
1022 | 0 | // the timeline time used to calculate the main thread animation values |
1023 | 0 | // does not lag far behind the time used on the compositor. Ideally we |
1024 | 0 | // would like to use the "animation ready time" calculated at the end of |
1025 | 0 | // the layer transaction as the timeline time but it will be too late to |
1026 | 0 | // update the style rule at that point so instead we just use the current |
1027 | 0 | // wallclock time. |
1028 | 0 | // |
1029 | 0 | // (b) For animations that are pausing that we have already taken off the |
1030 | 0 | // compositor. In this case we record a pending ready time but we don't |
1031 | 0 | // apply it until the next tick. However, while waiting for the next tick, |
1032 | 0 | // we should still use the pending ready time as the timeline time. If we |
1033 | 0 | // use the regular timeline time the animation may appear jump backwards |
1034 | 0 | // if the main thread's timeline time lags behind the compositor. |
1035 | 0 | // |
1036 | 0 | // (c) For animations that are play-pending due to an aborted pause operation |
1037 | 0 | // (i.e. a pause operation that was interrupted before we entered the |
1038 | 0 | // paused state). When we cancel a pending pause we might momentarily take |
1039 | 0 | // the animation off the compositor, only to re-add it moments later. In |
1040 | 0 | // that case the compositor might have been ahead of the main thread so we |
1041 | 0 | // should use the current wallclock time to ensure the animation doesn't |
1042 | 0 | // temporarily jump backwards. |
1043 | 0 | // |
1044 | 0 | // To address each of these cases we temporarily tweak the hold time |
1045 | 0 | // immediately before updating the style rule and then restore it immediately |
1046 | 0 | // afterwards. This is purely to prevent visual flicker. Other behavior |
1047 | 0 | // such as dispatching events continues to rely on the regular timeline time. |
1048 | 0 | bool pending = Pending(); |
1049 | 0 | { |
1050 | 0 | AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime); |
1051 | 0 |
|
1052 | 0 | if (pending && mHoldTime.IsNull() && !mStartTime.IsNull()) { |
1053 | 0 | Nullable<TimeDuration> timeToUse = mPendingReadyTime; |
1054 | 0 | if (timeToUse.IsNull() && |
1055 | 0 | mTimeline && |
1056 | 0 | mTimeline->TracksWallclockTime()) { |
1057 | 0 | timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now()); |
1058 | 0 | } |
1059 | 0 | if (!timeToUse.IsNull()) { |
1060 | 0 | mHoldTime = CurrentTimeFromTimelineTime( |
1061 | 0 | timeToUse.Value(), mStartTime.Value(), mPlaybackRate); |
1062 | 0 | } |
1063 | 0 | } |
1064 | 0 |
|
1065 | 0 | KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); |
1066 | 0 | if (keyframeEffect) { |
1067 | 0 | keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip); |
1068 | 0 | } |
1069 | 0 | } |
1070 | 0 |
|
1071 | 0 | MOZ_ASSERT(pending == Pending(), |
1072 | 0 | "Pending state should not change during the course of compositing"); |
1073 | 0 | } |
1074 | | |
1075 | | void |
1076 | | Animation::NotifyEffectTimingUpdated() |
1077 | 0 | { |
1078 | 0 | MOZ_ASSERT(mEffect, |
1079 | 0 | "We should only update timing effect when we have a target " |
1080 | 0 | "effect"); |
1081 | 0 | UpdateTiming(Animation::SeekFlag::NoSeek, |
1082 | 0 | Animation::SyncNotifyFlag::Async); |
1083 | 0 | } |
1084 | | |
1085 | | void |
1086 | | Animation::NotifyGeometricAnimationsStartingThisFrame() |
1087 | 0 | { |
1088 | 0 | if (!IsNewlyStarted() || !mEffect) { |
1089 | 0 | return; |
1090 | 0 | } |
1091 | 0 | |
1092 | 0 | mSyncWithGeometricAnimations = true; |
1093 | 0 | } |
1094 | | |
1095 | | // https://drafts.csswg.org/web-animations/#play-an-animation |
1096 | | void |
1097 | | Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) |
1098 | 0 | { |
1099 | 0 | AutoMutationBatchForAnimation mb(*this); |
1100 | 0 |
|
1101 | 0 | bool abortedPause = mPendingState == PendingState::PausePending; |
1102 | 0 |
|
1103 | 0 | double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); |
1104 | 0 |
|
1105 | 0 | Nullable<TimeDuration> currentTime = GetCurrentTime(); |
1106 | 0 | if (effectivePlaybackRate > 0.0 && |
1107 | 0 | (currentTime.IsNull() || |
1108 | 0 | (aLimitBehavior == LimitBehavior::AutoRewind && |
1109 | 0 | (currentTime.Value() < TimeDuration() || |
1110 | 0 | currentTime.Value() >= EffectEnd())))) { |
1111 | 0 | mHoldTime.SetValue(TimeDuration(0)); |
1112 | 0 | } else if (effectivePlaybackRate < 0.0 && |
1113 | 0 | (currentTime.IsNull() || |
1114 | 0 | (aLimitBehavior == LimitBehavior::AutoRewind && |
1115 | 0 | (currentTime.Value() <= TimeDuration() || |
1116 | 0 | currentTime.Value() > EffectEnd())))) { |
1117 | 0 | if (EffectEnd() == TimeDuration::Forever()) { |
1118 | 0 | aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
1119 | 0 | return; |
1120 | 0 | } |
1121 | 0 | mHoldTime.SetValue(TimeDuration(EffectEnd())); |
1122 | 0 | } else if (effectivePlaybackRate == 0.0 && currentTime.IsNull()) { |
1123 | 0 | mHoldTime.SetValue(TimeDuration(0)); |
1124 | 0 | } |
1125 | 0 |
|
1126 | 0 | bool reuseReadyPromise = false; |
1127 | 0 | if (mPendingState != PendingState::NotPending) { |
1128 | 0 | CancelPendingTasks(); |
1129 | 0 | reuseReadyPromise = true; |
1130 | 0 | } |
1131 | 0 |
|
1132 | 0 | // If the hold time is null then we're already playing normally and, |
1133 | 0 | // typically, we can bail out here. |
1134 | 0 | // |
1135 | 0 | // However, there are two cases where we can't do that: |
1136 | 0 | // |
1137 | 0 | // (a) If we just aborted a pause. In this case, for consistency, we need to |
1138 | 0 | // go through the motions of doing an asynchronous start. |
1139 | 0 | // |
1140 | 0 | // (b) If we have timing changes (specifically a change to the playbackRate) |
1141 | 0 | // that should be applied asynchronously. |
1142 | 0 | // |
1143 | 0 | if (mHoldTime.IsNull() && !abortedPause && !mPendingPlaybackRate) { |
1144 | 0 | return; |
1145 | 0 | } |
1146 | 0 | |
1147 | 0 | // Clear the start time until we resolve a new one. We do this except |
1148 | 0 | // for the case where we are aborting a pause and don't have a hold time. |
1149 | 0 | // |
1150 | 0 | // If we're aborting a pause and *do* have a hold time (e.g. because |
1151 | 0 | // the animation is finished or we just applied the auto-rewind behavior |
1152 | 0 | // above) we should respect it by clearing the start time. If we *don't* |
1153 | 0 | // have a hold time we should keep the current start time so that the |
1154 | 0 | // the animation continues moving uninterrupted by the aborted pause. |
1155 | 0 | // |
1156 | 0 | // (If we're not aborting a pause, mHoldTime must be resolved by now |
1157 | 0 | // or else we would have returned above.) |
1158 | 0 | if (!mHoldTime.IsNull()) { |
1159 | 0 | mStartTime.SetNull(); |
1160 | 0 | } |
1161 | 0 |
|
1162 | 0 | if (!reuseReadyPromise) { |
1163 | 0 | // Clear ready promise. We'll create a new one lazily. |
1164 | 0 | mReady = nullptr; |
1165 | 0 | } |
1166 | 0 |
|
1167 | 0 | mPendingState = PendingState::PlayPending; |
1168 | 0 |
|
1169 | 0 | // Clear flag that causes us to sync transform animations with the main |
1170 | 0 | // thread for now. We'll set this when we go to set up compositor |
1171 | 0 | // animations if it applies. |
1172 | 0 | mSyncWithGeometricAnimations = false; |
1173 | 0 |
|
1174 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1175 | 0 | if (doc) { |
1176 | 0 | PendingAnimationTracker* tracker = |
1177 | 0 | doc->GetOrCreatePendingAnimationTracker(); |
1178 | 0 | tracker->AddPlayPending(*this); |
1179 | 0 | } else { |
1180 | 0 | TriggerOnNextTick(Nullable<TimeDuration>()); |
1181 | 0 | } |
1182 | 0 |
|
1183 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
1184 | 0 | if (IsRelevant()) { |
1185 | 0 | nsNodeUtils::AnimationChanged(this); |
1186 | 0 | } |
1187 | 0 | } |
1188 | | |
1189 | | // https://drafts.csswg.org/web-animations/#pause-an-animation |
1190 | | void |
1191 | | Animation::Pause(ErrorResult& aRv) |
1192 | 0 | { |
1193 | 0 | if (IsPausedOrPausing()) { |
1194 | 0 | return; |
1195 | 0 | } |
1196 | 0 | |
1197 | 0 | AutoMutationBatchForAnimation mb(*this); |
1198 | 0 |
|
1199 | 0 | // If we are transitioning from idle, fill in the current time |
1200 | 0 | if (GetCurrentTime().IsNull()) { |
1201 | 0 | if (mPlaybackRate >= 0.0) { |
1202 | 0 | mHoldTime.SetValue(TimeDuration(0)); |
1203 | 0 | } else { |
1204 | 0 | if (EffectEnd() == TimeDuration::Forever()) { |
1205 | 0 | aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); |
1206 | 0 | return; |
1207 | 0 | } |
1208 | 0 | mHoldTime.SetValue(TimeDuration(EffectEnd())); |
1209 | 0 | } |
1210 | 0 | } |
1211 | 0 |
|
1212 | 0 | bool reuseReadyPromise = false; |
1213 | 0 | if (mPendingState == PendingState::PlayPending) { |
1214 | 0 | CancelPendingTasks(); |
1215 | 0 | reuseReadyPromise = true; |
1216 | 0 | } |
1217 | 0 |
|
1218 | 0 | if (!reuseReadyPromise) { |
1219 | 0 | // Clear ready promise. We'll create a new one lazily. |
1220 | 0 | mReady = nullptr; |
1221 | 0 | } |
1222 | 0 |
|
1223 | 0 | mPendingState = PendingState::PausePending; |
1224 | 0 |
|
1225 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1226 | 0 | if (doc) { |
1227 | 0 | PendingAnimationTracker* tracker = |
1228 | 0 | doc->GetOrCreatePendingAnimationTracker(); |
1229 | 0 | tracker->AddPausePending(*this); |
1230 | 0 | } else { |
1231 | 0 | TriggerOnNextTick(Nullable<TimeDuration>()); |
1232 | 0 | } |
1233 | 0 |
|
1234 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
1235 | 0 | if (IsRelevant()) { |
1236 | 0 | nsNodeUtils::AnimationChanged(this); |
1237 | 0 | } |
1238 | 0 |
|
1239 | 0 | PostUpdate(); |
1240 | 0 | } |
1241 | | |
1242 | | // https://drafts.csswg.org/web-animations/#play-an-animation |
1243 | | void |
1244 | | Animation::ResumeAt(const TimeDuration& aReadyTime) |
1245 | 0 | { |
1246 | 0 | // This method is only expected to be called for an animation that is |
1247 | 0 | // waiting to play. We can easily adapt it to handle other states |
1248 | 0 | // but it's currently not necessary. |
1249 | 0 | MOZ_ASSERT(mPendingState == PendingState::PlayPending, |
1250 | 0 | "Expected to resume a play-pending animation"); |
1251 | 0 | MOZ_ASSERT(!mHoldTime.IsNull() || !mStartTime.IsNull(), |
1252 | 0 | "An animation in the play-pending state should have either a" |
1253 | 0 | " resolved hold time or resolved start time"); |
1254 | 0 |
|
1255 | 0 | AutoMutationBatchForAnimation mb(*this); |
1256 | 0 | bool hadPendingPlaybackRate = mPendingPlaybackRate.isSome(); |
1257 | 0 |
|
1258 | 0 | if (!mHoldTime.IsNull()) { |
1259 | 0 | // The hold time is set, so we don't need any special handling to preserve |
1260 | 0 | // the current time. |
1261 | 0 | ApplyPendingPlaybackRate(); |
1262 | 0 | mStartTime = |
1263 | 0 | StartTimeFromTimelineTime(aReadyTime, mHoldTime.Value(), mPlaybackRate); |
1264 | 0 | if (mPlaybackRate != 0) { |
1265 | 0 | mHoldTime.SetNull(); |
1266 | 0 | } |
1267 | 0 | } else if (!mStartTime.IsNull() && mPendingPlaybackRate) { |
1268 | 0 | // Apply any pending playback rate, preserving the current time. |
1269 | 0 | TimeDuration currentTimeToMatch = CurrentTimeFromTimelineTime( |
1270 | 0 | aReadyTime, mStartTime.Value(), mPlaybackRate); |
1271 | 0 | ApplyPendingPlaybackRate(); |
1272 | 0 | mStartTime = |
1273 | 0 | StartTimeFromTimelineTime(aReadyTime, currentTimeToMatch, mPlaybackRate); |
1274 | 0 | if (mPlaybackRate == 0) { |
1275 | 0 | mHoldTime.SetValue(currentTimeToMatch); |
1276 | 0 | } |
1277 | 0 | } |
1278 | 0 |
|
1279 | 0 | mPendingState = PendingState::NotPending; |
1280 | 0 |
|
1281 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
1282 | 0 |
|
1283 | 0 | // If we had a pending playback rate, we will have now applied it so we need |
1284 | 0 | // to notify observers. |
1285 | 0 | if (hadPendingPlaybackRate && IsRelevant()) { |
1286 | 0 | nsNodeUtils::AnimationChanged(this); |
1287 | 0 | } |
1288 | 0 |
|
1289 | 0 | if (mReady) { |
1290 | 0 | mReady->MaybeResolve(this); |
1291 | 0 | } |
1292 | 0 | } |
1293 | | |
1294 | | void |
1295 | | Animation::PauseAt(const TimeDuration& aReadyTime) |
1296 | 0 | { |
1297 | 0 | MOZ_ASSERT(mPendingState == PendingState::PausePending, |
1298 | 0 | "Expected to pause a pause-pending animation"); |
1299 | 0 |
|
1300 | 0 | if (!mStartTime.IsNull() && mHoldTime.IsNull()) { |
1301 | 0 | mHoldTime = CurrentTimeFromTimelineTime( |
1302 | 0 | aReadyTime, mStartTime.Value(), mPlaybackRate); |
1303 | 0 | } |
1304 | 0 | ApplyPendingPlaybackRate(); |
1305 | 0 | mStartTime.SetNull(); |
1306 | 0 | mPendingState = PendingState::NotPending; |
1307 | 0 |
|
1308 | 0 | UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); |
1309 | 0 |
|
1310 | 0 | if (mReady) { |
1311 | 0 | mReady->MaybeResolve(this); |
1312 | 0 | } |
1313 | 0 | } |
1314 | | |
1315 | | void |
1316 | | Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) |
1317 | 0 | { |
1318 | 0 | // We call UpdateFinishedState before UpdateEffect because the former |
1319 | 0 | // can change the current time, which is used by the latter. |
1320 | 0 | UpdateFinishedState(aSeekFlag, aSyncNotifyFlag); |
1321 | 0 | UpdateEffect(); |
1322 | 0 |
|
1323 | 0 | if (mTimeline) { |
1324 | 0 | mTimeline->NotifyAnimationUpdated(*this); |
1325 | 0 | } |
1326 | 0 | } |
1327 | | |
1328 | | // https://drafts.csswg.org/web-animations/#update-an-animations-finished-state |
1329 | | void |
1330 | | Animation::UpdateFinishedState(SeekFlag aSeekFlag, |
1331 | | SyncNotifyFlag aSyncNotifyFlag) |
1332 | 0 | { |
1333 | 0 | Nullable<TimeDuration> currentTime = GetCurrentTime(); |
1334 | 0 | TimeDuration effectEnd = TimeDuration(EffectEnd()); |
1335 | 0 |
|
1336 | 0 | if (!mStartTime.IsNull() && |
1337 | 0 | mPendingState == PendingState::NotPending) { |
1338 | 0 | if (mPlaybackRate > 0.0 && |
1339 | 0 | !currentTime.IsNull() && |
1340 | 0 | currentTime.Value() >= effectEnd) { |
1341 | 0 | if (aSeekFlag == SeekFlag::DidSeek) { |
1342 | 0 | mHoldTime = currentTime; |
1343 | 0 | } else if (!mPreviousCurrentTime.IsNull()) { |
1344 | 0 | mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd)); |
1345 | 0 | } else { |
1346 | 0 | mHoldTime.SetValue(effectEnd); |
1347 | 0 | } |
1348 | 0 | } else if (mPlaybackRate < 0.0 && |
1349 | 0 | !currentTime.IsNull() && |
1350 | 0 | currentTime.Value() <= TimeDuration()) { |
1351 | 0 | if (aSeekFlag == SeekFlag::DidSeek) { |
1352 | 0 | mHoldTime = currentTime; |
1353 | 0 | } else if (!mPreviousCurrentTime.IsNull()) { |
1354 | 0 | mHoldTime.SetValue(std::min(mPreviousCurrentTime.Value(), |
1355 | 0 | TimeDuration(0))); |
1356 | 0 | } else { |
1357 | 0 | mHoldTime.SetValue(0); |
1358 | 0 | } |
1359 | 0 | } else if (mPlaybackRate != 0.0 && |
1360 | 0 | !currentTime.IsNull() && |
1361 | 0 | mTimeline && |
1362 | 0 | !mTimeline->GetCurrentTime().IsNull()) { |
1363 | 0 | if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) { |
1364 | 0 | mStartTime = |
1365 | 0 | StartTimeFromTimelineTime(mTimeline->GetCurrentTime().Value(), |
1366 | 0 | mHoldTime.Value(), |
1367 | 0 | mPlaybackRate); |
1368 | 0 | } |
1369 | 0 | mHoldTime.SetNull(); |
1370 | 0 | } |
1371 | 0 | } |
1372 | 0 |
|
1373 | 0 | bool currentFinishedState = PlayState() == AnimationPlayState::Finished; |
1374 | 0 | if (currentFinishedState && !mFinishedIsResolved) { |
1375 | 0 | DoFinishNotification(aSyncNotifyFlag); |
1376 | 0 | } else if (!currentFinishedState && mFinishedIsResolved) { |
1377 | 0 | ResetFinishedPromise(); |
1378 | 0 | } |
1379 | 0 | // We must recalculate the current time to take account of any mHoldTime |
1380 | 0 | // changes the code above made. |
1381 | 0 | mPreviousCurrentTime = GetCurrentTime(); |
1382 | 0 | } |
1383 | | |
1384 | | void |
1385 | | Animation::UpdateEffect() |
1386 | 0 | { |
1387 | 0 | if (mEffect) { |
1388 | 0 | UpdateRelevance(); |
1389 | 0 |
|
1390 | 0 | KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); |
1391 | 0 | if (keyframeEffect) { |
1392 | 0 | keyframeEffect->NotifyAnimationTimingUpdated(); |
1393 | 0 | } |
1394 | 0 | } |
1395 | 0 | } |
1396 | | |
1397 | | void |
1398 | | Animation::FlushUnanimatedStyle() const |
1399 | 0 | { |
1400 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1401 | 0 | if (doc) { |
1402 | 0 | doc->FlushPendingNotifications( |
1403 | 0 | ChangesToFlush(FlushType::Style, false /* flush animations */)); |
1404 | 0 | } |
1405 | 0 | } |
1406 | | |
1407 | | void |
1408 | | Animation::PostUpdate() |
1409 | 0 | { |
1410 | 0 | if (!mEffect) { |
1411 | 0 | return; |
1412 | 0 | } |
1413 | 0 | |
1414 | 0 | KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); |
1415 | 0 | if (!keyframeEffect) { |
1416 | 0 | return; |
1417 | 0 | } |
1418 | 0 | keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Layer); |
1419 | 0 | } |
1420 | | |
1421 | | void |
1422 | | Animation::CancelPendingTasks() |
1423 | 0 | { |
1424 | 0 | if (mPendingState == PendingState::NotPending) { |
1425 | 0 | return; |
1426 | 0 | } |
1427 | 0 | |
1428 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1429 | 0 | if (doc) { |
1430 | 0 | PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); |
1431 | 0 | if (tracker) { |
1432 | 0 | if (mPendingState == PendingState::PlayPending) { |
1433 | 0 | tracker->RemovePlayPending(*this); |
1434 | 0 | } else { |
1435 | 0 | tracker->RemovePausePending(*this); |
1436 | 0 | } |
1437 | 0 | } |
1438 | 0 | } |
1439 | 0 |
|
1440 | 0 | mPendingState = PendingState::NotPending; |
1441 | 0 | mPendingReadyTime.SetNull(); |
1442 | 0 | } |
1443 | | |
1444 | | // https://drafts.csswg.org/web-animations/#reset-an-animations-pending-tasks |
1445 | | void |
1446 | | Animation::ResetPendingTasks() |
1447 | 0 | { |
1448 | 0 | if (mPendingState == PendingState::NotPending) { |
1449 | 0 | return; |
1450 | 0 | } |
1451 | 0 | |
1452 | 0 | CancelPendingTasks(); |
1453 | 0 | ApplyPendingPlaybackRate(); |
1454 | 0 |
|
1455 | 0 | if (mReady) { |
1456 | 0 | mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR); |
1457 | 0 | mReady = nullptr; |
1458 | 0 | } |
1459 | 0 | } |
1460 | | |
1461 | | void |
1462 | | Animation::ReschedulePendingTasks() |
1463 | 0 | { |
1464 | 0 | if (mPendingState == PendingState::NotPending) { |
1465 | 0 | return; |
1466 | 0 | } |
1467 | 0 | |
1468 | 0 | mPendingReadyTime.SetNull(); |
1469 | 0 |
|
1470 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1471 | 0 | if (doc) { |
1472 | 0 | PendingAnimationTracker* tracker = |
1473 | 0 | doc->GetOrCreatePendingAnimationTracker(); |
1474 | 0 | if (mPendingState == PendingState::PlayPending && |
1475 | 0 | !tracker->IsWaitingToPlay(*this)) { |
1476 | 0 | tracker->AddPlayPending(*this); |
1477 | 0 | } else if (mPendingState == PendingState::PausePending && |
1478 | 0 | !tracker->IsWaitingToPause(*this)) { |
1479 | 0 | tracker->AddPausePending(*this); |
1480 | 0 | } |
1481 | 0 | } |
1482 | 0 | } |
1483 | | |
1484 | | bool |
1485 | | Animation::IsPossiblyOrphanedPendingAnimation() const |
1486 | 0 | { |
1487 | 0 | // Check if we are pending but might never start because we are not being |
1488 | 0 | // tracked. |
1489 | 0 | // |
1490 | 0 | // This covers the following cases: |
1491 | 0 | // |
1492 | 0 | // * We started playing but our effect's target element was orphaned |
1493 | 0 | // or bound to a different document. |
1494 | 0 | // (note that for the case of our effect changing we should handle |
1495 | 0 | // that in SetEffect) |
1496 | 0 | // * We started playing but our timeline became inactive. |
1497 | 0 | // In this case the pending animation tracker will drop us from its hashmap |
1498 | 0 | // when we have been painted. |
1499 | 0 | // * When we started playing we couldn't find a PendingAnimationTracker to |
1500 | 0 | // register with (perhaps the effect had no document) so we simply |
1501 | 0 | // set mPendingState in PlayNoUpdate and relied on this method to catch us |
1502 | 0 | // on the next tick. |
1503 | 0 |
|
1504 | 0 | // If we're not pending we're ok. |
1505 | 0 | if (mPendingState == PendingState::NotPending) { |
1506 | 0 | return false; |
1507 | 0 | } |
1508 | 0 | |
1509 | 0 | // If we have a pending ready time then we will be started on the next |
1510 | 0 | // tick. |
1511 | 0 | if (!mPendingReadyTime.IsNull()) { |
1512 | 0 | return false; |
1513 | 0 | } |
1514 | 0 | |
1515 | 0 | // If we don't have an active timeline then we shouldn't start until |
1516 | 0 | // we do. |
1517 | 0 | if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { |
1518 | 0 | return false; |
1519 | 0 | } |
1520 | 0 | |
1521 | 0 | // If we have no rendered document, or we're not in our rendered document's |
1522 | 0 | // PendingAnimationTracker then there's a good chance no one is tracking us. |
1523 | 0 | // |
1524 | 0 | // If we're wrong and another document is tracking us then, at worst, we'll |
1525 | 0 | // simply start/pause the animation one tick too soon. That's better than |
1526 | 0 | // never starting/pausing the animation and is unlikely. |
1527 | 0 | nsIDocument* doc = GetRenderedDocument(); |
1528 | 0 | if (!doc) { |
1529 | 0 | return true; |
1530 | 0 | } |
1531 | 0 | |
1532 | 0 | PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); |
1533 | 0 | return !tracker || |
1534 | 0 | (!tracker->IsWaitingToPlay(*this) && |
1535 | 0 | !tracker->IsWaitingToPause(*this)); |
1536 | 0 | } |
1537 | | |
1538 | | StickyTimeDuration |
1539 | | Animation::EffectEnd() const |
1540 | 0 | { |
1541 | 0 | if (!mEffect) { |
1542 | 0 | return StickyTimeDuration(0); |
1543 | 0 | } |
1544 | 0 | |
1545 | 0 | return mEffect->SpecifiedTiming().EndTime(); |
1546 | 0 | } |
1547 | | |
1548 | | nsIDocument* |
1549 | | Animation::GetRenderedDocument() const |
1550 | 0 | { |
1551 | 0 | if (!mEffect || !mEffect->AsKeyframeEffect()) { |
1552 | 0 | return nullptr; |
1553 | 0 | } |
1554 | 0 | |
1555 | 0 | return mEffect->AsKeyframeEffect()->GetRenderedDocument(); |
1556 | 0 | } |
1557 | | |
1558 | | nsIDocument* |
1559 | | Animation::GetTimelineDocument() const |
1560 | 0 | { |
1561 | 0 | return mTimeline ? mTimeline->GetDocument() : nullptr; |
1562 | 0 | } |
1563 | | |
1564 | | class AsyncFinishNotification : public MicroTaskRunnable |
1565 | | { |
1566 | | public: |
1567 | | explicit AsyncFinishNotification(Animation* aAnimation) |
1568 | | : MicroTaskRunnable() |
1569 | | , mAnimation(aAnimation) |
1570 | 0 | {} |
1571 | | |
1572 | | virtual void Run(AutoSlowOperation& aAso) override |
1573 | 0 | { |
1574 | 0 | mAnimation->DoFinishNotificationImmediately(this); |
1575 | 0 | mAnimation = nullptr; |
1576 | 0 | } |
1577 | | |
1578 | | virtual bool Suppressed() override |
1579 | 0 | { |
1580 | 0 | nsIGlobalObject* global = mAnimation->GetOwnerGlobal(); |
1581 | 0 | return global && global->IsInSyncOperation(); |
1582 | 0 | } |
1583 | | |
1584 | | private: |
1585 | | RefPtr<Animation> mAnimation; |
1586 | | }; |
1587 | | |
1588 | | void |
1589 | | Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) |
1590 | 0 | { |
1591 | 0 | CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); |
1592 | 0 |
|
1593 | 0 | if (aSyncNotifyFlag == SyncNotifyFlag::Sync) { |
1594 | 0 | DoFinishNotificationImmediately(); |
1595 | 0 | } else if (!mFinishNotificationTask) { |
1596 | 0 | RefPtr<MicroTaskRunnable> runnable = new AsyncFinishNotification(this); |
1597 | 0 | context->DispatchToMicroTask(do_AddRef(runnable)); |
1598 | 0 | mFinishNotificationTask = runnable.forget(); |
1599 | 0 | } |
1600 | 0 | } |
1601 | | |
1602 | | void |
1603 | | Animation::ResetFinishedPromise() |
1604 | 0 | { |
1605 | 0 | mFinishedIsResolved = false; |
1606 | 0 | mFinished = nullptr; |
1607 | 0 | } |
1608 | | |
1609 | | void |
1610 | | Animation::MaybeResolveFinishedPromise() |
1611 | 0 | { |
1612 | 0 | if (mFinished) { |
1613 | 0 | mFinished->MaybeResolve(this); |
1614 | 0 | } |
1615 | 0 | mFinishedIsResolved = true; |
1616 | 0 | } |
1617 | | |
1618 | | void |
1619 | | Animation::DoFinishNotificationImmediately(MicroTaskRunnable* aAsync) |
1620 | 0 | { |
1621 | 0 | if (aAsync && aAsync != mFinishNotificationTask) { |
1622 | 0 | return; |
1623 | 0 | } |
1624 | 0 | |
1625 | 0 | mFinishNotificationTask = nullptr; |
1626 | 0 |
|
1627 | 0 | if (PlayState() != AnimationPlayState::Finished) { |
1628 | 0 | return; |
1629 | 0 | } |
1630 | 0 | |
1631 | 0 | MaybeResolveFinishedPromise(); |
1632 | 0 |
|
1633 | 0 | QueuePlaybackEvent(NS_LITERAL_STRING("finish"), |
1634 | 0 | AnimationTimeToTimeStamp(EffectEnd())); |
1635 | 0 | } |
1636 | | |
1637 | | void |
1638 | | Animation::QueuePlaybackEvent(const nsAString& aName, |
1639 | | TimeStamp&& aScheduledEventTime) |
1640 | 0 | { |
1641 | 0 | // Use document for timing. |
1642 | 0 | // https://drafts.csswg.org/web-animations-1/#document-for-timing |
1643 | 0 | nsIDocument* doc = GetTimelineDocument(); |
1644 | 0 | if (!doc) { |
1645 | 0 | return; |
1646 | 0 | } |
1647 | 0 | |
1648 | 0 | nsPresContext* presContext = doc->GetPresContext(); |
1649 | 0 | if (!presContext) { |
1650 | 0 | return; |
1651 | 0 | } |
1652 | 0 | |
1653 | 0 | AnimationPlaybackEventInit init; |
1654 | 0 |
|
1655 | 0 | if (aName.EqualsLiteral("finish")) { |
1656 | 0 | init.mCurrentTime = GetCurrentTimeAsDouble(); |
1657 | 0 | } |
1658 | 0 | if (mTimeline) { |
1659 | 0 | init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble(); |
1660 | 0 | } |
1661 | 0 |
|
1662 | 0 | RefPtr<AnimationPlaybackEvent> event = |
1663 | 0 | AnimationPlaybackEvent::Constructor(this, aName, init); |
1664 | 0 | event->SetTrusted(true); |
1665 | 0 |
|
1666 | 0 | presContext->AnimationEventDispatcher()-> |
1667 | 0 | QueueEvent(AnimationEventInfo(aName, |
1668 | 0 | std::move(event), |
1669 | 0 | std::move(aScheduledEventTime), |
1670 | 0 | this)); |
1671 | 0 | } |
1672 | | |
1673 | | bool |
1674 | | Animation::IsRunningOnCompositor() const |
1675 | 0 | { |
1676 | 0 | return mEffect && |
1677 | 0 | mEffect->AsKeyframeEffect() && |
1678 | 0 | mEffect->AsKeyframeEffect()->IsRunningOnCompositor(); |
1679 | 0 | } |
1680 | | |
1681 | | |
1682 | | } // namespace dom |
1683 | | } // namespace mozilla |