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