| 1 | /* |
| 2 | * Copyright (C) 2018 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 "DeclarativeAnimation.h" |
| 28 | |
| 29 | #include "Animation.h" |
| 30 | #include "AnimationEvent.h" |
| 31 | #include "CSSAnimation.h" |
| 32 | #include "CSSTransition.h" |
| 33 | #include "DocumentTimeline.h" |
| 34 | #include "Element.h" |
| 35 | #include "EventNames.h" |
| 36 | #include "KeyframeEffect.h" |
| 37 | #include "PseudoElement.h" |
| 38 | #include "TransitionEvent.h" |
| 39 | #include <wtf/IsoMallocInlines.h> |
| 40 | |
| 41 | namespace WebCore { |
| 42 | |
| 43 | WTF_MAKE_ISO_ALLOCATED_IMPL(DeclarativeAnimation); |
| 44 | |
| 45 | DeclarativeAnimation::DeclarativeAnimation(Element& owningElement, const Animation& backingAnimation) |
| 46 | : WebAnimation(owningElement.document()) |
| 47 | , m_eventQueue(owningElement) |
| 48 | , m_owningElement(&owningElement) |
| 49 | , m_backingAnimation(const_cast<Animation&>(backingAnimation)) |
| 50 | { |
| 51 | } |
| 52 | |
| 53 | DeclarativeAnimation::~DeclarativeAnimation() |
| 54 | { |
| 55 | } |
| 56 | |
| 57 | void DeclarativeAnimation::tick() |
| 58 | { |
| 59 | bool wasRelevant = isRelevant(); |
| 60 | |
| 61 | WebAnimation::tick(); |
| 62 | invalidateDOMEvents(); |
| 63 | |
| 64 | // If a declarative animation transitions from a non-idle state to an idle state, it means it was |
| 65 | // canceled using the Web Animations API and it should be disassociated from its owner element. |
| 66 | // From this point on, this animation is like any other animation and should not appear in the |
| 67 | // maps containing running CSS Transitions and CSS Animations for a given element. |
| 68 | if (wasRelevant && playState() == WebAnimation::PlayState::Idle) { |
| 69 | disassociateFromOwningElement(); |
| 70 | m_eventQueue.close(); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | void DeclarativeAnimation::disassociateFromOwningElement() |
| 75 | { |
| 76 | if (!m_owningElement) |
| 77 | return; |
| 78 | |
| 79 | if (auto* animationTimeline = timeline()) |
| 80 | animationTimeline->removeDeclarativeAnimationFromListsForOwningElement(*this, *m_owningElement); |
| 81 | m_owningElement = nullptr; |
| 82 | } |
| 83 | |
| 84 | bool DeclarativeAnimation::needsTick() const |
| 85 | { |
| 86 | return WebAnimation::needsTick() || m_eventQueue.hasPendingEvents(); |
| 87 | } |
| 88 | |
| 89 | void DeclarativeAnimation::remove() |
| 90 | { |
| 91 | m_eventQueue.close(); |
| 92 | WebAnimation::remove(); |
| 93 | } |
| 94 | |
| 95 | void DeclarativeAnimation::setBackingAnimation(const Animation& backingAnimation) |
| 96 | { |
| 97 | m_backingAnimation = const_cast<Animation&>(backingAnimation); |
| 98 | syncPropertiesWithBackingAnimation(); |
| 99 | } |
| 100 | |
| 101 | void DeclarativeAnimation::initialize(const RenderStyle* oldStyle, const RenderStyle& newStyle) |
| 102 | { |
| 103 | // We need to suspend invalidation of the animation's keyframe effect during its creation |
| 104 | // as it would otherwise trigger invalidation of the document's style and this would be |
| 105 | // incorrect since it would happen during style invalidation. |
| 106 | suspendEffectInvalidation(); |
| 107 | |
| 108 | ASSERT(m_owningElement); |
| 109 | |
| 110 | setEffect(KeyframeEffect::create(*m_owningElement)); |
| 111 | setTimeline(&m_owningElement->document().timeline()); |
| 112 | downcast<KeyframeEffect>(effect())->computeDeclarativeAnimationBlendingKeyframes(oldStyle, newStyle); |
| 113 | syncPropertiesWithBackingAnimation(); |
| 114 | if (backingAnimation().playState() == AnimationPlayState::Playing) |
| 115 | play(); |
| 116 | else |
| 117 | pause(); |
| 118 | |
| 119 | unsuspendEffectInvalidation(); |
| 120 | } |
| 121 | |
| 122 | void DeclarativeAnimation::syncPropertiesWithBackingAnimation() |
| 123 | { |
| 124 | } |
| 125 | |
| 126 | Optional<double> DeclarativeAnimation::startTime() const |
| 127 | { |
| 128 | flushPendingStyleChanges(); |
| 129 | return WebAnimation::startTime(); |
| 130 | } |
| 131 | |
| 132 | void DeclarativeAnimation::setStartTime(Optional<double> startTime) |
| 133 | { |
| 134 | flushPendingStyleChanges(); |
| 135 | return WebAnimation::setStartTime(startTime); |
| 136 | } |
| 137 | |
| 138 | Optional<double> DeclarativeAnimation::bindingsCurrentTime() const |
| 139 | { |
| 140 | flushPendingStyleChanges(); |
| 141 | return WebAnimation::bindingsCurrentTime(); |
| 142 | } |
| 143 | |
| 144 | ExceptionOr<void> DeclarativeAnimation::setBindingsCurrentTime(Optional<double> currentTime) |
| 145 | { |
| 146 | flushPendingStyleChanges(); |
| 147 | return WebAnimation::setBindingsCurrentTime(currentTime); |
| 148 | } |
| 149 | |
| 150 | WebAnimation::PlayState DeclarativeAnimation::bindingsPlayState() const |
| 151 | { |
| 152 | flushPendingStyleChanges(); |
| 153 | return WebAnimation::bindingsPlayState(); |
| 154 | } |
| 155 | |
| 156 | bool DeclarativeAnimation::bindingsPending() const |
| 157 | { |
| 158 | flushPendingStyleChanges(); |
| 159 | return WebAnimation::bindingsPending(); |
| 160 | } |
| 161 | |
| 162 | WebAnimation::ReadyPromise& DeclarativeAnimation::bindingsReady() |
| 163 | { |
| 164 | flushPendingStyleChanges(); |
| 165 | return WebAnimation::bindingsReady(); |
| 166 | } |
| 167 | |
| 168 | WebAnimation::FinishedPromise& DeclarativeAnimation::bindingsFinished() |
| 169 | { |
| 170 | flushPendingStyleChanges(); |
| 171 | return WebAnimation::bindingsFinished(); |
| 172 | } |
| 173 | |
| 174 | ExceptionOr<void> DeclarativeAnimation::bindingsPlay() |
| 175 | { |
| 176 | flushPendingStyleChanges(); |
| 177 | return WebAnimation::bindingsPlay(); |
| 178 | } |
| 179 | |
| 180 | ExceptionOr<void> DeclarativeAnimation::bindingsPause() |
| 181 | { |
| 182 | flushPendingStyleChanges(); |
| 183 | return WebAnimation::bindingsPause(); |
| 184 | } |
| 185 | |
| 186 | void DeclarativeAnimation::flushPendingStyleChanges() const |
| 187 | { |
| 188 | if (auto* animationEffect = effect()) { |
| 189 | if (is<KeyframeEffect>(animationEffect)) { |
| 190 | if (auto* target = downcast<KeyframeEffect>(animationEffect)->target()) |
| 191 | target->document().updateStyleIfNeeded(); |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | void DeclarativeAnimation::setTimeline(RefPtr<AnimationTimeline>&& newTimeline) |
| 197 | { |
| 198 | if (timeline() && !newTimeline) |
| 199 | cancel(); |
| 200 | |
| 201 | WebAnimation::setTimeline(WTFMove(newTimeline)); |
| 202 | } |
| 203 | |
| 204 | void DeclarativeAnimation::cancel() |
| 205 | { |
| 206 | auto cancelationTime = 0_s; |
| 207 | if (auto* animationEffect = effect()) { |
| 208 | if (auto activeTime = animationEffect->getBasicTiming().activeTime) |
| 209 | cancelationTime = *activeTime; |
| 210 | } |
| 211 | |
| 212 | WebAnimation::cancel(); |
| 213 | |
| 214 | invalidateDOMEvents(cancelationTime); |
| 215 | } |
| 216 | |
| 217 | void DeclarativeAnimation::cancelFromStyle() |
| 218 | { |
| 219 | cancel(); |
| 220 | disassociateFromOwningElement(); |
| 221 | } |
| 222 | |
| 223 | AnimationEffectPhase DeclarativeAnimation::phaseWithoutEffect() const |
| 224 | { |
| 225 | // This shouldn't be called if we actually have an effect. |
| 226 | ASSERT(!effect()); |
| 227 | |
| 228 | auto animationCurrentTime = currentTime(); |
| 229 | if (!animationCurrentTime) |
| 230 | return AnimationEffectPhase::Idle; |
| 231 | |
| 232 | // Since we don't have an effect, the duration will be zero so the phase is 'before' if the current time is less than zero. |
| 233 | return *animationCurrentTime < 0_s ? AnimationEffectPhase::Before : AnimationEffectPhase::After; |
| 234 | } |
| 235 | |
| 236 | void DeclarativeAnimation::invalidateDOMEvents(Seconds elapsedTime) |
| 237 | { |
| 238 | if (!m_owningElement) |
| 239 | return; |
| 240 | |
| 241 | auto isPending = pending(); |
| 242 | if (isPending && m_wasPending) |
| 243 | return; |
| 244 | |
| 245 | double iteration = 0; |
| 246 | AnimationEffectPhase currentPhase; |
| 247 | Seconds intervalStart; |
| 248 | Seconds intervalEnd; |
| 249 | |
| 250 | auto* animationEffect = effect(); |
| 251 | if (animationEffect) { |
| 252 | auto timing = animationEffect->getComputedTiming(); |
| 253 | if (auto computedIteration = timing.currentIteration) |
| 254 | iteration = *computedIteration; |
| 255 | currentPhase = timing.phase; |
| 256 | intervalStart = std::max(0_s, Seconds::fromMilliseconds(std::min(-timing.delay, timing.activeDuration))); |
| 257 | intervalEnd = std::max(0_s, Seconds::fromMilliseconds(std::min(timing.endTime - timing.delay, timing.activeDuration))); |
| 258 | } else { |
| 259 | iteration = 0; |
| 260 | currentPhase = phaseWithoutEffect(); |
| 261 | intervalStart = 0_s; |
| 262 | intervalEnd = 0_s; |
| 263 | } |
| 264 | |
| 265 | bool wasActive = m_previousPhase == AnimationEffectPhase::Active; |
| 266 | bool wasAfter = m_previousPhase == AnimationEffectPhase::After; |
| 267 | bool wasBefore = m_previousPhase == AnimationEffectPhase::Before; |
| 268 | bool wasIdle = m_previousPhase == AnimationEffectPhase::Idle; |
| 269 | |
| 270 | bool isActive = currentPhase == AnimationEffectPhase::Active; |
| 271 | bool isAfter = currentPhase == AnimationEffectPhase::After; |
| 272 | bool isBefore = currentPhase == AnimationEffectPhase::Before; |
| 273 | bool isIdle = currentPhase == AnimationEffectPhase::Idle; |
| 274 | |
| 275 | if (is<CSSAnimation>(this)) { |
| 276 | // https://drafts.csswg.org/css-animations-2/#events |
| 277 | if ((wasIdle || wasBefore) && isActive) |
| 278 | enqueueDOMEvent(eventNames().animationstartEvent, intervalStart); |
| 279 | else if ((wasIdle || wasBefore) && isAfter) { |
| 280 | enqueueDOMEvent(eventNames().animationstartEvent, intervalStart); |
| 281 | enqueueDOMEvent(eventNames().animationendEvent, intervalEnd); |
| 282 | } else if (wasActive && isBefore) |
| 283 | enqueueDOMEvent(eventNames().animationendEvent, intervalStart); |
| 284 | else if (wasActive && isActive && m_previousIteration != iteration) { |
| 285 | auto iterationBoundary = iteration; |
| 286 | if (m_previousIteration > iteration) |
| 287 | iterationBoundary++; |
| 288 | auto elapsedTime = animationEffect ? animationEffect->iterationDuration() * (iterationBoundary - animationEffect->iterationStart()) : 0_s; |
| 289 | enqueueDOMEvent(eventNames().animationiterationEvent, elapsedTime); |
| 290 | } else if (wasActive && isAfter) |
| 291 | enqueueDOMEvent(eventNames().animationendEvent, intervalEnd); |
| 292 | else if (wasAfter && isActive) |
| 293 | enqueueDOMEvent(eventNames().animationstartEvent, intervalEnd); |
| 294 | else if (wasAfter && isBefore) { |
| 295 | enqueueDOMEvent(eventNames().animationstartEvent, intervalEnd); |
| 296 | enqueueDOMEvent(eventNames().animationendEvent, intervalStart); |
| 297 | } else if ((!wasIdle && !wasAfter) && isIdle) |
| 298 | enqueueDOMEvent(eventNames().animationcancelEvent, elapsedTime); |
| 299 | } else if (is<CSSTransition>(this)) { |
| 300 | // https://drafts.csswg.org/css-transitions-2/#transition-events |
| 301 | if (wasIdle && (isPending || isBefore)) |
| 302 | enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart); |
| 303 | else if (wasIdle && isActive) { |
| 304 | enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart); |
| 305 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart); |
| 306 | } else if (wasIdle && isAfter) { |
| 307 | enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart); |
| 308 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart); |
| 309 | enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd); |
| 310 | } else if ((m_wasPending || wasBefore) && isActive) |
| 311 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart); |
| 312 | else if ((m_wasPending || wasBefore) && isAfter) { |
| 313 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart); |
| 314 | enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd); |
| 315 | } else if (wasActive && isAfter) |
| 316 | enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd); |
| 317 | else if (wasActive && isBefore) |
| 318 | enqueueDOMEvent(eventNames().transitionendEvent, intervalStart); |
| 319 | else if (wasAfter && isActive) |
| 320 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalEnd); |
| 321 | else if (wasAfter && isBefore) { |
| 322 | enqueueDOMEvent(eventNames().transitionstartEvent, intervalEnd); |
| 323 | enqueueDOMEvent(eventNames().transitionendEvent, intervalStart); |
| 324 | } else if ((!wasIdle && !wasAfter) && isIdle) |
| 325 | enqueueDOMEvent(eventNames().transitioncancelEvent, elapsedTime); |
| 326 | } |
| 327 | |
| 328 | m_wasPending = isPending; |
| 329 | m_previousPhase = currentPhase; |
| 330 | m_previousIteration = iteration; |
| 331 | } |
| 332 | |
| 333 | void DeclarativeAnimation::enqueueDOMEvent(const AtomicString& eventType, Seconds elapsedTime) |
| 334 | { |
| 335 | ASSERT(m_owningElement); |
| 336 | auto time = secondsToWebAnimationsAPITime(elapsedTime) / 1000; |
| 337 | if (is<CSSAnimation>(this)) |
| 338 | m_eventQueue.enqueueEvent(AnimationEvent::create(eventType, downcast<CSSAnimation>(this)->animationName(), time)); |
| 339 | else if (is<CSSTransition>(this)) |
| 340 | m_eventQueue.enqueueEvent(TransitionEvent::create(eventType, downcast<CSSTransition>(this)->transitionProperty(), time, PseudoElement::pseudoElementNameForEvents(m_owningElement->pseudoId()))); |
| 341 | } |
| 342 | |
| 343 | void DeclarativeAnimation::stop() |
| 344 | { |
| 345 | m_eventQueue.close(); |
| 346 | WebAnimation::stop(); |
| 347 | } |
| 348 | |
| 349 | void DeclarativeAnimation::suspend(ReasonForSuspension reason) |
| 350 | { |
| 351 | m_eventQueue.suspend(); |
| 352 | WebAnimation::suspend(reason); |
| 353 | } |
| 354 | |
| 355 | void DeclarativeAnimation::resume() |
| 356 | { |
| 357 | m_eventQueue.resume(); |
| 358 | WebAnimation::resume(); |
| 359 | } |
| 360 | |
| 361 | } // namespace WebCore |
| 362 | |