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