1 | /* |
2 | * Copyright (C) 2008 Apple Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * 1. Redistributions of source code must retain the above copyright |
8 | * notice, this list of conditions and the following disclaimer. |
9 | * 2. Redistributions in binary form must reproduce the above copyright |
10 | * notice, this list of conditions and the following disclaimer in the |
11 | * documentation and/or other materials provided with the distribution. |
12 | * |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "SMILTimeContainer.h" |
28 | |
29 | #include "Document.h" |
30 | #include "ElementIterator.h" |
31 | #include "Page.h" |
32 | #include "SVGSMILElement.h" |
33 | #include "SVGSVGElement.h" |
34 | #include "ScopedEventQueue.h" |
35 | |
36 | namespace WebCore { |
37 | |
38 | static const Seconds SMILAnimationFrameDelay { 1_s / 60. }; |
39 | static const Seconds SMILAnimationFrameThrottledDelay { 1_s / 30. }; |
40 | |
41 | SMILTimeContainer::SMILTimeContainer(SVGSVGElement& owner) |
42 | : m_timer(*this, &SMILTimeContainer::timerFired) |
43 | , m_ownerSVGElement(owner) |
44 | { |
45 | } |
46 | |
47 | SMILTimeContainer::~SMILTimeContainer() |
48 | { |
49 | #ifndef NDEBUG |
50 | ASSERT(!m_preventScheduledAnimationsChanges); |
51 | #endif |
52 | } |
53 | |
54 | void SMILTimeContainer::schedule(SVGSMILElement* animation, SVGElement* target, const QualifiedName& attributeName) |
55 | { |
56 | ASSERT(animation->timeContainer() == this); |
57 | ASSERT(target); |
58 | ASSERT(animation->hasValidAttributeName()); |
59 | |
60 | #ifndef NDEBUG |
61 | ASSERT(!m_preventScheduledAnimationsChanges); |
62 | #endif |
63 | |
64 | ElementAttributePair key(target, attributeName); |
65 | std::unique_ptr<AnimationsVector>& scheduled = m_scheduledAnimations.add(key, nullptr).iterator->value; |
66 | if (!scheduled) |
67 | scheduled = std::make_unique<AnimationsVector>(); |
68 | ASSERT(!scheduled->contains(animation)); |
69 | scheduled->append(animation); |
70 | |
71 | SMILTime nextFireTime = animation->nextProgressTime(); |
72 | if (nextFireTime.isFinite()) |
73 | notifyIntervalsChanged(); |
74 | } |
75 | |
76 | void SMILTimeContainer::unschedule(SVGSMILElement* animation, SVGElement* target, const QualifiedName& attributeName) |
77 | { |
78 | ASSERT(animation->timeContainer() == this); |
79 | |
80 | #ifndef NDEBUG |
81 | ASSERT(!m_preventScheduledAnimationsChanges); |
82 | #endif |
83 | |
84 | ElementAttributePair key(target, attributeName); |
85 | AnimationsVector* scheduled = m_scheduledAnimations.get(key); |
86 | ASSERT(scheduled); |
87 | bool removed = scheduled->removeFirst(animation); |
88 | ASSERT_UNUSED(removed, removed); |
89 | } |
90 | |
91 | void SMILTimeContainer::notifyIntervalsChanged() |
92 | { |
93 | // Schedule updateAnimations() to be called asynchronously so multiple intervals |
94 | // can change with updateAnimations() only called once at the end. |
95 | startTimer(elapsed(), 0); |
96 | } |
97 | |
98 | Seconds SMILTimeContainer::animationFrameDelay() const |
99 | { |
100 | auto* page = m_ownerSVGElement.document().page(); |
101 | if (!page) |
102 | return SMILAnimationFrameDelay; |
103 | return page->isLowPowerModeEnabled() ? SMILAnimationFrameThrottledDelay : SMILAnimationFrameDelay; |
104 | } |
105 | |
106 | SMILTime SMILTimeContainer::elapsed() const |
107 | { |
108 | if (!m_beginTime) |
109 | return 0_s; |
110 | if (isPaused()) |
111 | return m_accumulatedActiveTime; |
112 | return MonotonicTime::now() + m_accumulatedActiveTime - m_resumeTime; |
113 | } |
114 | |
115 | bool SMILTimeContainer::isActive() const |
116 | { |
117 | return !!m_beginTime && !isPaused(); |
118 | } |
119 | |
120 | bool SMILTimeContainer::isPaused() const |
121 | { |
122 | return !!m_pauseTime; |
123 | } |
124 | |
125 | bool SMILTimeContainer::isStarted() const |
126 | { |
127 | return !!m_beginTime; |
128 | } |
129 | |
130 | void SMILTimeContainer::begin() |
131 | { |
132 | ASSERT(!m_beginTime); |
133 | MonotonicTime now = MonotonicTime::now(); |
134 | |
135 | // If 'm_presetStartTime' is set, the timeline was modified via setElapsed() before the document began. |
136 | // In this case pass on 'seekToTime=true' to updateAnimations(). |
137 | m_beginTime = m_resumeTime = now - m_presetStartTime; |
138 | updateAnimations(SMILTime(m_presetStartTime), m_presetStartTime ? true : false); |
139 | m_presetStartTime = 0_s; |
140 | |
141 | if (m_pauseTime) { |
142 | m_pauseTime = now; |
143 | m_timer.stop(); |
144 | } |
145 | } |
146 | |
147 | void SMILTimeContainer::pause() |
148 | { |
149 | ASSERT(!isPaused()); |
150 | |
151 | m_pauseTime = MonotonicTime::now(); |
152 | if (m_beginTime) { |
153 | m_accumulatedActiveTime += m_pauseTime - m_resumeTime; |
154 | m_timer.stop(); |
155 | } |
156 | } |
157 | |
158 | void SMILTimeContainer::resume() |
159 | { |
160 | ASSERT(isPaused()); |
161 | |
162 | m_resumeTime = MonotonicTime::now(); |
163 | m_pauseTime = MonotonicTime(); |
164 | startTimer(elapsed(), 0); |
165 | } |
166 | |
167 | void SMILTimeContainer::setElapsed(SMILTime time) |
168 | { |
169 | // If the documment didn't begin yet, record a new start time, we'll seek to once its possible. |
170 | if (!m_beginTime) { |
171 | m_presetStartTime = Seconds(time.value()); |
172 | return; |
173 | } |
174 | |
175 | if (m_beginTime) |
176 | m_timer.stop(); |
177 | |
178 | MonotonicTime now = MonotonicTime::now(); |
179 | m_beginTime = now - Seconds { time.value() }; |
180 | |
181 | if (m_pauseTime) { |
182 | m_resumeTime = m_pauseTime = now; |
183 | m_accumulatedActiveTime = Seconds(time.value()); |
184 | } else |
185 | m_resumeTime = m_beginTime; |
186 | |
187 | #ifndef NDEBUG |
188 | m_preventScheduledAnimationsChanges = true; |
189 | #endif |
190 | for (auto& animation : m_scheduledAnimations.values()) { |
191 | for (auto& element : *animation) |
192 | element->reset(); |
193 | } |
194 | #ifndef NDEBUG |
195 | m_preventScheduledAnimationsChanges = false; |
196 | #endif |
197 | |
198 | updateAnimations(time, true); |
199 | } |
200 | |
201 | void SMILTimeContainer::startTimer(SMILTime elapsed, SMILTime fireTime, SMILTime minimumDelay) |
202 | { |
203 | if (!m_beginTime || isPaused()) |
204 | return; |
205 | |
206 | if (!fireTime.isFinite()) |
207 | return; |
208 | |
209 | SMILTime delay = std::max(fireTime - elapsed, minimumDelay); |
210 | m_timer.startOneShot(1_s * delay.value()); |
211 | } |
212 | |
213 | void SMILTimeContainer::timerFired() |
214 | { |
215 | ASSERT(!!m_beginTime); |
216 | ASSERT(!m_pauseTime); |
217 | updateAnimations(elapsed()); |
218 | } |
219 | |
220 | void SMILTimeContainer::updateDocumentOrderIndexes() |
221 | { |
222 | unsigned timingElementCount = 0; |
223 | |
224 | for (auto& smilElement : descendantsOfType<SVGSMILElement>(m_ownerSVGElement)) |
225 | smilElement.setDocumentOrderIndex(timingElementCount++); |
226 | |
227 | m_documentOrderIndexesDirty = false; |
228 | } |
229 | |
230 | struct PriorityCompare { |
231 | PriorityCompare(SMILTime elapsed) : m_elapsed(elapsed) {} |
232 | bool operator()(SVGSMILElement* a, SVGSMILElement* b) |
233 | { |
234 | // FIXME: This should also consider possible timing relations between the elements. |
235 | SMILTime aBegin = a->intervalBegin(); |
236 | SMILTime bBegin = b->intervalBegin(); |
237 | // Frozen elements need to be prioritized based on their previous interval. |
238 | aBegin = a->isFrozen() && m_elapsed < aBegin ? a->previousIntervalBegin() : aBegin; |
239 | bBegin = b->isFrozen() && m_elapsed < bBegin ? b->previousIntervalBegin() : bBegin; |
240 | if (aBegin == bBegin) |
241 | return a->documentOrderIndex() < b->documentOrderIndex(); |
242 | return aBegin < bBegin; |
243 | } |
244 | SMILTime m_elapsed; |
245 | }; |
246 | |
247 | void SMILTimeContainer::sortByPriority(Vector<SVGSMILElement*>& smilElements, SMILTime elapsed) |
248 | { |
249 | if (m_documentOrderIndexesDirty) |
250 | updateDocumentOrderIndexes(); |
251 | std::sort(smilElements.begin(), smilElements.end(), PriorityCompare(elapsed)); |
252 | } |
253 | |
254 | void SMILTimeContainer::updateAnimations(SMILTime elapsed, bool seekToTime) |
255 | { |
256 | SMILTime earliestFireTime = SMILTime::unresolved(); |
257 | |
258 | // Don't mutate the DOM while updating the animations. |
259 | EventQueueScope scope; |
260 | |
261 | #ifndef NDEBUG |
262 | // This boolean will catch any attempts to schedule/unschedule scheduledAnimations during this critical section. |
263 | // Similarly, any elements removed will unschedule themselves, so this will catch modification of animationsToApply. |
264 | m_preventScheduledAnimationsChanges = true; |
265 | #endif |
266 | |
267 | AnimationsVector animationsToApply; |
268 | for (auto& it : m_scheduledAnimations) { |
269 | AnimationsVector* scheduled = it.value.get(); |
270 | for (auto* animation : *scheduled) { |
271 | if (!animation->hasConditionsConnected()) |
272 | animation->connectConditions(); |
273 | } |
274 | } |
275 | |
276 | for (auto& it : m_scheduledAnimations) { |
277 | AnimationsVector* scheduled = it.value.get(); |
278 | |
279 | // Sort according to priority. Elements with later begin time have higher priority. |
280 | // In case of a tie, document order decides. |
281 | // FIXME: This should also consider timing relationships between the elements. Dependents |
282 | // have higher priority. |
283 | sortByPriority(*scheduled, elapsed); |
284 | |
285 | RefPtr<SVGSMILElement> resultElement; |
286 | for (auto& animation : *scheduled) { |
287 | ASSERT(animation->timeContainer() == this); |
288 | ASSERT(animation->targetElement()); |
289 | ASSERT(animation->hasValidAttributeName()); |
290 | |
291 | // Results are accumulated to the first animation that animates and contributes to a particular element/attribute pair. |
292 | if (!resultElement) { |
293 | if (!animation->hasValidAttributeType()) |
294 | continue; |
295 | resultElement = animation; |
296 | } |
297 | |
298 | // This will calculate the contribution from the animation and add it to the resultsElement. |
299 | if (!animation->progress(elapsed, resultElement.get(), seekToTime) && resultElement == animation) |
300 | resultElement = nullptr; |
301 | |
302 | SMILTime nextFireTime = animation->nextProgressTime(); |
303 | if (nextFireTime.isFinite()) |
304 | earliestFireTime = std::min(nextFireTime, earliestFireTime); |
305 | } |
306 | |
307 | if (resultElement) |
308 | animationsToApply.append(resultElement.get()); |
309 | } |
310 | |
311 | if (animationsToApply.isEmpty()) { |
312 | #ifndef NDEBUG |
313 | m_preventScheduledAnimationsChanges = false; |
314 | #endif |
315 | startTimer(elapsed, earliestFireTime, animationFrameDelay()); |
316 | return; |
317 | } |
318 | |
319 | // Apply results to target elements. |
320 | for (auto& animation : animationsToApply) |
321 | animation->applyResultsToTarget(); |
322 | |
323 | #ifndef NDEBUG |
324 | m_preventScheduledAnimationsChanges = false; |
325 | #endif |
326 | |
327 | startTimer(elapsed, earliestFireTime, animationFrameDelay()); |
328 | } |
329 | |
330 | } |
331 | |