| 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 | |