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 "nsAnimationManager.h"
9 #include "nsTransitionManager.h"
11 #include "mozilla/AnimationEventDispatcher.h"
12 #include "mozilla/AnimationUtils.h"
13 #include "mozilla/EffectCompositor.h"
14 #include "mozilla/ElementAnimationData.h"
15 #include "mozilla/MemoryReporting.h"
16 #include "mozilla/ServoStyleSet.h"
17 #include "mozilla/TimelineCollection.h"
18 #include "mozilla/dom/AnimationEffect.h"
19 #include "mozilla/dom/DocumentTimeline.h"
20 #include "mozilla/dom/KeyframeEffect.h"
21 #include "mozilla/dom/MutationObservers.h"
22 #include "mozilla/dom/ScrollTimeline.h"
23 #include "mozilla/dom/ViewTimeline.h"
25 #include "nsPresContext.h"
26 #include "nsPresContextInlines.h"
27 #include "nsStyleChangeList.h"
28 #include "nsLayoutUtils.h"
30 #include "mozilla/dom/Document.h"
31 #include "nsDOMMutationObserver.h"
32 #include "nsRFPService.h"
33 #include <algorithm> // std::stable_sort
36 using namespace mozilla
;
37 using namespace mozilla::css
;
38 using mozilla::dom::Animation
;
39 using mozilla::dom::AnimationPlayState
;
40 using mozilla::dom::CSSAnimation
;
41 using mozilla::dom::Element
;
42 using mozilla::dom::KeyframeEffect
;
43 using mozilla::dom::MutationObservers
;
44 using mozilla::dom::ScrollTimeline
;
45 using mozilla::dom::ViewTimeline
;
47 ////////////////////////// nsAnimationManager ////////////////////////////
49 // Find the matching animation by |aName| in the old list
50 // of animations and remove the matched animation from the list.
51 static already_AddRefed
<CSSAnimation
> PopExistingAnimation(
53 nsAnimationManager::CSSAnimationCollection
* aCollection
) {
58 // Animations are stored in reverse order to how they appear in the
59 // animation-name property. However, we want to match animations beginning
60 // from the end of the animation-name list, so we iterate *forwards*
61 // through the collection.
62 for (size_t idx
= 0, length
= aCollection
->mAnimations
.Length();
63 idx
!= length
; ++idx
) {
64 CSSAnimation
* cssAnim
= aCollection
->mAnimations
[idx
];
65 if (cssAnim
->AnimationName() == aName
) {
66 RefPtr
<CSSAnimation
> match
= cssAnim
;
67 aCollection
->mAnimations
.RemoveElementAt(idx
);
68 return match
.forget();
75 class MOZ_STACK_CLASS ServoCSSAnimationBuilder final
{
77 explicit ServoCSSAnimationBuilder(const ComputedStyle
* aComputedStyle
)
78 : mComputedStyle(aComputedStyle
) {
79 MOZ_ASSERT(aComputedStyle
);
82 bool BuildKeyframes(const Element
& aElement
, nsPresContext
* aPresContext
,
84 const StyleComputedTimingFunction
& aTimingFunction
,
85 nsTArray
<Keyframe
>& aKeyframes
) {
86 return aPresContext
->StyleSet()->GetKeyframesForName(
87 aElement
, *mComputedStyle
, aName
, aTimingFunction
, aKeyframes
);
89 void SetKeyframes(KeyframeEffect
& aEffect
, nsTArray
<Keyframe
>&& aKeyframes
,
90 const dom::AnimationTimeline
* aTimeline
) {
91 aEffect
.SetKeyframes(std::move(aKeyframes
), mComputedStyle
, aTimeline
);
94 // Currently all the animation building code in this file is based on
95 // assumption that creating and removing animations should *not* trigger
96 // additional restyles since those changes will be handled within the same
99 // While that is true for the Gecko style backend, it is not true for the
100 // Servo style backend where we want restyles to be triggered so that we
101 // perform a second animation restyle where we will incorporate the changes
102 // arising from creating and removing animations.
104 // Fortunately, our attempts to avoid posting extra restyles as part of the
105 // processing here are imperfect and most of the time we happen to post
106 // them anyway. Occasionally, however, we don't. For example, we don't post
107 // a restyle when we create a new animation whose an animation index matches
108 // the default value it was given already (which is typically only true when
109 // the CSSAnimation we create is the first Animation created in a particular
112 // As a result, when we are using the Servo backend, whenever we have an added
113 // or removed animation we need to explicitly trigger a restyle.
115 // This code should eventually disappear along with the Gecko style backend
116 // and we should simply call Play() / Pause() / Cancel() etc. which will
117 // post the required restyles.
118 void NotifyNewOrRemovedAnimation(const Animation
& aAnimation
) {
119 dom::AnimationEffect
* effect
= aAnimation
.GetEffect();
124 KeyframeEffect
* keyframeEffect
= effect
->AsKeyframeEffect();
125 if (!keyframeEffect
) {
129 keyframeEffect
->RequestRestyle(EffectCompositor::RestyleType::Standard
);
133 const ComputedStyle
* mComputedStyle
;
136 static void UpdateOldAnimationPropertiesWithNew(
137 CSSAnimation
& aOld
, TimingParams
&& aNewTiming
,
138 nsTArray
<Keyframe
>&& aNewKeyframes
, bool aNewIsStylePaused
,
139 CSSAnimationProperties aOverriddenProperties
,
140 ServoCSSAnimationBuilder
& aBuilder
, dom::AnimationTimeline
* aTimeline
,
141 dom::CompositeOperation aNewComposite
) {
142 bool animationChanged
= false;
144 // Update the old from the new so we can keep the original object
145 // identity (and any expando properties attached to it).
146 if (aOld
.GetEffect()) {
147 dom::AnimationEffect
* oldEffect
= aOld
.GetEffect();
149 // Copy across the changes that are not overridden
150 TimingParams updatedTiming
= oldEffect
->SpecifiedTiming();
151 if (~aOverriddenProperties
& CSSAnimationProperties::Duration
) {
152 updatedTiming
.SetDuration(aNewTiming
.Duration());
154 if (~aOverriddenProperties
& CSSAnimationProperties::IterationCount
) {
155 updatedTiming
.SetIterations(aNewTiming
.Iterations());
157 if (~aOverriddenProperties
& CSSAnimationProperties::Direction
) {
158 updatedTiming
.SetDirection(aNewTiming
.Direction());
160 if (~aOverriddenProperties
& CSSAnimationProperties::Delay
) {
161 updatedTiming
.SetDelay(aNewTiming
.Delay());
163 if (~aOverriddenProperties
& CSSAnimationProperties::FillMode
) {
164 updatedTiming
.SetFill(aNewTiming
.Fill());
167 animationChanged
= oldEffect
->SpecifiedTiming() != updatedTiming
;
168 oldEffect
->SetSpecifiedTiming(std::move(updatedTiming
));
170 if (KeyframeEffect
* oldKeyframeEffect
= oldEffect
->AsKeyframeEffect()) {
171 if (~aOverriddenProperties
& CSSAnimationProperties::Keyframes
) {
172 aBuilder
.SetKeyframes(*oldKeyframeEffect
, std::move(aNewKeyframes
),
176 if (~aOverriddenProperties
& CSSAnimationProperties::Composition
) {
177 animationChanged
= oldKeyframeEffect
->Composite() != aNewComposite
;
178 oldKeyframeEffect
->SetCompositeFromStyle(aNewComposite
);
183 // Checking pointers should be enough. If both are scroll-timeline, we reuse
184 // the scroll-timeline object if their scrollers and axes are the same.
185 if (aOld
.GetTimeline() != aTimeline
) {
186 aOld
.SetTimeline(aTimeline
);
187 animationChanged
= true;
190 // Handle changes in play state. If the animation is idle, however,
191 // changes to animation-play-state should *not* restart it.
192 if (aOld
.PlayState() != AnimationPlayState::Idle
&&
193 ~aOverriddenProperties
& CSSAnimationProperties::PlayState
) {
194 bool wasPaused
= aOld
.PlayState() == AnimationPlayState::Paused
;
195 if (!wasPaused
&& aNewIsStylePaused
) {
196 aOld
.PauseFromStyle();
197 animationChanged
= true;
198 } else if (wasPaused
&& !aNewIsStylePaused
) {
199 aOld
.PlayFromStyle();
200 animationChanged
= true;
204 // Updating the effect timing above might already have caused the
205 // animation to become irrelevant so only add a changed record if
206 // the animation is still relevant.
207 if (animationChanged
&& aOld
.IsRelevant()) {
208 MutationObservers::NotifyAnimationChanged(&aOld
);
212 static already_AddRefed
<dom::AnimationTimeline
> GetNamedProgressTimeline(
213 dom::Document
* aDocument
, const NonOwningAnimationTarget
& aTarget
,
215 // A named progress timeline is referenceable in animation-timeline by:
216 // 1. the declaring element itself
217 // 2. that element’s descendants
218 // 3. that element’s following siblings and their descendants
219 // https://drafts.csswg.org/scroll-animations-1/#timeline-scope
220 // FIXME: Bug 1823500. Reduce default scoping to ancestors only.
221 for (Element
* curr
= AnimationUtils::GetElementForRestyle(
222 aTarget
.mElement
, aTarget
.mPseudoType
);
223 curr
; curr
= curr
->GetParentElement()) {
224 // If multiple elements have declared the same timeline name, the matching
225 // timeline is the one declared on the nearest element in tree order, which
226 // considers siblings closer than parents.
227 // Note: This is fine for parallel traversal because we update animations by
229 for (Element
* e
= curr
; e
; e
= e
->GetPreviousElementSibling()) {
230 // In case of a name conflict on the same element, scroll progress
231 // timelines take precedence over view progress timelines.
232 const auto [element
, pseudoType
] =
233 AnimationUtils::GetElementPseudoPair(e
);
234 if (auto* collection
=
235 TimelineCollection
<ScrollTimeline
>::Get(element
, pseudoType
)) {
236 if (RefPtr
<ScrollTimeline
> timeline
= collection
->Lookup(aName
)) {
237 return timeline
.forget();
241 if (auto* collection
=
242 TimelineCollection
<ViewTimeline
>::Get(element
, pseudoType
)) {
243 if (RefPtr
<ViewTimeline
> timeline
= collection
->Lookup(aName
)) {
244 return timeline
.forget();
250 // If we cannot find a matched scroll-timeline-name, this animation is not
251 // associated with a timeline.
252 // https://drafts.csswg.org/css-animations-2/#valdef-animation-timeline-custom-ident
256 static already_AddRefed
<dom::AnimationTimeline
> GetTimeline(
257 const StyleAnimationTimeline
& aStyleTimeline
, nsPresContext
* aPresContext
,
258 const NonOwningAnimationTarget
& aTarget
) {
259 switch (aStyleTimeline
.tag
) {
260 case StyleAnimationTimeline::Tag::Timeline
: {
261 // Check scroll-timeline-name property or view-timeline-property.
262 nsAtom
* name
= aStyleTimeline
.AsTimeline().AsAtom();
263 return name
!= nsGkAtoms::_empty
264 ? GetNamedProgressTimeline(aPresContext
->Document(), aTarget
,
268 case StyleAnimationTimeline::Tag::Scroll
: {
269 const auto& scroll
= aStyleTimeline
.AsScroll();
270 return ScrollTimeline::MakeAnonymous(aPresContext
->Document(), aTarget
,
271 scroll
.axis
, scroll
.scroller
);
273 case StyleAnimationTimeline::Tag::View
: {
274 const auto& view
= aStyleTimeline
.AsView();
275 return ViewTimeline::MakeAnonymous(aPresContext
->Document(), aTarget
,
276 view
.axis
, view
.inset
);
278 case StyleAnimationTimeline::Tag::Auto
:
279 return do_AddRef(aTarget
.mElement
->OwnerDoc()->Timeline());
281 MOZ_ASSERT_UNREACHABLE("Unknown animation-timeline value?");
285 // Returns a new animation set up with given StyleAnimation.
286 // Or returns an existing animation matching StyleAnimation's name updated
287 // with the new StyleAnimation.
288 static already_AddRefed
<CSSAnimation
> BuildAnimation(
289 nsPresContext
* aPresContext
, const NonOwningAnimationTarget
& aTarget
,
290 const nsStyleUIReset
& aStyle
, uint32_t animIdx
,
291 ServoCSSAnimationBuilder
& aBuilder
,
292 nsAnimationManager::CSSAnimationCollection
* aCollection
) {
293 MOZ_ASSERT(aPresContext
);
295 nsAtom
* animationName
= aStyle
.GetAnimationName(animIdx
);
296 nsTArray
<Keyframe
> keyframes
;
297 if (!aBuilder
.BuildKeyframes(*aTarget
.mElement
, aPresContext
, animationName
,
298 aStyle
.GetAnimationTimingFunction(animIdx
),
303 TimingParams timing
= TimingParamsFromCSSParams(
304 aStyle
.GetAnimationDuration(animIdx
).ToMilliseconds(),
305 aStyle
.GetAnimationDelay(animIdx
).ToMilliseconds(),
306 aStyle
.GetAnimationIterationCount(animIdx
),
307 aStyle
.GetAnimationDirection(animIdx
),
308 aStyle
.GetAnimationFillMode(animIdx
));
311 aStyle
.GetAnimationPlayState(animIdx
) == StyleAnimationPlayState::Paused
;
313 RefPtr
<dom::AnimationTimeline
> timeline
=
314 GetTimeline(aStyle
.GetTimeline(animIdx
), aPresContext
, aTarget
);
316 // Find the matching animation with animation name in the old list
317 // of animations and remove the matched animation from the list.
318 RefPtr
<CSSAnimation
> oldAnim
=
319 PopExistingAnimation(animationName
, aCollection
);
321 const auto composition
= StyleToDom(aStyle
.GetAnimationComposition(animIdx
));
323 // Copy over the start times and (if still paused) pause starts
324 // for each animation (matching on name only) that was also in the
325 // old list of animations.
326 // This means that we honor dynamic changes, which isn't what the
327 // spec says to do, but WebKit seems to honor at least some of
329 // http://lists.w3.org/Archives/Public/www-style/2011Apr/0079.html
330 // In order to honor what the spec said, we'd copy more data over.
331 UpdateOldAnimationPropertiesWithNew(
332 *oldAnim
, std::move(timing
), std::move(keyframes
), isStylePaused
,
333 oldAnim
->GetOverriddenProperties(), aBuilder
, timeline
, composition
);
334 return oldAnim
.forget();
337 KeyframeEffectParams
effectOptions(composition
);
338 RefPtr
<KeyframeEffect
> effect
= new dom::CSSAnimationKeyframeEffect(
339 aPresContext
->Document(),
340 OwningAnimationTarget(aTarget
.mElement
, aTarget
.mPseudoType
),
341 std::move(timing
), effectOptions
);
343 aBuilder
.SetKeyframes(*effect
, std::move(keyframes
), timeline
);
345 RefPtr
<CSSAnimation
> animation
= new CSSAnimation(
346 aPresContext
->Document()->GetScopeObject(), animationName
);
347 animation
->SetOwningElement(
348 OwningElementRef(*aTarget
.mElement
, aTarget
.mPseudoType
));
350 animation
->SetTimelineNoUpdate(timeline
);
351 animation
->SetEffectNoUpdate(effect
);
354 animation
->PauseFromStyle();
356 animation
->PlayFromStyle();
359 aBuilder
.NotifyNewOrRemovedAnimation(*animation
);
361 return animation
.forget();
364 static nsAnimationManager::OwningCSSAnimationPtrArray
BuildAnimations(
365 nsPresContext
* aPresContext
, const NonOwningAnimationTarget
& aTarget
,
366 const nsStyleUIReset
& aStyle
, ServoCSSAnimationBuilder
& aBuilder
,
367 nsAnimationManager::CSSAnimationCollection
* aCollection
,
368 nsTHashSet
<RefPtr
<nsAtom
>>& aReferencedAnimations
) {
369 nsAnimationManager::OwningCSSAnimationPtrArray result
;
371 for (size_t animIdx
= aStyle
.mAnimationNameCount
; animIdx
-- != 0;) {
372 nsAtom
* name
= aStyle
.GetAnimationName(animIdx
);
373 // CSS Animations whose animation-name does not match a @keyframes rule do
374 // not generate animation events. This includes when the animation-name is
375 // "none" which is represented by an empty name in the StyleAnimation.
376 // Since such animations neither affect style nor dispatch events, we do
377 // not generate a corresponding CSSAnimation for them.
378 if (name
== nsGkAtoms::_empty
) {
382 aReferencedAnimations
.Insert(name
);
383 RefPtr
<CSSAnimation
> dest
= BuildAnimation(aPresContext
, aTarget
, aStyle
,
384 animIdx
, aBuilder
, aCollection
);
389 dest
->SetAnimationIndex(static_cast<uint64_t>(animIdx
));
390 result
.AppendElement(dest
);
395 void nsAnimationManager::UpdateAnimations(dom::Element
* aElement
,
396 PseudoStyleType aPseudoType
,
397 const ComputedStyle
* aComputedStyle
) {
398 MOZ_ASSERT(mPresContext
->IsDynamic(),
399 "Should not update animations for print or print preview");
400 MOZ_ASSERT(aElement
->IsInComposedDoc(),
401 "Should not update animations that are not attached to the "
404 if (!aComputedStyle
||
405 aComputedStyle
->StyleDisplay()->mDisplay
== StyleDisplay::None
) {
406 // If we are in a display:none subtree we will have no computed values.
407 // However, if we are on the root of display:none subtree, the computed
408 // values might not have been cleared yet.
409 // In either case, since CSS animations should not run in display:none
410 // subtrees we should stop (actually, destroy) any animations on this
412 StopAnimationsForElement(aElement
, aPseudoType
);
416 NonOwningAnimationTarget
target(aElement
, aPseudoType
);
417 ServoCSSAnimationBuilder
builder(aComputedStyle
);
419 DoUpdateAnimations(target
, *aComputedStyle
->StyleUIReset(), builder
);
422 void nsAnimationManager::DoUpdateAnimations(
423 const NonOwningAnimationTarget
& aTarget
, const nsStyleUIReset
& aStyle
,
424 ServoCSSAnimationBuilder
& aBuilder
) {
425 // Everything that causes our animation data to change triggers a
426 // style change, which in turn triggers a non-animation restyle.
427 // Likewise, when we initially construct frames, we're not in a
428 // style change, but also not in an animation restyle.
431 CSSAnimationCollection::Get(aTarget
.mElement
, aTarget
.mPseudoType
);
432 if (!collection
&& aStyle
.mAnimationNameCount
== 1 &&
433 aStyle
.mAnimations
[0].GetName() == nsGkAtoms::_empty
) {
437 nsAutoAnimationMutationBatch
mb(aTarget
.mElement
->OwnerDoc());
439 // Build the updated animations list, extracting matching animations from
440 // the existing collection as we go.
441 OwningCSSAnimationPtrArray newAnimations
=
442 BuildAnimations(mPresContext
, aTarget
, aStyle
, aBuilder
, collection
,
443 mMaybeReferencedAnimations
);
445 if (newAnimations
.IsEmpty()) {
447 collection
->Destroy();
454 &aTarget
.mElement
->EnsureAnimationData().EnsureAnimationCollection(
455 *aTarget
.mElement
, aTarget
.mPseudoType
);
456 if (!collection
->isInList()) {
457 AddElementCollection(collection
);
460 collection
->mAnimations
.SwapElements(newAnimations
);
462 // Cancel removed animations
463 for (size_t newAnimIdx
= newAnimations
.Length(); newAnimIdx
-- != 0;) {
464 aBuilder
.NotifyNewOrRemovedAnimation(*newAnimations
[newAnimIdx
]);
465 newAnimations
[newAnimIdx
]->CancelFromStyle(PostRestyleMode::IfNeeded
);