/src/mozilla-central/dom/smil/nsSMILAnimationController.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 |
5 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | |
7 | | #include "nsSMILAnimationController.h" |
8 | | |
9 | | #include <algorithm> |
10 | | |
11 | | #include "mozilla/AutoRestore.h" |
12 | | #include "mozilla/RestyleManager.h" |
13 | | #include "mozilla/dom/Element.h" |
14 | | #include "mozilla/dom/SVGAnimationElement.h" |
15 | | #include "nsContentUtils.h" |
16 | | #include "nsCSSProps.h" |
17 | | #include "nsIDocument.h" |
18 | | #include "nsIPresShell.h" |
19 | | #include "nsIPresShellInlines.h" |
20 | | #include "nsITimer.h" |
21 | | #include "nsSMILCompositor.h" |
22 | | #include "nsSMILCSSProperty.h" |
23 | | #include "nsSMILTimedElement.h" |
24 | | |
25 | | using namespace mozilla; |
26 | | using namespace mozilla::dom; |
27 | | |
28 | | //---------------------------------------------------------------------- |
29 | | // nsSMILAnimationController implementation |
30 | | |
31 | | //---------------------------------------------------------------------- |
32 | | // ctors, dtors, factory methods |
33 | | |
34 | | nsSMILAnimationController::nsSMILAnimationController(nsIDocument* aDoc) |
35 | | : mAvgTimeBetweenSamples(0), |
36 | | mResampleNeeded(false), |
37 | | mDeferredStartSampling(false), |
38 | | mRunningSample(false), |
39 | | mRegisteredWithRefreshDriver(false), |
40 | | mMightHavePendingStyleUpdates(false), |
41 | | mDocument(aDoc) |
42 | 0 | { |
43 | 0 | MOZ_ASSERT(aDoc, "need a non-null document"); |
44 | 0 |
|
45 | 0 | nsRefreshDriver* refreshDriver = GetRefreshDriver(); |
46 | 0 | if (refreshDriver) { |
47 | 0 | mStartTime = refreshDriver->MostRecentRefresh(); |
48 | 0 | } else { |
49 | 0 | mStartTime = mozilla::TimeStamp::Now(); |
50 | 0 | } |
51 | 0 | mCurrentSampleTime = mStartTime; |
52 | 0 |
|
53 | 0 | Begin(); |
54 | 0 | } |
55 | | |
56 | | nsSMILAnimationController::~nsSMILAnimationController() |
57 | 0 | { |
58 | 0 | NS_ASSERTION(mAnimationElementTable.Count() == 0, |
59 | 0 | "Animation controller shouldn't be tracking any animation" |
60 | 0 | " elements when it dies"); |
61 | 0 | NS_ASSERTION(!mRegisteredWithRefreshDriver, |
62 | 0 | "Leaving stale entry in refresh driver's observer list"); |
63 | 0 | } |
64 | | |
65 | | void |
66 | | nsSMILAnimationController::Disconnect() |
67 | 0 | { |
68 | 0 | MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?"); |
69 | 0 | MOZ_ASSERT(mRefCnt.get() == 1, |
70 | 0 | "Expecting to disconnect when doc is sole remaining owner"); |
71 | 0 | NS_ASSERTION(mPauseState & nsSMILTimeContainer::PAUSE_PAGEHIDE, |
72 | 0 | "Expecting to be paused for pagehide before disconnect"); |
73 | 0 |
|
74 | 0 | StopSampling(GetRefreshDriver()); |
75 | 0 |
|
76 | 0 | mDocument = nullptr; // (raw pointer) |
77 | 0 | } |
78 | | |
79 | | //---------------------------------------------------------------------- |
80 | | // nsSMILTimeContainer methods: |
81 | | |
82 | | void |
83 | | nsSMILAnimationController::Pause(uint32_t aType) |
84 | 0 | { |
85 | 0 | nsSMILTimeContainer::Pause(aType); |
86 | 0 |
|
87 | 0 | if (mPauseState) { |
88 | 0 | mDeferredStartSampling = false; |
89 | 0 | StopSampling(GetRefreshDriver()); |
90 | 0 | } |
91 | 0 | } |
92 | | |
93 | | void |
94 | | nsSMILAnimationController::Resume(uint32_t aType) |
95 | 0 | { |
96 | 0 | bool wasPaused = (mPauseState != 0); |
97 | 0 | // Update mCurrentSampleTime so that calls to GetParentTime--used for |
98 | 0 | // calculating parent offsets--are accurate |
99 | 0 | mCurrentSampleTime = mozilla::TimeStamp::Now(); |
100 | 0 |
|
101 | 0 | nsSMILTimeContainer::Resume(aType); |
102 | 0 |
|
103 | 0 | if (wasPaused && !mPauseState && mChildContainerTable.Count()) { |
104 | 0 | MaybeStartSampling(GetRefreshDriver()); |
105 | 0 | Sample(); // Run the first sample manually |
106 | 0 | } |
107 | 0 | } |
108 | | |
109 | | nsSMILTime |
110 | | nsSMILAnimationController::GetParentTime() const |
111 | 0 | { |
112 | 0 | return (nsSMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); |
113 | 0 | } |
114 | | |
115 | | //---------------------------------------------------------------------- |
116 | | // nsARefreshObserver methods: |
117 | | NS_IMPL_ADDREF(nsSMILAnimationController) |
118 | | NS_IMPL_RELEASE(nsSMILAnimationController) |
119 | | |
120 | | // nsRefreshDriver Callback function |
121 | | void |
122 | | nsSMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) |
123 | 0 | { |
124 | 0 | // Although we never expect aTime to go backwards, when we initialise the |
125 | 0 | // animation controller, if we can't get hold of a refresh driver we |
126 | 0 | // initialise mCurrentSampleTime to Now(). It may be possible that after |
127 | 0 | // doing so we get sampled by a refresh driver whose most recent refresh time |
128 | 0 | // predates when we were initialised, so to be safe we make sure to take the |
129 | 0 | // most recent time here. |
130 | 0 | aTime = std::max(mCurrentSampleTime, aTime); |
131 | 0 |
|
132 | 0 | // Sleep detection: If the time between samples is a whole lot greater than we |
133 | 0 | // were expecting then we assume the computer went to sleep or someone's |
134 | 0 | // messing with the clock. In that case, fiddle our parent offset and use our |
135 | 0 | // average time between samples to calculate the new sample time. This |
136 | 0 | // prevents us from hanging while trying to catch up on all the missed time. |
137 | 0 |
|
138 | 0 | // Smoothing of coefficient for the average function. 0.2 should let us track |
139 | 0 | // the sample rate reasonably tightly without being overly affected by |
140 | 0 | // occasional delays. |
141 | 0 | static const double SAMPLE_DUR_WEIGHTING = 0.2; |
142 | 0 | // If the elapsed time exceeds our expectation by this number of times we'll |
143 | 0 | // initiate special behaviour to basically ignore the intervening time. |
144 | 0 | static const double SAMPLE_DEV_THRESHOLD = 200.0; |
145 | 0 |
|
146 | 0 | nsSMILTime elapsedTime = |
147 | 0 | (nsSMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); |
148 | 0 | if (mAvgTimeBetweenSamples == 0) { |
149 | 0 | // First sample. |
150 | 0 | mAvgTimeBetweenSamples = elapsedTime; |
151 | 0 | } else { |
152 | 0 | if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { |
153 | 0 | // Unexpectedly long delay between samples. |
154 | 0 | NS_WARNING("Detected really long delay between samples, continuing from " |
155 | 0 | "previous sample"); |
156 | 0 | mParentOffset += elapsedTime - mAvgTimeBetweenSamples; |
157 | 0 | } |
158 | 0 | // Update the moving average. Due to truncation here the average will |
159 | 0 | // normally be a little less than it should be but that's probably ok. |
160 | 0 | mAvgTimeBetweenSamples = |
161 | 0 | (nsSMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + |
162 | 0 | mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); |
163 | 0 | } |
164 | 0 | mCurrentSampleTime = aTime; |
165 | 0 |
|
166 | 0 | Sample(); |
167 | 0 | } |
168 | | |
169 | | //---------------------------------------------------------------------- |
170 | | // Animation element registration methods: |
171 | | |
172 | | void |
173 | | nsSMILAnimationController::RegisterAnimationElement( |
174 | | SVGAnimationElement* aAnimationElement) |
175 | 0 | { |
176 | 0 | mAnimationElementTable.PutEntry(aAnimationElement); |
177 | 0 | if (mDeferredStartSampling) { |
178 | 0 | mDeferredStartSampling = false; |
179 | 0 | if (mChildContainerTable.Count()) { |
180 | 0 | // mAnimationElementTable was empty, but now we've added its 1st element |
181 | 0 | MOZ_ASSERT(mAnimationElementTable.Count() == 1, |
182 | 0 | "we shouldn't have deferred sampling if we already had " |
183 | 0 | "animations registered"); |
184 | 0 | StartSampling(GetRefreshDriver()); |
185 | 0 | Sample(); // Run the first sample manually |
186 | 0 | } // else, don't sample until a time container is registered (via AddChild) |
187 | 0 | } |
188 | 0 | } |
189 | | |
190 | | void |
191 | | nsSMILAnimationController::UnregisterAnimationElement( |
192 | | SVGAnimationElement* aAnimationElement) |
193 | 0 | { |
194 | 0 | mAnimationElementTable.RemoveEntry(aAnimationElement); |
195 | 0 | } |
196 | | |
197 | | //---------------------------------------------------------------------- |
198 | | // Page show/hide |
199 | | |
200 | | void |
201 | | nsSMILAnimationController::OnPageShow() |
202 | 0 | { |
203 | 0 | Resume(nsSMILTimeContainer::PAUSE_PAGEHIDE); |
204 | 0 | } |
205 | | |
206 | | void |
207 | | nsSMILAnimationController::OnPageHide() |
208 | 0 | { |
209 | 0 | Pause(nsSMILTimeContainer::PAUSE_PAGEHIDE); |
210 | 0 | } |
211 | | |
212 | | //---------------------------------------------------------------------- |
213 | | // Cycle-collection support |
214 | | |
215 | | void |
216 | | nsSMILAnimationController::Traverse( |
217 | | nsCycleCollectionTraversalCallback* aCallback) |
218 | 0 | { |
219 | 0 | // Traverse last compositor table |
220 | 0 | if (mLastCompositorTable) { |
221 | 0 | for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { |
222 | 0 | nsSMILCompositor* compositor = iter.Get(); |
223 | 0 | compositor->Traverse(aCallback); |
224 | 0 | } |
225 | 0 | } |
226 | 0 | } |
227 | | |
228 | | void |
229 | | nsSMILAnimationController::Unlink() |
230 | 0 | { |
231 | 0 | mLastCompositorTable = nullptr; |
232 | 0 | } |
233 | | |
234 | | //---------------------------------------------------------------------- |
235 | | // Refresh driver lifecycle related methods |
236 | | |
237 | | void |
238 | | nsSMILAnimationController::NotifyRefreshDriverCreated( |
239 | | nsRefreshDriver* aRefreshDriver) |
240 | 0 | { |
241 | 0 | if (!mPauseState) { |
242 | 0 | MaybeStartSampling(aRefreshDriver); |
243 | 0 | } |
244 | 0 | } |
245 | | |
246 | | void |
247 | | nsSMILAnimationController::NotifyRefreshDriverDestroying( |
248 | | nsRefreshDriver* aRefreshDriver) |
249 | 0 | { |
250 | 0 | if (!mPauseState && !mDeferredStartSampling) { |
251 | 0 | StopSampling(aRefreshDriver); |
252 | 0 | } |
253 | 0 | } |
254 | | |
255 | | //---------------------------------------------------------------------- |
256 | | // Timer-related implementation helpers |
257 | | |
258 | | void |
259 | | nsSMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) |
260 | 0 | { |
261 | 0 | NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused"); |
262 | 0 | NS_ASSERTION(!mDeferredStartSampling, |
263 | 0 | "Started sampling but the deferred start flag is still set"); |
264 | 0 | if (aRefreshDriver) { |
265 | 0 | MOZ_ASSERT(!mRegisteredWithRefreshDriver, |
266 | 0 | "Redundantly registering with refresh driver"); |
267 | 0 | MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), |
268 | 0 | "Starting sampling with wrong refresh driver"); |
269 | 0 | // We're effectively resuming from a pause so update our current sample time |
270 | 0 | // or else it will confuse our "average time between samples" calculations. |
271 | 0 | mCurrentSampleTime = mozilla::TimeStamp::Now(); |
272 | 0 | aRefreshDriver->AddRefreshObserver(this, FlushType::Style); |
273 | 0 | mRegisteredWithRefreshDriver = true; |
274 | 0 | } |
275 | 0 | } |
276 | | |
277 | | void |
278 | | nsSMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) |
279 | 0 | { |
280 | 0 | if (aRefreshDriver && mRegisteredWithRefreshDriver) { |
281 | 0 | // NOTE: The document might already have been detached from its PresContext |
282 | 0 | // (and RefreshDriver), which would make GetRefreshDriver() return null. |
283 | 0 | MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), |
284 | 0 | "Stopping sampling with wrong refresh driver"); |
285 | 0 | aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); |
286 | 0 | mRegisteredWithRefreshDriver = false; |
287 | 0 | } |
288 | 0 | } |
289 | | |
290 | | void |
291 | | nsSMILAnimationController::MaybeStartSampling(nsRefreshDriver* aRefreshDriver) |
292 | 0 | { |
293 | 0 | if (mDeferredStartSampling) { |
294 | 0 | // We've received earlier 'MaybeStartSampling' calls, and we're |
295 | 0 | // deferring until we get a registered animation. |
296 | 0 | return; |
297 | 0 | } |
298 | 0 | |
299 | 0 | if (mAnimationElementTable.Count()) { |
300 | 0 | StartSampling(aRefreshDriver); |
301 | 0 | } else { |
302 | 0 | mDeferredStartSampling = true; |
303 | 0 | } |
304 | 0 | } |
305 | | |
306 | | //---------------------------------------------------------------------- |
307 | | // Sample-related methods and callbacks |
308 | | |
309 | | void |
310 | | nsSMILAnimationController::DoSample() |
311 | 0 | { |
312 | 0 | DoSample(true); // Skip unchanged time containers |
313 | 0 | } |
314 | | |
315 | | void |
316 | | nsSMILAnimationController::DoSample(bool aSkipUnchangedContainers) |
317 | 0 | { |
318 | 0 | if (!mDocument) { |
319 | 0 | NS_ERROR("Shouldn't be sampling after document has disconnected"); |
320 | 0 | return; |
321 | 0 | } |
322 | 0 | if (mRunningSample) { |
323 | 0 | NS_ERROR("Shouldn't be recursively sampling"); |
324 | 0 | return; |
325 | 0 | } |
326 | 0 |
|
327 | 0 | bool isStyleFlushNeeded = mResampleNeeded; |
328 | 0 | mResampleNeeded = false; |
329 | 0 |
|
330 | 0 | nsCOMPtr<nsIDocument> document(mDocument); // keeps 'this' alive too |
331 | 0 |
|
332 | 0 | // Set running sample flag -- do this before flushing styles so that when we |
333 | 0 | // flush styles we don't end up requesting extra samples |
334 | 0 | AutoRestore<bool> autoRestoreRunningSample(mRunningSample); |
335 | 0 | mRunningSample = true; |
336 | 0 |
|
337 | 0 | // STEP 1: Bring model up to date |
338 | 0 | // (i) Rewind elements where necessary |
339 | 0 | // (ii) Run milestone samples |
340 | 0 | RewindElements(); |
341 | 0 | DoMilestoneSamples(); |
342 | 0 |
|
343 | 0 | // STEP 2: Sample the child time containers |
344 | 0 | // |
345 | 0 | // When we sample the child time containers they will simply record the sample |
346 | 0 | // time in document time. |
347 | 0 | TimeContainerHashtable activeContainers(mChildContainerTable.Count()); |
348 | 0 | for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { |
349 | 0 | nsSMILTimeContainer* container = iter.Get()->GetKey(); |
350 | 0 | if (!container) { |
351 | 0 | continue; |
352 | 0 | } |
353 | 0 | |
354 | 0 | if (!container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN) && |
355 | 0 | (container->NeedsSample() || !aSkipUnchangedContainers)) { |
356 | 0 | container->ClearMilestones(); |
357 | 0 | container->Sample(); |
358 | 0 | container->MarkSeekFinished(); |
359 | 0 | activeContainers.PutEntry(container); |
360 | 0 | } |
361 | 0 | } |
362 | 0 |
|
363 | 0 | // STEP 3: (i) Sample the timed elements AND |
364 | 0 | // (ii) Create a table of compositors |
365 | 0 | // |
366 | 0 | // (i) Here we sample the timed elements (fetched from the |
367 | 0 | // SVGAnimationElements) which determine from the active time if the |
368 | 0 | // element is active and what its simple time etc. is. This information is |
369 | 0 | // then passed to its time client (nsSMILAnimationFunction). |
370 | 0 | // |
371 | 0 | // (ii) During the same loop we also build up a table that contains one |
372 | 0 | // compositor for each animated attribute and which maps animated elements to |
373 | 0 | // the corresponding compositor for their target attribute. |
374 | 0 | // |
375 | 0 | // Note that this compositor table needs to be allocated on the heap so we can |
376 | 0 | // store it until the next sample. This lets us find out which elements were |
377 | 0 | // animated in sample 'n-1' but not in sample 'n' (and hence need to have |
378 | 0 | // their animation effects removed in sample 'n'). |
379 | 0 | // |
380 | 0 | // Parts (i) and (ii) are not functionally related but we combine them here to |
381 | 0 | // save iterating over the animation elements twice. |
382 | 0 |
|
383 | 0 | // Create the compositor table |
384 | 0 | nsAutoPtr<nsSMILCompositorTable> |
385 | 0 | currentCompositorTable(new nsSMILCompositorTable(0)); |
386 | 0 | nsTArray<RefPtr<SVGAnimationElement>> |
387 | 0 | animElems(mAnimationElementTable.Count()); |
388 | 0 |
|
389 | 0 | for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { |
390 | 0 | SVGAnimationElement* animElem = iter.Get()->GetKey(); |
391 | 0 | SampleTimedElement(animElem, &activeContainers); |
392 | 0 | AddAnimationToCompositorTable(animElem, |
393 | 0 | currentCompositorTable, |
394 | 0 | isStyleFlushNeeded); |
395 | 0 | animElems.AppendElement(animElem); |
396 | 0 | } |
397 | 0 | activeContainers.Clear(); |
398 | 0 |
|
399 | 0 | // STEP 4: Compare previous sample's compositors against this sample's. |
400 | 0 | // (Transfer cached base values across, & remove animation effects from |
401 | 0 | // no-longer-animated targets.) |
402 | 0 | if (mLastCompositorTable) { |
403 | 0 | // * Transfer over cached base values, from last sample's compositors |
404 | 0 | for (auto iter = currentCompositorTable->Iter(); |
405 | 0 | !iter.Done(); |
406 | 0 | iter.Next()) { |
407 | 0 | nsSMILCompositor* compositor = iter.Get(); |
408 | 0 | nsSMILCompositor* lastCompositor = |
409 | 0 | mLastCompositorTable->GetEntry(compositor->GetKey()); |
410 | 0 |
|
411 | 0 | if (lastCompositor) { |
412 | 0 | compositor->StealCachedBaseValue(lastCompositor); |
413 | 0 | } |
414 | 0 | } |
415 | 0 |
|
416 | 0 | // * For each compositor in current sample's hash table, remove entry from |
417 | 0 | // prev sample's hash table -- we don't need to clear animation |
418 | 0 | // effects of those compositors, since they're still being animated. |
419 | 0 | for (auto iter = currentCompositorTable->Iter(); |
420 | 0 | !iter.Done(); |
421 | 0 | iter.Next()) { |
422 | 0 | mLastCompositorTable->RemoveEntry(iter.Get()->GetKey()); |
423 | 0 | } |
424 | 0 |
|
425 | 0 | // * For each entry that remains in prev sample's hash table (i.e. for |
426 | 0 | // every target that's no longer animated), clear animation effects. |
427 | 0 | for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { |
428 | 0 | iter.Get()->ClearAnimationEffects(); |
429 | 0 | } |
430 | 0 | } |
431 | 0 |
|
432 | 0 | // return early if there are no active animations to avoid a style flush |
433 | 0 | if (currentCompositorTable->Count() == 0) { |
434 | 0 | mLastCompositorTable = nullptr; |
435 | 0 | return; |
436 | 0 | } |
437 | 0 | |
438 | 0 | if (isStyleFlushNeeded) { |
439 | 0 | document->FlushPendingNotifications(FlushType::Style); |
440 | 0 | } |
441 | 0 |
|
442 | 0 | // WARNING: |
443 | 0 | // WARNING: the above flush may have destroyed the pres shell and/or |
444 | 0 | // WARNING: frames and other layout related objects. |
445 | 0 | // WARNING: |
446 | 0 |
|
447 | 0 | // STEP 5: Compose currently-animated attributes. |
448 | 0 | // XXXdholbert: This step traverses our animation targets in an effectively |
449 | 0 | // random order. For animation from/to 'inherit' values to work correctly |
450 | 0 | // when the inherited value is *also* being animated, we really should be |
451 | 0 | // traversing our animated nodes in an ancestors-first order (bug 501183) |
452 | 0 | bool mightHavePendingStyleUpdates = false; |
453 | 0 | for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { |
454 | 0 | iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates); |
455 | 0 | } |
456 | 0 |
|
457 | 0 | // Update last compositor table |
458 | 0 | mLastCompositorTable = currentCompositorTable.forget(); |
459 | 0 | mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates; |
460 | 0 |
|
461 | 0 | NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); |
462 | 0 | } |
463 | | |
464 | | void |
465 | | nsSMILAnimationController::RewindElements() |
466 | 0 | { |
467 | 0 | bool rewindNeeded = false; |
468 | 0 | for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { |
469 | 0 | nsSMILTimeContainer* container = iter.Get()->GetKey(); |
470 | 0 | if (container->NeedsRewind()) { |
471 | 0 | rewindNeeded = true; |
472 | 0 | break; |
473 | 0 | } |
474 | 0 | } |
475 | 0 |
|
476 | 0 | if (!rewindNeeded) |
477 | 0 | return; |
478 | 0 | |
479 | 0 | for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { |
480 | 0 | SVGAnimationElement* animElem = iter.Get()->GetKey(); |
481 | 0 | nsSMILTimeContainer* timeContainer = animElem->GetTimeContainer(); |
482 | 0 | if (timeContainer && timeContainer->NeedsRewind()) { |
483 | 0 | animElem->TimedElement().Rewind(); |
484 | 0 | } |
485 | 0 | } |
486 | 0 |
|
487 | 0 | for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { |
488 | 0 | iter.Get()->GetKey()->ClearNeedsRewind(); |
489 | 0 | } |
490 | 0 | } |
491 | | |
492 | | void |
493 | | nsSMILAnimationController::DoMilestoneSamples() |
494 | 0 | { |
495 | 0 | // We need to sample the timing model but because SMIL operates independently |
496 | 0 | // of the frame-rate, we can get one sample at t=0s and the next at t=10min. |
497 | 0 | // |
498 | 0 | // In between those two sample times a whole string of significant events |
499 | 0 | // might be expected to take place: events firing, new interdependencies |
500 | 0 | // between animations resolved and dissolved, etc. |
501 | 0 | // |
502 | 0 | // Furthermore, at any given time, we want to sample all the intervals that |
503 | 0 | // end at that time BEFORE any that begin. This behaviour is implied by SMIL's |
504 | 0 | // endpoint-exclusive timing model. |
505 | 0 | // |
506 | 0 | // So we have the animations (specifically the timed elements) register the |
507 | 0 | // next significant moment (called a milestone) in their lifetime and then we |
508 | 0 | // step through the model at each of these moments and sample those animations |
509 | 0 | // registered for those times. This way events can fire in the correct order, |
510 | 0 | // dependencies can be resolved etc. |
511 | 0 |
|
512 | 0 | nsSMILTime sampleTime = INT64_MIN; |
513 | 0 |
|
514 | 0 | while (true) { |
515 | 0 | // We want to find any milestones AT OR BEFORE the current sample time so we |
516 | 0 | // initialise the next milestone to the moment after (1ms after, to be |
517 | 0 | // precise) the current sample time and see if there are any milestones |
518 | 0 | // before that. Any other milestones will be dealt with in a subsequent |
519 | 0 | // sample. |
520 | 0 | nsSMILMilestone nextMilestone(GetCurrentTime() + 1, true); |
521 | 0 | for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { |
522 | 0 | nsSMILTimeContainer* container = iter.Get()->GetKey(); |
523 | 0 | if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { |
524 | 0 | continue; |
525 | 0 | } |
526 | 0 | nsSMILMilestone thisMilestone; |
527 | 0 | bool didGetMilestone = |
528 | 0 | container->GetNextMilestoneInParentTime(thisMilestone); |
529 | 0 | if (didGetMilestone && thisMilestone < nextMilestone) { |
530 | 0 | nextMilestone = thisMilestone; |
531 | 0 | } |
532 | 0 | } |
533 | 0 |
|
534 | 0 | if (nextMilestone.mTime > GetCurrentTime()) { |
535 | 0 | break; |
536 | 0 | } |
537 | 0 | |
538 | 0 | nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements; |
539 | 0 | for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { |
540 | 0 | nsSMILTimeContainer* container = iter.Get()->GetKey(); |
541 | 0 | if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { |
542 | 0 | continue; |
543 | 0 | } |
544 | 0 | container->PopMilestoneElementsAtMilestone(nextMilestone, elements); |
545 | 0 | } |
546 | 0 |
|
547 | 0 | uint32_t length = elements.Length(); |
548 | 0 |
|
549 | 0 | // During the course of a sampling we don't want to actually go backwards. |
550 | 0 | // Due to negative offsets, early ends and the like, a timed element might |
551 | 0 | // register a milestone that is actually in the past. That's fine, but it's |
552 | 0 | // still only going to get *sampled* with whatever time we're up to and no |
553 | 0 | // earlier. |
554 | 0 | // |
555 | 0 | // Because we're only performing this clamping at the last moment, the |
556 | 0 | // animations will still all get sampled in the correct order and |
557 | 0 | // dependencies will be appropriately resolved. |
558 | 0 | sampleTime = std::max(nextMilestone.mTime, sampleTime); |
559 | 0 |
|
560 | 0 | for (uint32_t i = 0; i < length; ++i) { |
561 | 0 | SVGAnimationElement* elem = elements[i].get(); |
562 | 0 | MOZ_ASSERT(elem, "nullptr animation element in list"); |
563 | 0 | nsSMILTimeContainer* container = elem->GetTimeContainer(); |
564 | 0 | if (!container) |
565 | 0 | // The container may be nullptr if the element has been detached from its |
566 | 0 | // parent since registering a milestone. |
567 | 0 | continue; |
568 | 0 | |
569 | 0 | nsSMILTimeValue containerTimeValue = |
570 | 0 | container->ParentToContainerTime(sampleTime); |
571 | 0 | if (!containerTimeValue.IsDefinite()) |
572 | 0 | continue; |
573 | 0 | |
574 | 0 | // Clamp the converted container time to non-negative values. |
575 | 0 | nsSMILTime containerTime = std::max<nsSMILTime>(0, containerTimeValue.GetMillis()); |
576 | 0 |
|
577 | 0 | if (nextMilestone.mIsEnd) { |
578 | 0 | elem->TimedElement().SampleEndAt(containerTime); |
579 | 0 | } else { |
580 | 0 | elem->TimedElement().SampleAt(containerTime); |
581 | 0 | } |
582 | 0 | } |
583 | 0 | } |
584 | 0 | } |
585 | | |
586 | | /*static*/ void |
587 | | nsSMILAnimationController::SampleTimedElement( |
588 | | SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) |
589 | 0 | { |
590 | 0 | nsSMILTimeContainer* timeContainer = aElement->GetTimeContainer(); |
591 | 0 | if (!timeContainer) |
592 | 0 | return; |
593 | 0 | |
594 | 0 | // We'd like to call timeContainer->NeedsSample() here and skip all timed |
595 | 0 | // elements that belong to paused time containers that don't need a sample, |
596 | 0 | // but that doesn't work because we've already called Sample() on all the time |
597 | 0 | // containers so the paused ones don't need a sample any more and they'll |
598 | 0 | // return false. |
599 | 0 | // |
600 | 0 | // Instead we build up a hashmap of active time containers during the previous |
601 | 0 | // step (SampleTimeContainer) and then test here if the container for this |
602 | 0 | // timed element is in the list. |
603 | 0 | if (!aActiveContainers->GetEntry(timeContainer)) |
604 | 0 | return; |
605 | 0 | |
606 | 0 | nsSMILTime containerTime = timeContainer->GetCurrentTime(); |
607 | 0 |
|
608 | 0 | MOZ_ASSERT(!timeContainer->IsSeeking(), |
609 | 0 | "Doing a regular sample but the time container is still seeking"); |
610 | 0 | aElement->TimedElement().SampleAt(containerTime); |
611 | 0 | } |
612 | | |
613 | | /*static*/ void |
614 | | nsSMILAnimationController::AddAnimationToCompositorTable( |
615 | | SVGAnimationElement* aElement, |
616 | | nsSMILCompositorTable* aCompositorTable, |
617 | | bool& aStyleFlushNeeded) |
618 | 0 | { |
619 | 0 | // Add a compositor to the hash table if there's not already one there |
620 | 0 | nsSMILTargetIdentifier key; |
621 | 0 | if (!GetTargetIdentifierForAnimation(aElement, key)) |
622 | 0 | // Something's wrong/missing about animation's target; skip this animation |
623 | 0 | return; |
624 | 0 | |
625 | 0 | nsSMILAnimationFunction& func = aElement->AnimationFunction(); |
626 | 0 |
|
627 | 0 | // Only add active animation functions. If there are no active animations |
628 | 0 | // targeting an attribute, no compositor will be created and any previously |
629 | 0 | // applied animations will be cleared. |
630 | 0 | if (func.IsActiveOrFrozen()) { |
631 | 0 | // Look up the compositor for our target, & add our animation function |
632 | 0 | // to its list of animation functions. |
633 | 0 | nsSMILCompositor* result = aCompositorTable->PutEntry(key); |
634 | 0 | result->AddAnimationFunction(&func); |
635 | 0 |
|
636 | 0 | } else if (func.HasChanged()) { |
637 | 0 | // Look up the compositor for our target, and force it to skip the |
638 | 0 | // "nothing's changed so don't bother compositing" optimization for this |
639 | 0 | // sample. |func| is inactive, but it's probably *newly* inactive (since |
640 | 0 | // it's got HasChanged() == true), so we need to make sure to recompose |
641 | 0 | // its target. |
642 | 0 | nsSMILCompositor* result = aCompositorTable->PutEntry(key); |
643 | 0 | result->ToggleForceCompositing(); |
644 | 0 |
|
645 | 0 | // We've now made sure that |func|'s inactivity will be reflected as of |
646 | 0 | // this sample. We need to clear its HasChanged() flag so that it won't |
647 | 0 | // trigger this same clause in future samples (until it changes again). |
648 | 0 | func.ClearHasChanged(); |
649 | 0 | } |
650 | 0 | aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); |
651 | 0 | } |
652 | | |
653 | | static inline bool |
654 | | IsTransformAttribute(int32_t aNamespaceID, nsAtom *aAttributeName) |
655 | 0 | { |
656 | 0 | return aNamespaceID == kNameSpaceID_None && |
657 | 0 | (aAttributeName == nsGkAtoms::transform || |
658 | 0 | aAttributeName == nsGkAtoms::patternTransform || |
659 | 0 | aAttributeName == nsGkAtoms::gradientTransform); |
660 | 0 | } |
661 | | |
662 | | // Helper function that, given a SVGAnimationElement, looks up its target |
663 | | // element & target attribute and populates a nsSMILTargetIdentifier |
664 | | // for this target. |
665 | | /*static*/ bool |
666 | | nsSMILAnimationController::GetTargetIdentifierForAnimation( |
667 | | SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult) |
668 | 0 | { |
669 | 0 | // Look up target (animated) element |
670 | 0 | Element* targetElem = aAnimElem->GetTargetElementContent(); |
671 | 0 | if (!targetElem) |
672 | 0 | // Animation has no target elem -- skip it. |
673 | 0 | return false; |
674 | 0 | |
675 | 0 | // Look up target (animated) attribute |
676 | 0 | // SMILANIM section 3.1, attributeName may |
677 | 0 | // have an XMLNS prefix to indicate the XML namespace. |
678 | 0 | RefPtr<nsAtom> attributeName; |
679 | 0 | int32_t attributeNamespaceID; |
680 | 0 | if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, |
681 | 0 | getter_AddRefs(attributeName))) |
682 | 0 | // Animation has no target attr -- skip it. |
683 | 0 | return false; |
684 | 0 | |
685 | 0 | // animateTransform can only animate transforms, conversely transforms |
686 | 0 | // can only be animated by animateTransform |
687 | 0 | if (IsTransformAttribute(attributeNamespaceID, attributeName) != |
688 | 0 | (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform))) |
689 | 0 | return false; |
690 | 0 | |
691 | 0 | // Construct the key |
692 | 0 | aResult.mElement = targetElem; |
693 | 0 | aResult.mAttributeName = attributeName; |
694 | 0 | aResult.mAttributeNamespaceID = attributeNamespaceID; |
695 | 0 |
|
696 | 0 | return true; |
697 | 0 | } |
698 | | |
699 | | bool |
700 | | nsSMILAnimationController::PreTraverse() |
701 | 0 | { |
702 | 0 | return PreTraverseInSubtree(nullptr); |
703 | 0 | } |
704 | | |
705 | | bool |
706 | | nsSMILAnimationController::PreTraverseInSubtree(Element* aRoot) |
707 | 0 | { |
708 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
709 | 0 |
|
710 | 0 | if (!mMightHavePendingStyleUpdates) { |
711 | 0 | return false; |
712 | 0 | } |
713 | 0 | |
714 | 0 | nsPresContext* context = mDocument->GetPresContext(); |
715 | 0 | if (!context) { |
716 | 0 | return false; |
717 | 0 | } |
718 | 0 | |
719 | 0 | bool foundElementsNeedingRestyle = false; |
720 | 0 | for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { |
721 | 0 | SVGAnimationElement* animElement = iter.Get()->GetKey(); |
722 | 0 |
|
723 | 0 | nsSMILTargetIdentifier key; |
724 | 0 | if (!GetTargetIdentifierForAnimation(animElement, key)) { |
725 | 0 | // Something's wrong/missing about animation's target; skip this animation |
726 | 0 | continue; |
727 | 0 | } |
728 | 0 | |
729 | 0 | // Ignore restyles that aren't in the flattened tree subtree rooted at |
730 | 0 | // aRoot. |
731 | 0 | if (aRoot && |
732 | 0 | !nsContentUtils::ContentIsFlattenedTreeDescendantOf(key.mElement, |
733 | 0 | aRoot)) { |
734 | 0 | continue; |
735 | 0 | } |
736 | 0 | |
737 | 0 | context->RestyleManager()-> |
738 | 0 | PostRestyleEventForAnimations(key.mElement, |
739 | 0 | CSSPseudoElementType::NotPseudo, |
740 | 0 | eRestyle_StyleAttribute_Animations); |
741 | 0 |
|
742 | 0 | foundElementsNeedingRestyle = true; |
743 | 0 | } |
744 | 0 |
|
745 | 0 | // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted |
746 | 0 | // all restyles. |
747 | 0 | if (!aRoot) { |
748 | 0 | mMightHavePendingStyleUpdates = false; |
749 | 0 | } |
750 | 0 |
|
751 | 0 | return foundElementsNeedingRestyle; |
752 | 0 | } |
753 | | |
754 | | //---------------------------------------------------------------------- |
755 | | // Add/remove child time containers |
756 | | |
757 | | nsresult |
758 | | nsSMILAnimationController::AddChild(nsSMILTimeContainer& aChild) |
759 | 0 | { |
760 | 0 | TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); |
761 | 0 | NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); |
762 | 0 |
|
763 | 0 | if (!mPauseState && mChildContainerTable.Count() == 1) { |
764 | 0 | MaybeStartSampling(GetRefreshDriver()); |
765 | 0 | Sample(); // Run the first sample manually |
766 | 0 | } |
767 | 0 |
|
768 | 0 | return NS_OK; |
769 | 0 | } |
770 | | |
771 | | void |
772 | | nsSMILAnimationController::RemoveChild(nsSMILTimeContainer& aChild) |
773 | 0 | { |
774 | 0 | mChildContainerTable.RemoveEntry(&aChild); |
775 | 0 |
|
776 | 0 | if (!mPauseState && mChildContainerTable.Count() == 0) { |
777 | 0 | StopSampling(GetRefreshDriver()); |
778 | 0 | } |
779 | 0 | } |
780 | | |
781 | | // Helper method |
782 | | nsRefreshDriver* |
783 | | nsSMILAnimationController::GetRefreshDriver() |
784 | 0 | { |
785 | 0 | if (!mDocument) { |
786 | 0 | NS_ERROR("Requesting refresh driver after document has disconnected!"); |
787 | 0 | return nullptr; |
788 | 0 | } |
789 | 0 |
|
790 | 0 | nsPresContext* context = mDocument->GetPresContext(); |
791 | 0 | return context ? context->RefreshDriver() : nullptr; |
792 | 0 | } |
793 | | |
794 | | void |
795 | | nsSMILAnimationController::FlagDocumentNeedsFlush() |
796 | 0 | { |
797 | 0 | if (nsIPresShell* shell = mDocument->GetShell()) { |
798 | 0 | shell->SetNeedStyleFlush(); |
799 | 0 | } |
800 | 0 | } |