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