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 "mozilla/dom/AnimationEffect.h"
8 #include "mozilla/dom/AnimationEffectBinding.h"
9 
10 #include "mozilla/dom/Animation.h"
11 #include "mozilla/dom/KeyframeEffect.h"
12 #include "mozilla/dom/MutationObservers.h"
13 #include "mozilla/AnimationUtils.h"
14 #include "mozilla/FloatingPoint.h"
15 #include "nsDOMMutationObserver.h"
16 
17 namespace mozilla::dom {
18 
19 NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffect)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffect)20 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffect)
21   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mAnimation)
22   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
23 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
24 
25 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffect)
26   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mAnimation)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
28 
29 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffect)
30 
31 NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffect)
32 NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffect)
33 
34 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffect)
35   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
36   NS_INTERFACE_MAP_ENTRY(nsISupports)
37 NS_INTERFACE_MAP_END
38 
39 AnimationEffect::AnimationEffect(Document* aDocument, TimingParams&& aTiming)
40     : mDocument(aDocument), mTiming(std::move(aTiming)) {}
41 
42 AnimationEffect::~AnimationEffect() = default;
43 
GetParentObject() const44 nsISupports* AnimationEffect::GetParentObject() const {
45   return ToSupports(mDocument);
46 }
47 
48 // https://drafts.csswg.org/web-animations/#current
IsCurrent() const49 bool AnimationEffect::IsCurrent() const {
50   if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
51     return false;
52   }
53 
54   ComputedTiming computedTiming = GetComputedTiming();
55   if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) {
56     return true;
57   }
58 
59   return (mAnimation->PlaybackRate() > 0 &&
60           computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) ||
61          (mAnimation->PlaybackRate() < 0 &&
62           computedTiming.mPhase == ComputedTiming::AnimationPhase::After);
63 }
64 
65 // https://drafts.csswg.org/web-animations/#in-effect
IsInEffect() const66 bool AnimationEffect::IsInEffect() const {
67   ComputedTiming computedTiming = GetComputedTiming();
68   return !computedTiming.mProgress.IsNull();
69 }
70 
SetSpecifiedTiming(TimingParams && aTiming)71 void AnimationEffect::SetSpecifiedTiming(TimingParams&& aTiming) {
72   if (mTiming == aTiming) {
73     return;
74   }
75 
76   mTiming = aTiming;
77 
78   if (mAnimation) {
79     Maybe<nsAutoAnimationMutationBatch> mb;
80     if (AsKeyframeEffect() && AsKeyframeEffect()->GetAnimationTarget()) {
81       mb.emplace(AsKeyframeEffect()->GetAnimationTarget().mElement->OwnerDoc());
82     }
83 
84     mAnimation->NotifyEffectTimingUpdated();
85 
86     if (mAnimation->IsRelevant()) {
87       MutationObservers::NotifyAnimationChanged(mAnimation);
88     }
89 
90     if (AsKeyframeEffect()) {
91       AsKeyframeEffect()->RequestRestyle(EffectCompositor::RestyleType::Layer);
92     }
93   }
94   // For keyframe effects, NotifyEffectTimingUpdated above will eventually
95   // cause KeyframeEffect::NotifyAnimationTimingUpdated to be called so it can
96   // update its registration with the target element as necessary.
97 }
98 
GetComputedTimingAt(const Nullable<TimeDuration> & aLocalTime,const TimingParams & aTiming,double aPlaybackRate)99 ComputedTiming AnimationEffect::GetComputedTimingAt(
100     const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
101     double aPlaybackRate) {
102   static const StickyTimeDuration zeroDuration;
103 
104   // Always return the same object to benefit from return-value optimization.
105   ComputedTiming result;
106 
107   if (aTiming.Duration()) {
108     MOZ_ASSERT(aTiming.Duration().ref() >= zeroDuration,
109                "Iteration duration should be positive");
110     result.mDuration = aTiming.Duration().ref();
111   }
112 
113   MOZ_ASSERT(aTiming.Iterations() >= 0.0 && !IsNaN(aTiming.Iterations()),
114              "mIterations should be nonnegative & finite, as ensured by "
115              "ValidateIterations or CSSParser");
116   result.mIterations = aTiming.Iterations();
117 
118   MOZ_ASSERT(aTiming.IterationStart() >= 0.0,
119              "mIterationStart should be nonnegative, as ensured by "
120              "ValidateIterationStart");
121   result.mIterationStart = aTiming.IterationStart();
122 
123   result.mActiveDuration = aTiming.ActiveDuration();
124   result.mEndTime = aTiming.EndTime();
125   result.mFill = aTiming.Fill() == dom::FillMode::Auto ? dom::FillMode::None
126                                                        : aTiming.Fill();
127 
128   // The default constructor for ComputedTiming sets all other members to
129   // values consistent with an animation that has not been sampled.
130   if (aLocalTime.IsNull()) {
131     return result;
132   }
133   const TimeDuration& localTime = aLocalTime.Value();
134 
135   StickyTimeDuration beforeActiveBoundary =
136       std::max(std::min(StickyTimeDuration(aTiming.Delay()), result.mEndTime),
137                zeroDuration);
138 
139   StickyTimeDuration activeAfterBoundary = std::max(
140       std::min(StickyTimeDuration(aTiming.Delay() + result.mActiveDuration),
141                result.mEndTime),
142       zeroDuration);
143 
144   if (localTime > activeAfterBoundary ||
145       (aPlaybackRate >= 0 && localTime == activeAfterBoundary)) {
146     result.mPhase = ComputedTiming::AnimationPhase::After;
147     if (!result.FillsForwards()) {
148       // The animation isn't active or filling at this time.
149       return result;
150     }
151     result.mActiveTime =
152         std::max(std::min(StickyTimeDuration(localTime - aTiming.Delay()),
153                           result.mActiveDuration),
154                  zeroDuration);
155   } else if (localTime < beforeActiveBoundary ||
156              (aPlaybackRate < 0 && localTime == beforeActiveBoundary)) {
157     result.mPhase = ComputedTiming::AnimationPhase::Before;
158     if (!result.FillsBackwards()) {
159       // The animation isn't active or filling at this time.
160       return result;
161     }
162     result.mActiveTime =
163         std::max(StickyTimeDuration(localTime - aTiming.Delay()), zeroDuration);
164   } else {
165     MOZ_ASSERT(result.mActiveDuration,
166                "How can we be in the middle of a zero-duration interval?");
167     result.mPhase = ComputedTiming::AnimationPhase::Active;
168     result.mActiveTime = localTime - aTiming.Delay();
169   }
170 
171   // Convert active time to a multiple of iterations.
172   // https://drafts.csswg.org/web-animations/#overall-progress
173   double overallProgress;
174   if (!result.mDuration) {
175     overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before
176                           ? 0.0
177                           : result.mIterations;
178   } else {
179     overallProgress = result.mActiveTime / result.mDuration;
180   }
181 
182   // Factor in iteration start offset.
183   if (IsFinite(overallProgress)) {
184     overallProgress += result.mIterationStart;
185   }
186 
187   // Determine the 0-based index of the current iteration.
188   // https://drafts.csswg.org/web-animations/#current-iteration
189   result.mCurrentIteration =
190       (result.mIterations >= double(UINT64_MAX) &&
191        result.mPhase == ComputedTiming::AnimationPhase::After) ||
192               overallProgress >= double(UINT64_MAX)
193           ? UINT64_MAX  // In GetComputedTimingDictionary(),
194                         // we will convert this into Infinity
195           : static_cast<uint64_t>(overallProgress);
196 
197   // Convert the overall progress to a fraction of a single iteration--the
198   // simply iteration progress.
199   // https://drafts.csswg.org/web-animations/#simple-iteration-progress
200   double progress = IsFinite(overallProgress)
201                         ? fmod(overallProgress, 1.0)
202                         : fmod(result.mIterationStart, 1.0);
203 
204   // When we are at the end of the active interval and the end of an iteration
205   // we need to report the end of the final iteration and not the start of the
206   // next iteration. We *don't* want to do this, however, when we have
207   // a zero-iteration animation.
208   if (progress == 0.0 &&
209       (result.mPhase == ComputedTiming::AnimationPhase::After ||
210        result.mPhase == ComputedTiming::AnimationPhase::Active) &&
211       result.mActiveTime == result.mActiveDuration &&
212       result.mIterations != 0.0) {
213     // The only way we can reach the end of the active interval and have
214     // a progress of zero and a current iteration of zero, is if we have a
215     // zero iteration count -- something we should have detected above.
216     MOZ_ASSERT(result.mCurrentIteration != 0,
217                "Should not have zero current iteration");
218     progress = 1.0;
219     if (result.mCurrentIteration != UINT64_MAX) {
220       result.mCurrentIteration--;
221     }
222   }
223 
224   // Factor in the direction.
225   bool thisIterationReverse = false;
226   switch (aTiming.Direction()) {
227     case PlaybackDirection::Normal:
228       thisIterationReverse = false;
229       break;
230     case PlaybackDirection::Reverse:
231       thisIterationReverse = true;
232       break;
233     case PlaybackDirection::Alternate:
234       thisIterationReverse = (result.mCurrentIteration & 1) == 1;
235       break;
236     case PlaybackDirection::Alternate_reverse:
237       thisIterationReverse = (result.mCurrentIteration & 1) == 0;
238       break;
239     default:
240       MOZ_ASSERT_UNREACHABLE("Unknown PlaybackDirection type");
241   }
242   if (thisIterationReverse) {
243     progress = 1.0 - progress;
244   }
245 
246   // Calculate the 'before flag' which we use when applying step timing
247   // functions.
248   if ((result.mPhase == ComputedTiming::AnimationPhase::After &&
249        thisIterationReverse) ||
250       (result.mPhase == ComputedTiming::AnimationPhase::Before &&
251        !thisIterationReverse)) {
252     result.mBeforeFlag = ComputedTimingFunction::BeforeFlag::Set;
253   }
254 
255   // Apply the easing.
256   if (aTiming.TimingFunction()) {
257     progress = aTiming.TimingFunction()->GetValue(progress, result.mBeforeFlag);
258   }
259 
260   MOZ_ASSERT(IsFinite(progress), "Progress value should be finite");
261   result.mProgress.SetValue(progress);
262   return result;
263 }
264 
GetComputedTiming(const TimingParams * aTiming) const265 ComputedTiming AnimationEffect::GetComputedTiming(
266     const TimingParams* aTiming) const {
267   double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
268   return GetComputedTimingAt(
269       GetLocalTime(), aTiming ? *aTiming : SpecifiedTiming(), playbackRate);
270 }
271 
272 // Helper function for generating an (Computed)EffectTiming dictionary
GetEffectTimingDictionary(const TimingParams & aTiming,EffectTiming & aRetVal)273 static void GetEffectTimingDictionary(const TimingParams& aTiming,
274                                       EffectTiming& aRetVal) {
275   aRetVal.mDelay = aTiming.Delay().ToMilliseconds();
276   aRetVal.mEndDelay = aTiming.EndDelay().ToMilliseconds();
277   aRetVal.mFill = aTiming.Fill();
278   aRetVal.mIterationStart = aTiming.IterationStart();
279   aRetVal.mIterations = aTiming.Iterations();
280   if (aTiming.Duration()) {
281     aRetVal.mDuration.SetAsUnrestrictedDouble() =
282         aTiming.Duration()->ToMilliseconds();
283   }
284   aRetVal.mDirection = aTiming.Direction();
285   if (aTiming.TimingFunction()) {
286     aRetVal.mEasing.Truncate();
287     aTiming.TimingFunction()->AppendToString(aRetVal.mEasing);
288   }
289 }
290 
GetTiming(EffectTiming & aRetVal) const291 void AnimationEffect::GetTiming(EffectTiming& aRetVal) const {
292   GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
293 }
294 
GetComputedTimingAsDict(ComputedEffectTiming & aRetVal) const295 void AnimationEffect::GetComputedTimingAsDict(
296     ComputedEffectTiming& aRetVal) const {
297   // Specified timing
298   GetEffectTimingDictionary(SpecifiedTiming(), aRetVal);
299 
300   // Computed timing
301   double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
302   const Nullable<TimeDuration> currentTime = GetLocalTime();
303   ComputedTiming computedTiming =
304       GetComputedTimingAt(currentTime, SpecifiedTiming(), playbackRate);
305 
306   aRetVal.mDuration.SetAsUnrestrictedDouble() =
307       computedTiming.mDuration.ToMilliseconds();
308   aRetVal.mFill = computedTiming.mFill;
309   aRetVal.mActiveDuration = computedTiming.mActiveDuration.ToMilliseconds();
310   aRetVal.mEndTime = computedTiming.mEndTime.ToMilliseconds();
311   aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(currentTime);
312   aRetVal.mProgress = computedTiming.mProgress;
313 
314   if (!aRetVal.mProgress.IsNull()) {
315     // Convert the returned currentIteration into Infinity if we set
316     // (uint64_t) computedTiming.mCurrentIteration to UINT64_MAX
317     double iteration =
318         computedTiming.mCurrentIteration == UINT64_MAX
319             ? PositiveInfinity<double>()
320             : static_cast<double>(computedTiming.mCurrentIteration);
321     aRetVal.mCurrentIteration.SetValue(iteration);
322   }
323 }
324 
UpdateTiming(const OptionalEffectTiming & aTiming,ErrorResult & aRv)325 void AnimationEffect::UpdateTiming(const OptionalEffectTiming& aTiming,
326                                    ErrorResult& aRv) {
327   TimingParams timing =
328       TimingParams::MergeOptionalEffectTiming(mTiming, aTiming, aRv);
329   if (aRv.Failed()) {
330     return;
331   }
332 
333   SetSpecifiedTiming(std::move(timing));
334 }
335 
GetLocalTime() const336 Nullable<TimeDuration> AnimationEffect::GetLocalTime() const {
337   // Since the *animation* start time is currently always zero, the local
338   // time is equal to the parent time.
339   Nullable<TimeDuration> result;
340   if (mAnimation) {
341     result = mAnimation->GetCurrentTimeAsDuration();
342   }
343   return result;
344 }
345 
346 }  // namespace mozilla::dom
347