nsAnimationManager.cpp (18819B)
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 "nsAnimationManager.h" 8 9 #include <math.h> 10 11 #include <algorithm> // std::stable_sort 12 13 #include "mozilla/AnimationEventDispatcher.h" 14 #include "mozilla/AnimationUtils.h" 15 #include "mozilla/EffectCompositor.h" 16 #include "mozilla/ElementAnimationData.h" 17 #include "mozilla/ServoStyleSet.h" 18 #include "mozilla/TimelineCollection.h" 19 #include "mozilla/dom/AnimationEffect.h" 20 #include "mozilla/dom/Document.h" 21 #include "mozilla/dom/DocumentTimeline.h" 22 #include "mozilla/dom/KeyframeEffect.h" 23 #include "mozilla/dom/MutationObservers.h" 24 #include "mozilla/dom/ScrollTimeline.h" 25 #include "mozilla/dom/ViewTimeline.h" 26 #include "nsDOMMutationObserver.h" 27 #include "nsIFrame.h" 28 #include "nsINode.h" 29 #include "nsLayoutUtils.h" 30 #include "nsPresContext.h" 31 #include "nsPresContextInlines.h" 32 #include "nsRFPService.h" 33 #include "nsStyleChangeList.h" 34 #include "nsTransitionManager.h" 35 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; 46 47 ////////////////////////// nsAnimationManager //////////////////////////// 48 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( 52 const nsAtom* aName, 53 nsAnimationManager::CSSAnimationCollection* aCollection) { 54 if (!aCollection) { 55 return nullptr; 56 } 57 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(); 69 } 70 } 71 72 return nullptr; 73 } 74 75 class MOZ_STACK_CLASS ServoCSSAnimationBuilder final { 76 public: 77 explicit ServoCSSAnimationBuilder(const ComputedStyle* aComputedStyle) 78 : mComputedStyle(aComputedStyle) { 79 MOZ_ASSERT(aComputedStyle); 80 } 81 82 bool BuildKeyframes(const Element& aElement, nsPresContext* aPresContext, 83 nsAtom* aName, 84 const StyleComputedTimingFunction& aTimingFunction, 85 nsTArray<Keyframe>& aKeyframes) { 86 return aPresContext->StyleSet()->GetKeyframesForName( 87 aElement, *mComputedStyle, aName, aTimingFunction, aKeyframes); 88 } 89 void SetKeyframes(KeyframeEffect& aEffect, nsTArray<Keyframe>&& aKeyframes, 90 const dom::AnimationTimeline* aTimeline) { 91 aEffect.SetKeyframes(std::move(aKeyframes), mComputedStyle, aTimeline); 92 } 93 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 97 // restyle. 98 // 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. 103 // 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 110 // content process). 111 // 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. 114 // 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(); 120 if (!effect) { 121 return; 122 } 123 124 KeyframeEffect* keyframeEffect = effect->AsKeyframeEffect(); 125 if (!keyframeEffect) { 126 return; 127 } 128 129 keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Standard); 130 } 131 132 private: 133 const ComputedStyle* mComputedStyle; 134 }; 135 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; 143 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(); 148 149 // Copy across the changes that are not overridden 150 TimingParams updatedTiming = oldEffect->SpecifiedTiming(); 151 if (~aOverriddenProperties & CSSAnimationProperties::Duration) { 152 updatedTiming.SetDuration(aNewTiming.Duration()); 153 } 154 if (~aOverriddenProperties & CSSAnimationProperties::IterationCount) { 155 updatedTiming.SetIterations(aNewTiming.Iterations()); 156 } 157 if (~aOverriddenProperties & CSSAnimationProperties::Direction) { 158 updatedTiming.SetDirection(aNewTiming.Direction()); 159 } 160 if (~aOverriddenProperties & CSSAnimationProperties::Delay) { 161 updatedTiming.SetDelay(aNewTiming.Delay()); 162 } 163 if (~aOverriddenProperties & CSSAnimationProperties::FillMode) { 164 updatedTiming.SetFill(aNewTiming.Fill()); 165 } 166 167 animationChanged = oldEffect->SpecifiedTiming() != updatedTiming; 168 oldEffect->SetSpecifiedTiming(std::move(updatedTiming)); 169 170 if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) { 171 if (~aOverriddenProperties & CSSAnimationProperties::Keyframes) { 172 aBuilder.SetKeyframes(*oldKeyframeEffect, std::move(aNewKeyframes), 173 aTimeline); 174 } 175 176 if (~aOverriddenProperties & CSSAnimationProperties::Composition) { 177 animationChanged = oldKeyframeEffect->Composite() != aNewComposite; 178 oldKeyframeEffect->SetCompositeFromStyle(aNewComposite); 179 } 180 } 181 } 182 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; 188 } 189 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; 201 } 202 } 203 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); 209 } 210 } 211 212 static already_AddRefed<dom::AnimationTimeline> GetNamedProgressTimeline( 213 dom::Document* aDocument, const NonOwningAnimationTarget& aTarget, 214 nsAtom* aName) { 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 = 222 aTarget.mElement->GetPseudoElement(aTarget.mPseudoRequest); 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 228 // SequentialTask. 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, pseudo] = AnimationUtils::GetElementPseudoPair(e); 233 if (auto* collection = 234 TimelineCollection<ScrollTimeline>::Get(element, pseudo)) { 235 if (RefPtr<ScrollTimeline> timeline = collection->Lookup(aName)) { 236 return timeline.forget(); 237 } 238 } 239 240 if (auto* collection = 241 TimelineCollection<ViewTimeline>::Get(element, pseudo)) { 242 if (RefPtr<ViewTimeline> timeline = collection->Lookup(aName)) { 243 return timeline.forget(); 244 } 245 } 246 } 247 } 248 249 // If we cannot find a matched scroll-timeline-name, this animation is not 250 // associated with a timeline. 251 // https://drafts.csswg.org/css-animations-2/#valdef-animation-timeline-custom-ident 252 return nullptr; 253 } 254 255 static already_AddRefed<dom::AnimationTimeline> GetTimeline( 256 const StyleAnimationTimeline& aStyleTimeline, nsPresContext* aPresContext, 257 const NonOwningAnimationTarget& aTarget) { 258 switch (aStyleTimeline.tag) { 259 case StyleAnimationTimeline::Tag::Timeline: { 260 // Check scroll-timeline-name property or view-timeline-property. 261 nsAtom* name = aStyleTimeline.AsTimeline().AsAtom(); 262 return name != nsGkAtoms::_empty 263 ? GetNamedProgressTimeline(aPresContext->Document(), aTarget, 264 name) 265 : nullptr; 266 } 267 case StyleAnimationTimeline::Tag::Scroll: { 268 const auto& scroll = aStyleTimeline.AsScroll(); 269 return ScrollTimeline::MakeAnonymous(aPresContext->Document(), aTarget, 270 scroll.axis, scroll.scroller); 271 } 272 case StyleAnimationTimeline::Tag::View: { 273 const auto& view = aStyleTimeline.AsView(); 274 return ViewTimeline::MakeAnonymous(aPresContext->Document(), aTarget, 275 view.axis, view.inset); 276 } 277 case StyleAnimationTimeline::Tag::Auto: 278 return do_AddRef(aTarget.mElement->OwnerDoc()->Timeline()); 279 } 280 MOZ_ASSERT_UNREACHABLE("Unknown animation-timeline value?"); 281 return nullptr; 282 } 283 284 // Returns a new animation set up with given StyleAnimation. 285 // Or returns an existing animation matching StyleAnimation's name updated 286 // with the new StyleAnimation. 287 static already_AddRefed<CSSAnimation> BuildAnimation( 288 nsPresContext* aPresContext, const NonOwningAnimationTarget& aTarget, 289 const nsStyleUIReset& aStyle, uint32_t animIdx, 290 ServoCSSAnimationBuilder& aBuilder, 291 nsAnimationManager::CSSAnimationCollection* aCollection) { 292 MOZ_ASSERT(aPresContext); 293 294 nsAtom* animationName = aStyle.GetAnimationName(animIdx); 295 nsTArray<Keyframe> keyframes; 296 if (!aBuilder.BuildKeyframes(*aTarget.mElement, aPresContext, animationName, 297 aStyle.GetAnimationTimingFunction(animIdx), 298 keyframes)) { 299 return nullptr; 300 } 301 302 const StyleAnimationDuration& duration = aStyle.GetAnimationDuration(animIdx); 303 TimingParams timing = TimingParamsFromCSSParams( 304 duration.IsAuto() ? Nothing() : Some(duration.AsTime().ToMilliseconds()), 305 aStyle.GetAnimationDelay(animIdx).ToMilliseconds(), 306 aStyle.GetAnimationIterationCount(animIdx), 307 aStyle.GetAnimationDirection(animIdx), 308 aStyle.GetAnimationFillMode(animIdx)); 309 310 bool isStylePaused = 311 aStyle.GetAnimationPlayState(animIdx) == StyleAnimationPlayState::Paused; 312 313 RefPtr<dom::AnimationTimeline> timeline = 314 GetTimeline(aStyle.GetTimeline(animIdx), aPresContext, aTarget); 315 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); 320 321 const auto composition = StyleToDom(aStyle.GetAnimationComposition(animIdx)); 322 if (oldAnim) { 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 328 // them. See 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(); 335 } 336 337 KeyframeEffectParams effectOptions(composition); 338 auto effect = MakeRefPtr<dom::CSSAnimationKeyframeEffect>( 339 aPresContext->Document(), 340 OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoRequest), 341 std::move(timing), effectOptions); 342 343 aBuilder.SetKeyframes(*effect, std::move(keyframes), timeline); 344 345 auto animation = MakeRefPtr<CSSAnimation>( 346 aPresContext->Document()->GetScopeObject(), animationName); 347 animation->SetOwningElement( 348 OwningElementRef(*aTarget.mElement, aTarget.mPseudoRequest)); 349 350 animation->SetTimelineNoUpdate(timeline); 351 animation->SetEffectNoUpdate(effect); 352 353 if (isStylePaused) { 354 animation->PauseFromStyle(); 355 } else { 356 animation->PlayFromStyle(); 357 } 358 359 aBuilder.NotifyNewOrRemovedAnimation(*animation); 360 361 return animation.forget(); 362 } 363 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; 370 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) { 379 continue; 380 } 381 382 aReferencedAnimations.Insert(name); 383 RefPtr<CSSAnimation> dest = BuildAnimation(aPresContext, aTarget, aStyle, 384 animIdx, aBuilder, aCollection); 385 if (!dest) { 386 continue; 387 } 388 389 dest->SetAnimationIndex(static_cast<uint64_t>(animIdx)); 390 result.AppendElement(dest); 391 } 392 return result; 393 } 394 395 void nsAnimationManager::UpdateAnimations( 396 dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, 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 " 402 "document tree"); 403 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 411 // element here. 412 StopAnimationsForElement(aElement, aPseudoRequest); 413 return; 414 } 415 416 NonOwningAnimationTarget target(aElement, aPseudoRequest); 417 ServoCSSAnimationBuilder builder(aComputedStyle); 418 419 DoUpdateAnimations(target, *aComputedStyle->StyleUIReset(), builder); 420 } 421 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. 429 430 auto* collection = 431 CSSAnimationCollection::Get(aTarget.mElement, aTarget.mPseudoRequest); 432 if (!collection && aStyle.mAnimationNameCount == 1 && 433 aStyle.mAnimations[0].GetName() == nsGkAtoms::_empty) { 434 return; 435 } 436 437 nsAutoAnimationMutationBatch mb(aTarget.mElement->OwnerDoc()); 438 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); 444 445 if (newAnimations.IsEmpty()) { 446 if (collection) { 447 collection->Destroy(); 448 } 449 return; 450 } 451 452 if (!collection) { 453 collection = 454 &aTarget.mElement->EnsureAnimationData().EnsureAnimationCollection( 455 *aTarget.mElement, aTarget.mPseudoRequest); 456 if (!collection->isInList()) { 457 AddElementCollection(collection); 458 } 459 } 460 collection->mAnimations.SwapElements(newAnimations); 461 462 // Cancel removed animations 463 for (size_t newAnimIdx = newAnimations.Length(); newAnimIdx-- != 0;) { 464 aBuilder.NotifyNewOrRemovedAnimation(*newAnimations[newAnimIdx]); 465 newAnimations[newAnimIdx]->CancelFromStyle(PostRestyleMode::IfNeeded); 466 } 467 }