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
36namespace WebCore {
37
38static const Seconds SMILAnimationFrameDelay { 1_s / 60. };
39static const Seconds SMILAnimationFrameThrottledDelay { 1_s / 30. };
40
41SMILTimeContainer::SMILTimeContainer(SVGSVGElement& owner)
42 : m_timer(*this, &SMILTimeContainer::timerFired)
43 , m_ownerSVGElement(owner)
44{
45}
46
47SMILTimeContainer::~SMILTimeContainer()
48{
49#ifndef NDEBUG
50 ASSERT(!m_preventScheduledAnimationsChanges);
51#endif
52}
53
54void 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
76void 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
91void 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
98Seconds 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
106SMILTime 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
115bool SMILTimeContainer::isActive() const
116{
117 return !!m_beginTime && !isPaused();
118}
119
120bool SMILTimeContainer::isPaused() const
121{
122 return !!m_pauseTime;
123}
124
125bool SMILTimeContainer::isStarted() const
126{
127 return !!m_beginTime;
128}
129
130void 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
147void 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
158void SMILTimeContainer::resume()
159{
160 ASSERT(isPaused());
161
162 m_resumeTime = MonotonicTime::now();
163 m_pauseTime = MonotonicTime();
164 startTimer(elapsed(), 0);
165}
166
167void 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
201void 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
213void SMILTimeContainer::timerFired()
214{
215 ASSERT(!!m_beginTime);
216 ASSERT(!m_pauseTime);
217 updateAnimations(elapsed());
218}
219
220void 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
230struct 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
247void 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
254void 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