| /* |
| * Copyright (C) 2017-2018 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 "AnimationEffect.h" |
| |
| #include "CSSAnimation.h" |
| #include "CSSNumericFactory.h" |
| #include "CSSNumericValue.h" |
| #include "CSSParserContext.h" |
| #include "CSSPropertyParserConsumer+Easing.h" |
| #include "CommonAtomStrings.h" |
| #include "FillMode.h" |
| #include "JSComputedEffectTiming.h" |
| #include "ScriptExecutionContext.h" |
| #include "ScrollTimeline.h" |
| #include "WebAnimation.h" |
| #include "WebAnimationUtilities.h" |
| #include <wtf/TZoneMallocInlines.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(AnimationEffect); |
| |
| AnimationEffect::AnimationEffect() = default; |
| |
| AnimationEffect::~AnimationEffect() = default; |
| |
| void AnimationEffect::setAnimation(WebAnimation* animation) |
| { |
| if (m_animation == animation) |
| return; |
| |
| m_animation = animation; |
| m_timingDidMutate = true; |
| } |
| |
| EffectTiming AnimationEffect::getBindingsTiming() const |
| { |
| if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation())) |
| styleOriginatedAnimation->flushPendingStyleChanges(); |
| |
| EffectTiming timing; |
| timing.delay = secondsToWebAnimationsAPITime(m_timing.specifiedStartDelay); |
| timing.endDelay = secondsToWebAnimationsAPITime(m_timing.specifiedEndDelay); |
| timing.fill = m_timing.fill; |
| timing.iterationStart = m_timing.iterationStart; |
| timing.iterations = m_timing.iterations; |
| if (auto specifiedDuration = m_timing.specifiedIterationDuration) |
| timing.duration = secondsToWebAnimationsAPITime(*specifiedDuration); |
| else |
| timing.duration = autoAtom(); |
| timing.direction = m_timing.direction; |
| timing.easing = RefPtr { m_timing.timingFunction }->cssText(); |
| return timing; |
| } |
| |
| AnimationEffectTiming::ResolutionData AnimationEffect::resolutionData(UseCachedCurrentTime useCachedCurrentTime) const |
| { |
| if (!m_animation) |
| return { }; |
| |
| RefPtr animation = m_animation.get(); |
| RefPtr timeline = animation->timeline(); |
| return { |
| timeline ? timeline->currentTime(useCachedCurrentTime) : std::nullopt, |
| timeline ? timeline->duration() : std::nullopt, |
| animation->startTime(), |
| animation->currentTime(useCachedCurrentTime), |
| animation->playbackRate() |
| }; |
| } |
| |
| BasicEffectTiming AnimationEffect::getBasicTiming() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.getBasicTiming(resolutionData()); |
| } |
| |
| ComputedEffectTiming AnimationEffect::getBindingsComputedTiming() |
| { |
| if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation())) |
| styleOriginatedAnimation->flushPendingStyleChanges(); |
| return getComputedTiming(); |
| } |
| |
| ComputedEffectTiming AnimationEffect::getComputedTiming(UseCachedCurrentTime useCachedCurrentTime) |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| |
| auto data = resolutionData(useCachedCurrentTime); |
| auto resolvedTiming = m_timing.resolve(data); |
| |
| // https://drafts.csswg.org/web-animations-2/#dom-animationeffect-getcomputedtiming |
| // The description of the duration attribute of the object needs to indicate that if timing.duration |
| // is the string auto, this attribute will return the current calculated value of the intrinsic iteration |
| // duration, which may be a expressed as a double representing the duration in milliseconds or a percentage |
| // when the effect is associated with a progress-based timeline. |
| auto computedDuration = [&]() -> DoubleOrCSSNumericValueOrString { |
| auto& duration = m_timing.specifiedIterationDuration ? m_timing.iterationDuration : m_timing.intrinsicIterationDuration; |
| if (auto percent = duration.percentage()) |
| return CSSNumericFactory::percent(*percent); |
| ASSERT(duration.time()); |
| return secondsToWebAnimationsAPITime(*duration.time()); |
| }(); |
| |
| ComputedEffectTiming computedTiming; |
| computedTiming.delay = secondsToWebAnimationsAPITime(m_timing.specifiedStartDelay); |
| computedTiming.endDelay = secondsToWebAnimationsAPITime(m_timing.specifiedEndDelay); |
| computedTiming.fill = m_timing.fill == FillMode::Auto ? FillMode::None : m_timing.fill; |
| computedTiming.iterationStart = m_timing.iterationStart; |
| computedTiming.iterations = m_timing.iterations; |
| computedTiming.duration = computedDuration; |
| computedTiming.direction = m_timing.direction; |
| computedTiming.easing = RefPtr { m_timing.timingFunction }->cssText(); |
| computedTiming.endTime = m_timing.endTime; |
| computedTiming.activeDuration = m_timing.activeDuration; |
| computedTiming.localTime = data.localTime; |
| computedTiming.simpleIterationProgress = resolvedTiming.simpleIterationProgress; |
| computedTiming.progress = resolvedTiming.transformedProgress; |
| computedTiming.currentIteration = resolvedTiming.currentIteration; |
| computedTiming.phase = resolvedTiming.phase; |
| computedTiming.before = resolvedTiming.before; |
| return computedTiming; |
| } |
| |
| ExceptionOr<void> AnimationEffect::bindingsUpdateTiming(Document& document, std::optional<OptionalEffectTiming> timing) |
| { |
| auto retVal = updateTiming(document, timing); |
| if (!retVal.hasException() && timing) { |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) |
| cssAnimation->effectTimingWasUpdatedUsingBindings(*timing); |
| } |
| return retVal; |
| } |
| |
| ExceptionOr<void> AnimationEffect::updateTiming(Document& document, std::optional<OptionalEffectTiming> timing) |
| { |
| // 6.5.4. Updating the timing of an AnimationEffect |
| // https://drafts.csswg.org/web-animations/#updating-animationeffect-timing |
| |
| // To update the timing properties of an animation effect, effect, from an EffectTiming or OptionalEffectTiming object, input, perform the following steps: |
| if (!timing) |
| return { }; |
| |
| // 1. If the iterationStart member of input is present and less than zero, throw a TypeError and abort this procedure. |
| if (timing->iterationStart) { |
| if (timing->iterationStart.value() < 0) |
| return Exception { ExceptionCode::TypeError }; |
| } |
| |
| // 2. If the iterations member of input is present, and less than zero or is the value NaN, throw a TypeError and abort this procedure. |
| if (timing->iterations) { |
| if (timing->iterations.value() < 0 || std::isnan(timing->iterations.value())) |
| return Exception { ExceptionCode::TypeError }; |
| } |
| |
| // 3. If the duration member of input is present, and less than zero or is the value NaN, throw a TypeError and abort this procedure. |
| // FIXME: should it not throw an exception on a string other than "auto"? |
| if (timing->duration) { |
| if (std::holds_alternative<double>(timing->duration.value())) { |
| auto durationAsDouble = std::get<double>(timing->duration.value()); |
| if (durationAsDouble < 0 || std::isnan(durationAsDouble)) |
| return Exception { ExceptionCode::TypeError }; |
| } else { |
| if (std::get<String>(timing->duration.value()) != autoAtom()) |
| return Exception { ExceptionCode::TypeError }; |
| } |
| } |
| |
| if (auto iterations = timing->iterations) { |
| // https://github.com/w3c/csswg-drafts/issues/11343 |
| if (std::isinf(*iterations)) { |
| if (RefPtr animation = m_animation.get()) { |
| if (RefPtr timeline = animation->timeline()) { |
| if (timeline->isProgressBased()) |
| return Exception { ExceptionCode::TypeError, "The number of iterations cannot be set to Infinity for progress-based animations"_s }; |
| } |
| } |
| } |
| } |
| |
| // 4. If the easing member of input is present but cannot be parsed using the <timing-function> production [CSS-EASING-1], throw a TypeError and abort this procedure. |
| if (!timing->easing.isNull()) { |
| CSSParserContext parsingContext(document); |
| // FIXME: Determine the how calc() and relative units should be resolved and switch to the non-deprecated parsing function. |
| auto timingFunctionResult = CSSPropertyParserHelpers::parseEasingFunctionDeprecated(timing->easing, parsingContext); |
| if (!timingFunctionResult) |
| return Exception { ExceptionCode::TypeError }; |
| setTimingFunction(WTFMove(timingFunctionResult)); |
| } |
| |
| // 5. Assign each member present in input to the corresponding timing property of effect as follows: |
| // |
| // delay → start delay |
| // endDelay → end delay |
| // fill → fill mode |
| // iterationStart → iteration start |
| // iterations → iteration count |
| // duration → iteration duration |
| // direction → playback direction |
| // easing → timing function |
| |
| if (auto delay = timing->delay) |
| setDelay(Seconds::fromMilliseconds(*delay)); |
| |
| if (auto endDelay = timing->endDelay) |
| setEndDelay(Seconds::fromMilliseconds(*endDelay)); |
| |
| if (auto fill = timing->fill) |
| setFill(*fill); |
| |
| if (auto iterationStart = timing->iterationStart) |
| setIterationStart(*iterationStart); |
| |
| if (auto iterations = timing->iterations) |
| setIterations(*iterations); |
| |
| if (auto duration = timing->duration) { |
| if (auto* durationDouble = std::get_if<double>(&*duration)) |
| setIterationDuration(Seconds::fromMilliseconds(*durationDouble)); |
| else |
| setIterationDuration(std::nullopt); |
| } |
| |
| if (auto direction = timing->direction) |
| setDirection(*direction); |
| |
| if (RefPtr animation = m_animation.get()) |
| animation->effectTimingDidChange(); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> AnimationEffect::setIterationStart(double iterationStart) |
| { |
| // https://drafts.csswg.org/web-animations-1/#dom-animationeffecttiming-iterationstart |
| // If an attempt is made to set this attribute to a value less than zero, a TypeError must |
| // be thrown and the value of the iterationStart attribute left unchanged. |
| if (iterationStart < 0) |
| return Exception { ExceptionCode::TypeError }; |
| |
| if (m_timing.iterationStart == iterationStart) |
| return { }; |
| |
| m_timing.iterationStart = iterationStart; |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> AnimationEffect::setIterations(double iterations) |
| { |
| // https://drafts.csswg.org/web-animations-1/#dom-animationeffecttiming-iterations |
| // If an attempt is made to set this attribute to a value less than zero or a NaN value, a |
| // TypeError must be thrown and the value of the iterations attribute left unchanged. |
| if (iterations < 0 || std::isnan(iterations)) |
| return Exception { ExceptionCode::TypeError }; |
| |
| if (m_timing.iterations == iterations) |
| return { }; |
| |
| m_timing.iterations = iterations; |
| m_timingDidMutate = true; |
| |
| return { }; |
| } |
| |
| WebAnimationTime AnimationEffect::delay() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.startDelay; |
| } |
| |
| void AnimationEffect::setDelay(const Seconds& delay) |
| { |
| if (m_timing.specifiedStartDelay == delay) |
| return; |
| |
| m_timing.specifiedStartDelay = delay; |
| m_timingDidMutate = true; |
| } |
| |
| WebAnimationTime AnimationEffect::endDelay() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.endDelay; |
| } |
| |
| void AnimationEffect::setEndDelay(const Seconds& endDelay) |
| { |
| if (m_timing.specifiedEndDelay == endDelay) |
| return; |
| |
| m_timing.specifiedEndDelay = endDelay; |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::setFill(FillMode fill) |
| { |
| if (m_timing.fill == fill) |
| return; |
| |
| m_timing.fill = fill; |
| } |
| |
| WebAnimationTime AnimationEffect::iterationDuration() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.iterationDuration; |
| } |
| |
| void AnimationEffect::setIterationDuration(const std::optional<Seconds>& duration) |
| { |
| if (m_timing.specifiedIterationDuration == duration) |
| return; |
| |
| m_timing.specifiedIterationDuration = duration; |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::setDirection(PlaybackDirection direction) |
| { |
| if (m_timing.direction == direction) |
| return; |
| |
| m_timing.direction = direction; |
| } |
| |
| void AnimationEffect::setTimingFunction(const RefPtr<TimingFunction>& timingFunction) |
| { |
| m_timing.timingFunction = timingFunction; |
| } |
| |
| WebAnimationTime AnimationEffect::activeDuration() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.activeDuration; |
| } |
| |
| WebAnimationTime AnimationEffect::endTime() |
| { |
| updateComputedTimingPropertiesIfNeeded(); |
| return m_timing.endTime; |
| } |
| |
| std::optional<double> AnimationEffect::progressUntilNextStep(double iterationProgress) const |
| { |
| RefPtr stepsTimingFunction = dynamicDowncast<StepsTimingFunction>(m_timing.timingFunction); |
| if (!stepsTimingFunction) |
| return std::nullopt; |
| |
| auto numberOfSteps = stepsTimingFunction->numberOfSteps(); |
| auto nextStepProgress = ceil(iterationProgress * numberOfSteps) / numberOfSteps; |
| return nextStepProgress - iterationProgress; |
| } |
| |
| Seconds AnimationEffect::timeToNextTick(const BasicEffectTiming& timing) |
| { |
| switch (timing.phase) { |
| case AnimationEffectPhase::Before: |
| // The effect is in its "before" phase, in this case we can wait until it enters its "active" phase. |
| return delay() - *timing.localTime; |
| case AnimationEffectPhase::Active: { |
| if (!ticksContinuouslyWhileActive()) |
| return endTime() - *timing.localTime; |
| if (auto iterationProgress = getComputedTiming().simpleIterationProgress) { |
| // In case we're in a range that uses a steps() timing function, we can compute the time until the next step starts. |
| if (auto progressUntilNextStep = this->progressUntilNextStep(*iterationProgress)) |
| return iterationDuration() * *progressUntilNextStep; |
| } |
| // Other effects that continuously tick in the "active" phase will need to update their animated |
| // progress at the immediate next opportunity. |
| return 0_s; |
| } |
| case AnimationEffectPhase::After: |
| // The effect is in its after phase, which means it will no longer update its progress, so it doens't need a tick. |
| return Seconds::infinity(); |
| case AnimationEffectPhase::Idle: |
| ASSERT_NOT_REACHED(); |
| return Seconds::infinity(); |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return Seconds::infinity(); |
| } |
| |
| void AnimationEffect::animationTimelineDidChange(const AnimationTimeline*) |
| { |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::animationPlaybackRateDidChange() |
| { |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::animationProgressBasedTimelineSourceDidChangeMetrics(const TimelineRange& animationAttachmentRange) |
| { |
| if (!animationAttachmentRange.isDefault()) |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::animationRangeDidChange() |
| { |
| m_timingDidMutate = true; |
| } |
| |
| void AnimationEffect::updateComputedTimingPropertiesIfNeeded() |
| { |
| if (!m_timingDidMutate) |
| return; |
| |
| m_timingDidMutate = false; |
| |
| auto playbackRate = [&] { |
| if (m_animation) |
| return m_animation->playbackRate(); |
| return 1.0; |
| }(); |
| |
| auto rangeDuration = [&] -> std::optional<WebAnimationTime> { |
| RefPtr animation = m_animation.get(); |
| if (!animation) |
| return std::nullopt; |
| |
| RefPtr timeline = animation->timeline(); |
| if (!timeline) |
| return std::nullopt; |
| |
| if (RefPtr scrollTimeline = dynamicDowncast<ScrollTimeline>(timeline)) { |
| auto interval = scrollTimeline->intervalForAttachmentRange(animation->range()); |
| return interval.second - interval.first; |
| } |
| |
| return timeline->duration(); |
| }(); |
| |
| m_timing.updateComputedProperties(rangeDuration, playbackRate); |
| } |
| |
| } // namespace WebCore |