blob: 0b1d435730d9cbdb7f6c24e716cca14650f84f77 [file] [log] [blame]
/*
* Copyright (C) 2018-2025 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "StyleOriginatedAnimation.h"
#include "Animation.h"
#include "CSSAnimation.h"
#include "CSSTransition.h"
#include "DocumentTimeline.h"
#include "Element.h"
#include "EventNames.h"
#include "EventTargetInlines.h"
#include "KeyframeEffect.h"
#include "Logging.h"
#include "RenderStyle.h"
#include "StyleOriginatedAnimationEvent.h"
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/TextStream.h>
namespace WebCore {
WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(StyleOriginatedAnimation);
StyleOriginatedAnimation::StyleOriginatedAnimation(const Styleable& styleable, const Animation& backingAnimation)
: WebAnimation(styleable.element.document())
, m_owningElement(styleable.element)
, m_owningPseudoElementIdentifier(styleable.pseudoElementIdentifier)
, m_backingAnimation(const_cast<Animation&>(backingAnimation))
{
}
StyleOriginatedAnimation::~StyleOriginatedAnimation() = default;
const std::optional<const Styleable> StyleOriginatedAnimation::owningElement() const
{
if (m_owningElement)
return Styleable(*m_owningElement, m_owningPseudoElementIdentifier);
return std::nullopt;
}
void StyleOriginatedAnimation::tick()
{
LOG_WITH_STREAM(Animations, stream << "StyleOriginatedAnimation::tick for element " << m_owningElement);
bool wasRelevant = isRelevant();
WebAnimation::tick();
invalidateDOMEvents();
// If a style-originated animation transitions from a non-idle state to an idle state, it means it was
// canceled using the Web Animations API and it should be disassociated from its owner element.
// From this point on, this animation is like any other animation and should not appear in the
// maps containing running CSS Transitions and CSS Animations for a given element.
if (wasRelevant && playState() == WebAnimation::PlayState::Idle)
disassociateFromOwningElement();
}
bool StyleOriginatedAnimation::canHaveGlobalPosition()
{
// https://drafts.csswg.org/css-animations-2/#animation-composite-order
// https://drafts.csswg.org/css-transitions-2/#animation-composite-order
// CSS Animations and CSS Transitions generated using the markup defined in this specification are not added
// to the global animation list when they are created. Instead, these animations are appended to the global
// animation list at the first moment when they transition out of the idle play state after being disassociated
// from their owning element.
return !m_owningElement && playState() != WebAnimation::PlayState::Idle;
}
void StyleOriginatedAnimation::disassociateFromOwningElement()
{
if (!m_owningElement)
return;
owningElement()->removeStyleOriginatedAnimationFromListsForOwningElement(*this);
m_owningElement = nullptr;
}
void StyleOriginatedAnimation::setBackingAnimation(const Animation& backingAnimation)
{
m_backingAnimation = const_cast<Animation&>(backingAnimation);
syncPropertiesWithBackingAnimation();
}
void StyleOriginatedAnimation::initialize(const RenderStyle* oldStyle, const RenderStyle& newStyle, const Style::ResolutionContext& resolutionContext)
{
WebAnimation::initialize();
// We need to suspend invalidation of the animation's keyframe effect during its creation
// as it would otherwise trigger invalidation of the document's style and this would be
// incorrect since it would happen during style invalidation.
suspendEffectInvalidation();
ASSERT(m_owningElement);
Ref effect = KeyframeEffect::create(Ref { *m_owningElement }, m_owningPseudoElementIdentifier);
setEffect(effect.copyRef());
setTimeline(&m_owningElement->document().timeline());
effect->computeStyleOriginatedAnimationBlendingKeyframes(oldStyle, newStyle, resolutionContext);
syncPropertiesWithBackingAnimation();
if (backingAnimation().playState() == AnimationPlayState::Playing)
play();
else
pause();
unsuspendEffectInvalidation();
}
void StyleOriginatedAnimation::syncPropertiesWithBackingAnimation()
{
}
std::optional<WebAnimationTime> StyleOriginatedAnimation::bindingsStartTime() const
{
flushPendingStyleChanges();
return WebAnimation::bindingsStartTime();
}
std::optional<WebAnimationTime> StyleOriginatedAnimation::bindingsCurrentTime() const
{
flushPendingStyleChanges();
return WebAnimation::bindingsCurrentTime();
}
WebAnimation::PlayState StyleOriginatedAnimation::bindingsPlayState() const
{
flushPendingStyleChanges();
return WebAnimation::bindingsPlayState();
}
WebAnimation::ReplaceState StyleOriginatedAnimation::bindingsReplaceState() const
{
flushPendingStyleChanges();
return WebAnimation::bindingsReplaceState();
}
bool StyleOriginatedAnimation::bindingsPending() const
{
flushPendingStyleChanges();
return WebAnimation::bindingsPending();
}
WebAnimation::ReadyPromise& StyleOriginatedAnimation::bindingsReady()
{
flushPendingStyleChanges();
return WebAnimation::bindingsReady();
}
WebAnimation::FinishedPromise& StyleOriginatedAnimation::bindingsFinished()
{
flushPendingStyleChanges();
return WebAnimation::bindingsFinished();
}
ExceptionOr<void> StyleOriginatedAnimation::bindingsPlay()
{
flushPendingStyleChanges();
return WebAnimation::bindingsPlay();
}
ExceptionOr<void> StyleOriginatedAnimation::bindingsPause()
{
flushPendingStyleChanges();
return WebAnimation::bindingsPause();
}
void StyleOriginatedAnimation::flushPendingStyleChanges() const
{
if (RefPtr keyframeEffect = dynamicDowncast<KeyframeEffect>(effect())) {
if (RefPtr target = keyframeEffect->target())
target->document().updateStyleIfNeeded();
}
}
void StyleOriginatedAnimation::setTimeline(RefPtr<AnimationTimeline>&& newTimeline)
{
if (timeline() && !newTimeline) {
invalidateDOMEvents([protectedThis = Ref { *this }] {
protectedThis->WebAnimation::setTimeline(nullptr);
});
} else
WebAnimation::setTimeline(WTFMove(newTimeline));
}
void StyleOriginatedAnimation::cancel(WebAnimation::Silently silently)
{
invalidateDOMEvents([protectedThis = Ref { *this }, silently] {
protectedThis->WebAnimation::cancel(silently);
});
}
void StyleOriginatedAnimation::cancelFromStyle(WebAnimation::Silently silently)
{
cancel(silently);
disassociateFromOwningElement();
}
AnimationEffectPhase StyleOriginatedAnimation::phaseWithoutEffect() const
{
// This shouldn't be called if we actually have an effect.
ASSERT(!effect());
auto animationCurrentTime = currentTime();
if (!animationCurrentTime)
return AnimationEffectPhase::Idle;
// 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.
return *animationCurrentTime < animationCurrentTime->matchingZero() ? AnimationEffectPhase::Before : AnimationEffectPhase::After;
}
WebAnimationTime StyleOriginatedAnimation::effectTimeAtStart() const
{
if (RefPtr effect = this->effect())
return effect->delay();
return 0_s;
}
WebAnimationTime StyleOriginatedAnimation::effectTimeAtIteration(double iteration) const
{
if (RefPtr effect = this->effect()) {
auto iterationDuration = effect->iterationDuration();
// We need not account for delay with progress-based animations as the
// Web Animations spec does not specify how to account for them.
if (iterationDuration.percentage())
return iterationDuration * iteration;
return effect->delay() + iterationDuration * iteration;
}
return 0_s;
}
WebAnimationTime StyleOriginatedAnimation::effectTimeAtEnd() const
{
if (RefPtr effect = this->effect())
return effect->endTime();
return 0_s;
}
template<typename F> void StyleOriginatedAnimation::invalidateDOMEvents(F&& callback)
{
WebAnimationTime cancelationTime = 0_s;
if (m_owningElement) {
if (RefPtr animationEffect = effect()) {
if (auto activeTime = animationEffect->getBasicTiming().activeTime)
cancelationTime = *activeTime;
}
}
callback();
invalidateDOMEvents(cancelationTime);
}
void StyleOriginatedAnimation::invalidateDOMEvents(WebAnimationTime cancelationTime)
{
if (!m_owningElement)
return;
auto isPending = pending();
if (isPending && m_wasPending)
return;
double iteration = 0;
AnimationEffectPhase currentPhase;
WebAnimationTime intervalStart;
WebAnimationTime intervalEnd;
RefPtr animationEffect = effect();
if (animationEffect) {
auto timing = animationEffect->getComputedTiming();
if (auto computedIteration = timing.currentIteration)
iteration = *computedIteration;
currentPhase = timing.phase;
if (timing.activeDuration.percentage()) {
// We need not account for delay with progress-based animations as the
// Web Animations spec does not specify how to account for them.
auto zero = timing.activeDuration.matchingZero();
intervalStart = std::max(zero, timing.activeDuration);
intervalEnd = std::max(zero, std::min(timing.endTime, timing.activeDuration));
} else {
auto activeDuration = timing.activeDuration.time()->milliseconds();
intervalStart = std::max(0_s, Seconds::fromMilliseconds(std::min(-timing.delay, activeDuration)));
intervalEnd = std::max(0_s, Seconds::fromMilliseconds(std::min(timing.endTime.time()->milliseconds() - timing.delay, activeDuration)));
}
} else {
iteration = 0;
currentPhase = phaseWithoutEffect();
intervalStart = 0_s;
intervalEnd = 0_s;
}
bool wasActive = m_previousPhase == AnimationEffectPhase::Active;
bool wasAfter = m_previousPhase == AnimationEffectPhase::After;
bool wasBefore = m_previousPhase == AnimationEffectPhase::Before;
bool wasIdle = m_previousPhase == AnimationEffectPhase::Idle;
bool isActive = currentPhase == AnimationEffectPhase::Active;
bool isAfter = currentPhase == AnimationEffectPhase::After;
bool isBefore = currentPhase == AnimationEffectPhase::Before;
bool isIdle = currentPhase == AnimationEffectPhase::Idle;
if (isCSSAnimation()) {
// https://drafts.csswg.org/css-animations-2/#events
if ((wasIdle || wasBefore) && isActive)
enqueueDOMEvent(eventNames().animationstartEvent, intervalStart, effectTimeAtStart());
else if ((wasIdle || wasBefore) && isAfter) {
enqueueDOMEvent(eventNames().animationstartEvent, intervalStart, effectTimeAtStart());
enqueueDOMEvent(eventNames().animationendEvent, intervalEnd, effectTimeAtEnd());
} else if (wasActive && isBefore)
enqueueDOMEvent(eventNames().animationendEvent, intervalStart, effectTimeAtEnd());
else if (wasActive && isActive && m_previousIteration != iteration) {
auto iterationBoundary = iteration;
if (m_previousIteration > iteration)
iterationBoundary++;
auto elapsedTime = animationEffect ? animationEffect->iterationDuration() * (iterationBoundary - animationEffect->iterationStart()) : zeroTime();
enqueueDOMEvent(eventNames().animationiterationEvent, elapsedTime, effectTimeAtIteration(iteration));
} else if (wasActive && isAfter)
enqueueDOMEvent(eventNames().animationendEvent, intervalEnd, effectTimeAtEnd());
else if (wasAfter && isActive)
enqueueDOMEvent(eventNames().animationstartEvent, intervalEnd, effectTimeAtStart());
else if (wasAfter && isBefore) {
enqueueDOMEvent(eventNames().animationstartEvent, intervalEnd, effectTimeAtStart());
enqueueDOMEvent(eventNames().animationendEvent, intervalStart, effectTimeAtEnd());
} else if ((!wasIdle && !wasAfter) && isIdle)
enqueueDOMEvent(eventNames().animationcancelEvent, cancelationTime, cancelationTime);
} else if (isCSSTransition()) {
// https://drafts.csswg.org/css-transitions-2/#transition-events
if (wasIdle && (isPending || isBefore))
enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart, effectTimeAtStart());
else if (wasIdle && isActive) {
auto scheduledEffectTime = effectTimeAtStart();
enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart, scheduledEffectTime);
enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart, scheduledEffectTime);
} else if (wasIdle && isAfter) {
enqueueDOMEvent(eventNames().transitionrunEvent, intervalStart, effectTimeAtStart());
enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart, effectTimeAtStart());
enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd, effectTimeAtEnd());
} else if ((m_wasPending || wasBefore) && isActive)
enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart, effectTimeAtStart());
else if ((m_wasPending || wasBefore) && isAfter) {
enqueueDOMEvent(eventNames().transitionstartEvent, intervalStart, effectTimeAtStart());
enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd, effectTimeAtEnd());
} else if (wasActive && isAfter)
enqueueDOMEvent(eventNames().transitionendEvent, intervalEnd, effectTimeAtEnd());
else if (wasActive && isBefore)
enqueueDOMEvent(eventNames().transitionendEvent, intervalStart, effectTimeAtEnd());
else if (wasAfter && isActive)
enqueueDOMEvent(eventNames().transitionstartEvent, intervalEnd, effectTimeAtStart());
else if (wasAfter && isBefore) {
enqueueDOMEvent(eventNames().transitionstartEvent, intervalEnd, effectTimeAtStart());
enqueueDOMEvent(eventNames().transitionendEvent, intervalStart, effectTimeAtEnd());
} else if ((!wasIdle && !wasAfter) && isIdle)
enqueueDOMEvent(eventNames().transitioncancelEvent, cancelationTime, cancelationTime);
}
m_wasPending = isPending;
m_previousPhase = currentPhase;
m_previousIteration = iteration;
}
void StyleOriginatedAnimation::enqueueDOMEvent(const AtomString& eventType, WebAnimationTime elapsedTime, WebAnimationTime scheduledEffectTime)
{
if (!m_owningElement)
return;
auto scheduledTimelineTime = [&]() -> std::optional<Seconds> {
if (RefPtr documentTimeline = dynamicDowncast<DocumentTimeline>(timeline())) {
ASSERT(scheduledEffectTime.time());
if (auto scheduledAnimationTime = convertAnimationTimeToTimelineTime(*scheduledEffectTime.time()))
return documentTimeline->convertTimelineTimeToOriginRelativeTime(*scheduledAnimationTime);
}
return std::nullopt;
}();
auto time = [&]() {
if (auto seconds = elapsedTime.time())
return secondsToWebAnimationsAPITime(*seconds) / 1000;
return 0.0;
};
auto event = createEvent(eventType, scheduledTimelineTime, time(), m_owningPseudoElementIdentifier);
event->setTarget(RefPtr { m_owningElement.get() });
enqueueAnimationEvent(WTFMove(event));
}
} // namespace WebCore