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/. */
7 #include "CSSAnimation.h"
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"
15 namespace mozilla::dom
{
17 using AnimationPhase
= ComputedTiming::AnimationPhase
;
19 JSObject
* CSSAnimation::WrapObject(JSContext
* aCx
,
20 JS::Handle
<JSObject
*> aGivenProto
) {
21 return dom::CSSAnimation_Binding::Wrap(aCx
, this, aGivenProto
);
24 void CSSAnimation::SetEffect(AnimationEffect
* aEffect
) {
25 Animation::SetEffect(aEffect
);
27 AddOverriddenProperties(CSSAnimationProperties::Effect
);
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
;
36 Animation::SetStartTimeAsDouble(aStartTime
);
38 bool isPaused
= PlayState() == AnimationPlayState::Paused
;
40 if (wasPaused
!= isPaused
) {
41 AddOverriddenProperties(CSSAnimationProperties::PlayState
);
45 mozilla::dom::Promise
* CSSAnimation::GetReady(ErrorResult
& aRv
) {
46 FlushUnanimatedStyle();
47 return Animation::GetReady(aRv
);
50 void CSSAnimation::Reverse(ErrorResult
& aRv
) {
51 // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in
53 bool wasPaused
= PlayState() == AnimationPlayState::Paused
;
55 Animation::Reverse(aRv
);
60 bool isPaused
= PlayState() == AnimationPlayState::Paused
;
62 if (wasPaused
!= isPaused
) {
63 AddOverriddenProperties(CSSAnimationProperties::PlayState
);
67 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();
74 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();
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
);
90 AddOverriddenProperties(CSSAnimationProperties::PlayState
);
93 void CSSAnimation::PauseFromJS(ErrorResult
& aRv
) {
94 Animation::PauseFromJS(aRv
);
99 AddOverriddenProperties(CSSAnimationProperties::PlayState
);
102 void CSSAnimation::PlayFromStyle() {
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");
109 void CSSAnimation::PauseFromStyle() {
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
122 NS_WARNING("Unexpected exception pausing animation - silently failing");
126 void CSSAnimation::Tick() {
131 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)");
137 // 0. Object-equality case
138 if (&aOther
== this) {
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());
150 // 2. (Same element and pseudo): Sort by position in animation-name
151 return mAnimationIndex
< aOther
.mAnimationIndex
;
154 void CSSAnimation::QueueEvents(const StickyTimeDuration
& aActiveTime
) {
155 // If the animation is pending, we ignore animation events until we finish
157 if (mPendingState
!= PendingState::NotPending
) {
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
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()) {
177 nsPresContext
* presContext
= mOwningElement
.GetPresContext();
182 uint64_t currentIteration
= 0;
183 ComputedTiming::AnimationPhase currentPhase
;
184 StickyTimeDuration intervalStartTime
;
185 StickyTimeDuration intervalEndTime
;
186 StickyTimeDuration iterationStartTime
;
190 GetAnimationPhaseWithoutEffect
<ComputedTiming::AnimationPhase
>(*this);
191 if (currentPhase
== mPreviousPhase
) {
195 ComputedTiming computedTiming
= mEffect
->GetComputedTiming();
196 currentPhase
= computedTiming
.mPhase
;
197 currentIteration
= computedTiming
.mCurrentIteration
;
198 if (currentPhase
== mPreviousPhase
&&
199 currentIteration
== mPreviousIteration
) {
202 intervalStartTime
= IntervalStartTime(computedTiming
.mActiveDuration
);
203 intervalEndTime
= IntervalEndTime(computedTiming
.mActiveDuration
);
205 uint64_t iterationBoundary
= mPreviousIteration
> currentIteration
206 ? currentIteration
+ 1
208 double multiplier
= iterationBoundary
- computedTiming
.mIterationStart
;
209 if (multiplier
!= 0.0) {
210 iterationStartTime
= computedTiming
.mDuration
.MultDouble(multiplier
);
214 TimeStamp startTimeStamp
= ElapsedTimeToTimeStamp(intervalStartTime
);
215 TimeStamp endTimeStamp
= ElapsedTimeToTimeStamp(intervalEndTime
);
216 TimeStamp iterationTimeStamp
= ElapsedTimeToTimeStamp(iterationStartTime
);
218 AutoTArray
<AnimationEventInfo
, 2> events
;
220 auto appendAnimationEvent
= [&](EventMessage aMessage
,
221 const StickyTimeDuration
& aElapsedTime
,
222 const TimeStamp
& aScheduledEventTimeStamp
) {
223 double elapsedTime
= aElapsedTime
.ToSeconds();
224 if (aMessage
== eAnimationCancel
) {
225 // 0 is an inappropriate value for this callsite. What we need to do is
226 // use a single random value for all increasing times reportable.
227 // That is to say, whenever elapsedTime goes negative (because an
228 // animation restarts, something rewinds the animation, or otherwise)
229 // a new random value for the mix-in must be generated.
231 nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(elapsedTime
, 0);
233 events
.AppendElement(
234 AnimationEventInfo(mAnimationName
, mOwningElement
.Target(), aMessage
,
235 elapsedTime
, aScheduledEventTimeStamp
, this));
238 // Handle cancel event first
239 if ((mPreviousPhase
!= AnimationPhase::Idle
&&
240 mPreviousPhase
!= AnimationPhase::After
) &&
241 currentPhase
== AnimationPhase::Idle
) {
242 appendAnimationEvent(eAnimationCancel
, aActiveTime
,
243 GetTimelineCurrentTimeAsTimeStamp());
246 switch (mPreviousPhase
) {
247 case AnimationPhase::Idle
:
248 case AnimationPhase::Before
:
249 if (currentPhase
== AnimationPhase::Active
) {
250 appendAnimationEvent(eAnimationStart
, intervalStartTime
,
252 } else if (currentPhase
== AnimationPhase::After
) {
253 appendAnimationEvent(eAnimationStart
, intervalStartTime
,
255 appendAnimationEvent(eAnimationEnd
, intervalEndTime
, endTimeStamp
);
258 case AnimationPhase::Active
:
259 if (currentPhase
== AnimationPhase::Before
) {
260 appendAnimationEvent(eAnimationEnd
, intervalStartTime
, startTimeStamp
);
261 } else if (currentPhase
== AnimationPhase::Active
) {
262 // The currentIteration must have changed or element we would have
263 // returned early above.
264 MOZ_ASSERT(currentIteration
!= mPreviousIteration
);
265 appendAnimationEvent(eAnimationIteration
, iterationStartTime
,
267 } else if (currentPhase
== AnimationPhase::After
) {
268 appendAnimationEvent(eAnimationEnd
, intervalEndTime
, endTimeStamp
);
271 case AnimationPhase::After
:
272 if (currentPhase
== AnimationPhase::Before
) {
273 appendAnimationEvent(eAnimationStart
, intervalEndTime
, startTimeStamp
);
274 appendAnimationEvent(eAnimationEnd
, intervalStartTime
, endTimeStamp
);
275 } else if (currentPhase
== AnimationPhase::Active
) {
276 appendAnimationEvent(eAnimationStart
, intervalEndTime
, endTimeStamp
);
280 mPreviousPhase
= currentPhase
;
281 mPreviousIteration
= currentIteration
;
283 if (!events
.IsEmpty()) {
284 presContext
->AnimationEventDispatcher()->QueueEvents(std::move(events
));
288 void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag
,
289 SyncNotifyFlag aSyncNotifyFlag
) {
290 if (mNeedsNewAnimationIndexWhenRun
&&
291 PlayState() != AnimationPlayState::Idle
) {
292 mAnimationIndex
= sNextAnimationIndex
++;
293 mNeedsNewAnimationIndexWhenRun
= false;
296 Animation::UpdateTiming(aSeekFlag
, aSyncNotifyFlag
);
299 /////////////////////// CSSAnimationKeyframeEffect ////////////////////////
301 void CSSAnimationKeyframeEffect::GetTiming(EffectTiming
& aRetVal
) const {
302 MaybeFlushUnanimatedStyle();
303 KeyframeEffect::GetTiming(aRetVal
);
306 void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
307 ComputedEffectTiming
& aRetVal
) const {
308 MaybeFlushUnanimatedStyle();
309 KeyframeEffect::GetComputedTimingAsDict(aRetVal
);
312 void CSSAnimationKeyframeEffect::UpdateTiming(
313 const OptionalEffectTiming
& aTiming
, ErrorResult
& aRv
) {
314 KeyframeEffect::UpdateTiming(aTiming
, aRv
);
320 if (CSSAnimation
* cssAnimation
= GetOwningCSSAnimation()) {
321 CSSAnimationProperties updatedProperties
= CSSAnimationProperties::None
;
322 if (aTiming
.mDuration
.WasPassed()) {
323 updatedProperties
|= CSSAnimationProperties::Duration
;
325 if (aTiming
.mIterations
.WasPassed()) {
326 updatedProperties
|= CSSAnimationProperties::IterationCount
;
328 if (aTiming
.mDirection
.WasPassed()) {
329 updatedProperties
|= CSSAnimationProperties::Direction
;
331 if (aTiming
.mDelay
.WasPassed()) {
332 updatedProperties
|= CSSAnimationProperties::Delay
;
334 if (aTiming
.mFill
.WasPassed()) {
335 updatedProperties
|= CSSAnimationProperties::FillMode
;
338 cssAnimation
->AddOverriddenProperties(updatedProperties
);
342 void CSSAnimationKeyframeEffect::SetKeyframes(JSContext
* aContext
,
343 JS::Handle
<JSObject
*> aKeyframes
,
345 KeyframeEffect::SetKeyframes(aContext
, aKeyframes
, aRv
);
351 if (CSSAnimation
* cssAnimation
= GetOwningCSSAnimation()) {
352 cssAnimation
->AddOverriddenProperties(CSSAnimationProperties::Keyframes
);
356 void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
357 if (!GetOwningCSSAnimation()) {
361 if (dom::Document
* doc
= GetRenderedDocument()) {
362 doc
->FlushPendingNotifications(
363 ChangesToFlush(FlushType::Style
, false /* flush animations */));
367 } // namespace mozilla::dom