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 "CSSAnimation.h"
8 
9 #include "mozilla/AnimationEventDispatcher.h"
10 #include "mozilla/dom/CSSAnimationBinding.h"
11 #include "mozilla/dom/KeyframeEffectBinding.h"
12 #include "mozilla/TimeStamp.h"
13 #include "nsPresContext.h"
14 
15 namespace mozilla::dom {
16 
17 using AnimationPhase = ComputedTiming::AnimationPhase;
18 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)19 JSObject* CSSAnimation::WrapObject(JSContext* aCx,
20                                    JS::Handle<JSObject*> aGivenProto) {
21   return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto);
22 }
23 
SetEffect(AnimationEffect * aEffect)24 void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
25   Animation::SetEffect(aEffect);
26 
27   AddOverriddenProperties(CSSAnimationProperties::Effect);
28 }
29 
SetStartTimeAsDouble(const Nullable<double> & aStartTime)30 void CSSAnimation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) {
31   // Note that we always compare with the paused state since for the purposes
32   // of determining if play control is being overridden or not, we want to
33   // treat the finished state as running.
34   bool wasPaused = PlayState() == AnimationPlayState::Paused;
35 
36   Animation::SetStartTimeAsDouble(aStartTime);
37 
38   bool isPaused = PlayState() == AnimationPlayState::Paused;
39 
40   if (wasPaused != isPaused) {
41     AddOverriddenProperties(CSSAnimationProperties::PlayState);
42   }
43 }
44 
GetReady(ErrorResult & aRv)45 mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
46   FlushUnanimatedStyle();
47   return Animation::GetReady(aRv);
48 }
49 
Reverse(ErrorResult & aRv)50 void CSSAnimation::Reverse(ErrorResult& aRv) {
51   // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in
52   // the paused state.
53   bool wasPaused = PlayState() == AnimationPlayState::Paused;
54 
55   Animation::Reverse(aRv);
56   if (aRv.Failed()) {
57     return;
58   }
59 
60   bool isPaused = PlayState() == AnimationPlayState::Paused;
61 
62   if (wasPaused != isPaused) {
63     AddOverriddenProperties(CSSAnimationProperties::PlayState);
64   }
65 }
66 
PlayStateFromJS() const67 AnimationPlayState CSSAnimation::PlayStateFromJS() const {
68   // Flush style to ensure that any properties controlling animation state
69   // (e.g. animation-play-state) are fully updated.
70   FlushUnanimatedStyle();
71   return Animation::PlayStateFromJS();
72 }
73 
PendingFromJS() const74 bool CSSAnimation::PendingFromJS() const {
75   // Flush style since, for example, if the animation-play-state was just
76   // changed its possible we should now be pending.
77   FlushUnanimatedStyle();
78   return Animation::PendingFromJS();
79 }
80 
PlayFromJS(ErrorResult & aRv)81 void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
82   // Note that flushing style below might trigger calls to
83   // PlayFromStyle()/PauseFromStyle() on this object.
84   FlushUnanimatedStyle();
85   Animation::PlayFromJS(aRv);
86   if (aRv.Failed()) {
87     return;
88   }
89 
90   AddOverriddenProperties(CSSAnimationProperties::PlayState);
91 }
92 
PauseFromJS(ErrorResult & aRv)93 void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
94   Animation::PauseFromJS(aRv);
95   if (aRv.Failed()) {
96     return;
97   }
98 
99   AddOverriddenProperties(CSSAnimationProperties::PlayState);
100 }
101 
PlayFromStyle()102 void CSSAnimation::PlayFromStyle() {
103   ErrorResult rv;
104   Animation::Play(rv, Animation::LimitBehavior::Continue);
105   // play() should not throw when LimitBehavior is Continue
106   MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
107 }
108 
PauseFromStyle()109 void CSSAnimation::PauseFromStyle() {
110   ErrorResult rv;
111   Animation::Pause(rv);
112   // pause() should only throw when *all* of the following conditions are true:
113   // - we are in the idle state, and
114   // - we have a negative playback rate, and
115   // - we have an infinitely repeating animation
116   // The first two conditions will never happen under regular style processing
117   // but could happen if an author made modifications to the Animation object
118   // and then updated animation-play-state. It's an unusual case and there's
119   // no obvious way to pass on the exception information so we just silently
120   // fail for now.
121   if (rv.Failed()) {
122     NS_WARNING("Unexpected exception pausing animation - silently failing");
123   }
124 }
125 
Tick()126 void CSSAnimation::Tick() {
127   Animation::Tick();
128   QueueEvents();
129 }
130 
HasLowerCompositeOrderThan(const CSSAnimation & aOther) const131 bool CSSAnimation::HasLowerCompositeOrderThan(
132     const CSSAnimation& aOther) const {
133   MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
134              "Should only be called for CSS animations that are sorted "
135              "as CSS animations (i.e. tied to CSS markup)");
136 
137   // 0. Object-equality case
138   if (&aOther == this) {
139     return false;
140   }
141 
142   // 1. Sort by document order
143   if (!mOwningElement.Equals(aOther.mOwningElement)) {
144     return mOwningElement.LessThan(
145         const_cast<CSSAnimation*>(this)->CachedChildIndexRef(),
146         aOther.mOwningElement,
147         const_cast<CSSAnimation*>(&aOther)->CachedChildIndexRef());
148   }
149 
150   // 2. (Same element and pseudo): Sort by position in animation-name
151   return mAnimationIndex < aOther.mAnimationIndex;
152 }
153 
QueueEvents(const StickyTimeDuration & aActiveTime)154 void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
155   // If the animation is pending, we ignore animation events until we finish
156   // pending.
157   if (mPendingState != PendingState::NotPending) {
158     return;
159   }
160 
161   // CSS animations dispatch events at their owning element. This allows
162   // script to repurpose a CSS animation to target a different element,
163   // to use a group effect (which has no obvious "target element"), or
164   // to remove the animation effect altogether whilst still getting
165   // animation events.
166   //
167   // It does mean, however, that for a CSS animation that has no owning
168   // element (e.g. it was created using the CSSAnimation constructor or
169   // disassociated from CSS) no events are fired. If it becomes desirable
170   // for these animations to still fire events we should spec the concept
171   // of the "original owning element" or "event target" and allow script
172   // to set it when creating a CSSAnimation object.
173   if (!mOwningElement.IsSet()) {
174     return;
175   }
176 
177   nsPresContext* presContext = mOwningElement.GetPresContext();
178   if (!presContext) {
179     return;
180   }
181 
182   uint64_t currentIteration = 0;
183   ComputedTiming::AnimationPhase currentPhase;
184   StickyTimeDuration intervalStartTime;
185   StickyTimeDuration intervalEndTime;
186   StickyTimeDuration iterationStartTime;
187 
188   if (!mEffect) {
189     currentPhase =
190         GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
191     if (currentPhase == mPreviousPhase) {
192       return;
193     }
194   } else {
195     ComputedTiming computedTiming = mEffect->GetComputedTiming();
196     currentPhase = computedTiming.mPhase;
197     currentIteration = computedTiming.mCurrentIteration;
198     if (currentPhase == mPreviousPhase &&
199         currentIteration == mPreviousIteration) {
200       return;
201     }
202     intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
203     intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
204 
205     uint64_t iterationBoundary = mPreviousIteration > currentIteration
206                                      ? currentIteration + 1
207                                      : currentIteration;
208     iterationStartTime = computedTiming.mDuration.MultDouble(
209         (iterationBoundary - computedTiming.mIterationStart));
210   }
211 
212   TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
213   TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
214   TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
215 
216   AutoTArray<AnimationEventInfo, 2> events;
217 
218   auto appendAnimationEvent = [&](EventMessage aMessage,
219                                   const StickyTimeDuration& aElapsedTime,
220                                   const TimeStamp& aScheduledEventTimeStamp) {
221     double elapsedTime = aElapsedTime.ToSeconds();
222     if (aMessage == eAnimationCancel) {
223       // 0 is an inappropriate value for this callsite. What we need to do is
224       // use a single random value for all increasing times reportable.
225       // That is to say, whenever elapsedTime goes negative (because an
226       // animation restarts, something rewinds the animation, or otherwise)
227       // a new random value for the mix-in must be generated.
228       elapsedTime =
229           nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(elapsedTime, 0);
230     }
231     events.AppendElement(
232         AnimationEventInfo(mAnimationName, mOwningElement.Target(), aMessage,
233                            elapsedTime, aScheduledEventTimeStamp, this));
234   };
235 
236   // Handle cancel event first
237   if ((mPreviousPhase != AnimationPhase::Idle &&
238        mPreviousPhase != AnimationPhase::After) &&
239       currentPhase == AnimationPhase::Idle) {
240     appendAnimationEvent(eAnimationCancel, aActiveTime,
241                          GetTimelineCurrentTimeAsTimeStamp());
242   }
243 
244   switch (mPreviousPhase) {
245     case AnimationPhase::Idle:
246     case AnimationPhase::Before:
247       if (currentPhase == AnimationPhase::Active) {
248         appendAnimationEvent(eAnimationStart, intervalStartTime,
249                              startTimeStamp);
250       } else if (currentPhase == AnimationPhase::After) {
251         appendAnimationEvent(eAnimationStart, intervalStartTime,
252                              startTimeStamp);
253         appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
254       }
255       break;
256     case AnimationPhase::Active:
257       if (currentPhase == AnimationPhase::Before) {
258         appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
259       } else if (currentPhase == AnimationPhase::Active) {
260         // The currentIteration must have changed or element we would have
261         // returned early above.
262         MOZ_ASSERT(currentIteration != mPreviousIteration);
263         appendAnimationEvent(eAnimationIteration, iterationStartTime,
264                              iterationTimeStamp);
265       } else if (currentPhase == AnimationPhase::After) {
266         appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
267       }
268       break;
269     case AnimationPhase::After:
270       if (currentPhase == AnimationPhase::Before) {
271         appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
272         appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
273       } else if (currentPhase == AnimationPhase::Active) {
274         appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
275       }
276       break;
277   }
278   mPreviousPhase = currentPhase;
279   mPreviousIteration = currentIteration;
280 
281   if (!events.IsEmpty()) {
282     presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
283   }
284 }
285 
UpdateTiming(SeekFlag aSeekFlag,SyncNotifyFlag aSyncNotifyFlag)286 void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
287                                 SyncNotifyFlag aSyncNotifyFlag) {
288   if (mNeedsNewAnimationIndexWhenRun &&
289       PlayState() != AnimationPlayState::Idle) {
290     mAnimationIndex = sNextAnimationIndex++;
291     mNeedsNewAnimationIndexWhenRun = false;
292   }
293 
294   Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
295 }
296 
297 /////////////////////// CSSAnimationKeyframeEffect ////////////////////////
298 
GetTiming(EffectTiming & aRetVal) const299 void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
300   MaybeFlushUnanimatedStyle();
301   KeyframeEffect::GetTiming(aRetVal);
302 }
303 
GetComputedTimingAsDict(ComputedEffectTiming & aRetVal) const304 void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
305     ComputedEffectTiming& aRetVal) const {
306   MaybeFlushUnanimatedStyle();
307   KeyframeEffect::GetComputedTimingAsDict(aRetVal);
308 }
309 
UpdateTiming(const OptionalEffectTiming & aTiming,ErrorResult & aRv)310 void CSSAnimationKeyframeEffect::UpdateTiming(
311     const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
312   KeyframeEffect::UpdateTiming(aTiming, aRv);
313 
314   if (aRv.Failed()) {
315     return;
316   }
317 
318   if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
319     CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
320     if (aTiming.mDuration.WasPassed()) {
321       updatedProperties |= CSSAnimationProperties::Duration;
322     }
323     if (aTiming.mIterations.WasPassed()) {
324       updatedProperties |= CSSAnimationProperties::IterationCount;
325     }
326     if (aTiming.mDirection.WasPassed()) {
327       updatedProperties |= CSSAnimationProperties::Direction;
328     }
329     if (aTiming.mDelay.WasPassed()) {
330       updatedProperties |= CSSAnimationProperties::Delay;
331     }
332     if (aTiming.mFill.WasPassed()) {
333       updatedProperties |= CSSAnimationProperties::FillMode;
334     }
335 
336     cssAnimation->AddOverriddenProperties(updatedProperties);
337   }
338 }
339 
SetKeyframes(JSContext * aContext,JS::Handle<JSObject * > aKeyframes,ErrorResult & aRv)340 void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
341                                               JS::Handle<JSObject*> aKeyframes,
342                                               ErrorResult& aRv) {
343   KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv);
344 
345   if (aRv.Failed()) {
346     return;
347   }
348 
349   if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
350     cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes);
351   }
352 }
353 
MaybeFlushUnanimatedStyle() const354 void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
355   if (!GetOwningCSSAnimation()) {
356     return;
357   }
358 
359   if (dom::Document* doc = GetRenderedDocument()) {
360     doc->FlushPendingNotifications(
361         ChangesToFlush(FlushType::Style, false /* flush animations */));
362   }
363 }
364 
365 }  // namespace mozilla::dom
366