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 file,
5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "PendingAnimationTracker.h"
8 
9 #include "mozilla/PresShell.h"
10 #include "mozilla/dom/AnimationEffect.h"
11 #include "mozilla/dom/AnimationTimeline.h"
12 #include "mozilla/dom/Nullable.h"
13 #include "nsIFrame.h"
14 #include "nsTransitionManager.h"  // For CSSTransition
15 
16 using mozilla::dom::Nullable;
17 
18 namespace mozilla {
19 
NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker,mPlayPendingSet,mPausePendingSet,mDocument)20 NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker, mPlayPendingSet,
21                          mPausePendingSet, mDocument)
22 
23 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PendingAnimationTracker, AddRef)
24 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PendingAnimationTracker, Release)
25 
26 PendingAnimationTracker::PendingAnimationTracker(dom::Document* aDocument)
27     : mDocument(aDocument) {}
28 
AddPending(dom::Animation & aAnimation,AnimationSet & aSet)29 void PendingAnimationTracker::AddPending(dom::Animation& aAnimation,
30                                          AnimationSet& aSet) {
31   aSet.Insert(&aAnimation);
32 
33   // Schedule a paint. Otherwise animations that don't trigger a paint by
34   // themselves (e.g. CSS animations with an empty keyframes rule) won't
35   // start until something else paints.
36   EnsurePaintIsScheduled();
37 }
38 
RemovePending(dom::Animation & aAnimation,AnimationSet & aSet)39 void PendingAnimationTracker::RemovePending(dom::Animation& aAnimation,
40                                             AnimationSet& aSet) {
41   aSet.Remove(&aAnimation);
42 }
43 
IsWaiting(const dom::Animation & aAnimation,const AnimationSet & aSet) const44 bool PendingAnimationTracker::IsWaiting(const dom::Animation& aAnimation,
45                                         const AnimationSet& aSet) const {
46   return aSet.Contains(const_cast<dom::Animation*>(&aAnimation));
47 }
48 
TriggerPendingAnimationsOnNextTick(const TimeStamp & aReadyTime)49 void PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(
50     const TimeStamp& aReadyTime) {
51   auto triggerAnimationsAtReadyTime = [aReadyTime](
52                                           AnimationSet& aAnimationSet) {
53     for (auto iter = aAnimationSet.begin(), end = aAnimationSet.end();
54          iter != end; ++iter) {
55       dom::Animation* animation = *iter;
56       dom::AnimationTimeline* timeline = animation->GetTimeline();
57 
58       // If the animation does not have a timeline, just drop it from the map.
59       // The animation will detect that it is not being tracked and will trigger
60       // itself on the next tick where it has a timeline.
61       if (!timeline) {
62         aAnimationSet.Remove(iter);
63         continue;
64       }
65 
66       // When the timeline's refresh driver is under test control, its values
67       // have no correspondance to wallclock times so we shouldn't try to
68       // convert aReadyTime (which is a wallclock time) to a timeline value.
69       // Instead, the animation will be started/paused when the refresh driver
70       // is next advanced since this will trigger a call to
71       // TriggerPendingAnimationsNow.
72       if (!timeline->TracksWallclockTime()) {
73         continue;
74       }
75 
76       Nullable<TimeDuration> readyTime = timeline->ToTimelineTime(aReadyTime);
77       animation->TriggerOnNextTick(readyTime);
78 
79       aAnimationSet.Remove(iter);
80     }
81   };
82 
83   triggerAnimationsAtReadyTime(mPlayPendingSet);
84   triggerAnimationsAtReadyTime(mPausePendingSet);
85 
86   mHasPlayPendingGeometricAnimations =
87       mPlayPendingSet.Count() ? CheckState::Indeterminate : CheckState::Absent;
88 }
89 
TriggerPendingAnimationsNow()90 void PendingAnimationTracker::TriggerPendingAnimationsNow() {
91   auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) {
92     for (const auto& animation : aAnimationSet) {
93       animation->TriggerNow();
94     }
95     aAnimationSet.Clear();
96   };
97 
98   triggerAndClearAnimations(mPlayPendingSet);
99   triggerAndClearAnimations(mPausePendingSet);
100 
101   mHasPlayPendingGeometricAnimations = CheckState::Absent;
102 }
103 
IsTransition(const dom::Animation & aAnimation)104 static bool IsTransition(const dom::Animation& aAnimation) {
105   const dom::CSSTransition* transition = aAnimation.AsCSSTransition();
106   return transition && transition->IsTiedToMarkup();
107 }
108 
MarkAnimationsThatMightNeedSynchronization()109 void PendingAnimationTracker::MarkAnimationsThatMightNeedSynchronization() {
110   // We only set mHasPlayPendingGeometricAnimations to "present" in this method
111   // and nowhere else. After setting the state to "present", if there is any
112   // change to the set of play-pending animations we will reset
113   // mHasPlayPendingGeometricAnimations to either "indeterminate" or "absent".
114   //
115   // As a result, if mHasPlayPendingGeometricAnimations is "present", we can
116   // assume that this method has already been called for the current set of
117   // play-pending animations and it is not necessary to run this method again.
118   //
119   // If mHasPlayPendingGeometricAnimations is "absent", then we can also skip
120   // the body of this method since there are no notifications to be sent.
121   //
122   // Therefore, the only case we need to be concerned about is the
123   // "indeterminate" case. For all other cases we can return early.
124   //
125   // Note that *without* this optimization, starting animations would become
126   // O(n^2) in the case where each animation is on a different element and
127   // contains a compositor-animatable property since we would end up iterating
128   // over all animations in the play-pending set for each target element.
129   if (mHasPlayPendingGeometricAnimations != CheckState::Indeterminate) {
130     return;
131   }
132 
133   // We only synchronize CSS transitions with other CSS transitions (and we only
134   // synchronize non-transition animations with non-transition animations)
135   // since typically the author will not trigger both CSS animations and
136   // CSS transitions simultaneously and expect them to be synchronized.
137   //
138   // If we try to synchronize CSS transitions with non-transitions then for some
139   // content we will end up degrading performance by forcing animations to run
140   // on the main thread that really don't need to.
141 
142   mHasPlayPendingGeometricAnimations = CheckState::Absent;
143   for (const auto& animation : mPlayPendingSet) {
144     if (animation->GetEffect() && animation->GetEffect()->AffectsGeometry()) {
145       mHasPlayPendingGeometricAnimations &= ~CheckState::Absent;
146       mHasPlayPendingGeometricAnimations |= IsTransition(*animation)
147                                                 ? CheckState::TransitionsPresent
148                                                 : CheckState::AnimationsPresent;
149 
150       // If we have both transitions and animations we don't need to look any
151       // further.
152       if (mHasPlayPendingGeometricAnimations ==
153           (CheckState::TransitionsPresent | CheckState::AnimationsPresent)) {
154         break;
155       }
156     }
157   }
158 
159   if (mHasPlayPendingGeometricAnimations == CheckState::Absent) {
160     return;
161   }
162 
163   for (const auto& animation : mPlayPendingSet) {
164     bool isTransition = IsTransition(*animation);
165     if ((isTransition &&
166          mHasPlayPendingGeometricAnimations & CheckState::TransitionsPresent) ||
167         (!isTransition &&
168          mHasPlayPendingGeometricAnimations & CheckState::AnimationsPresent)) {
169       animation->NotifyGeometricAnimationsStartingThisFrame();
170     }
171   }
172 }
173 
EnsurePaintIsScheduled()174 void PendingAnimationTracker::EnsurePaintIsScheduled() {
175   if (!mDocument) {
176     return;
177   }
178 
179   PresShell* presShell = mDocument->GetPresShell();
180   if (!presShell) {
181     return;
182   }
183 
184   nsIFrame* rootFrame = presShell->GetRootFrame();
185   if (!rootFrame) {
186     return;
187   }
188 
189   rootFrame->SchedulePaintWithoutInvalidatingObservers();
190 }
191 
192 }  // namespace mozilla
193