blob: 006f2489d7127ab1e9f957d317e036382f659a73 [file] [log] [blame]
/*
* 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