blob: 7f6bf48b1002688d26679a2b6fc369f942582a4e [file] [log] [blame]
/*
* Copyright (C) 2023-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 "KeyframeInterpolation.h"
namespace WebCore {
const KeyframeInterpolation::KeyframeInterval KeyframeInterpolation::interpolationKeyframes(Property property, double iterationProgress, const Keyframe& defaultStartKeyframe, const Keyframe& defaultEndKeyframe) const
{
// 1. If iteration progress is unresolved abort this procedure.
// 2. Let target property be the longhand property for which the effect value is to be calculated.
// 3. If animation type of the target property is not animatable abort this procedure since the effect cannot be applied.
// 4. Define the neutral value for composition as a value which, when combined with an underlying value using the add composite operation,
// produces the underlying value.
// 5. Let property-specific keyframes be the result of getting the set of computed keyframes for this keyframe effect.
// 6. Remove any keyframes from property-specific keyframes that do not have a property value for target property.
unsigned numberOfKeyframesWithZeroOffset = 0;
unsigned numberOfKeyframesWithOneOffset = 0;
Vector<const Keyframe*> propertySpecificKeyframes;
for (size_t i = 0; i < numberOfKeyframes(); ++i) {
auto& keyframe = keyframeAtIndex(i);
if (!keyframe.hasResolvedOffset() || !keyframe.animatesProperty(property))
continue;
auto offset = keyframe.offset();
if (!offset)
numberOfKeyframesWithZeroOffset++;
if (offset == 1)
numberOfKeyframesWithOneOffset++;
propertySpecificKeyframes.append(&keyframe);
}
// 7. If property-specific keyframes is empty, return underlying value.
if (propertySpecificKeyframes.isEmpty())
return { };
auto hasImplicitZeroKeyframe = !numberOfKeyframesWithZeroOffset;
auto hasImplicitOneKeyframe = !numberOfKeyframesWithOneOffset;
// 8. If there is no keyframe in property-specific keyframes with a computed keyframe offset of 0, create a new keyframe with a computed keyframe
// offset of 0, a property value set to the neutral value for composition, and a composite operation of add, and prepend it to the beginning of
// property-specific keyframes.
if (hasImplicitZeroKeyframe) {
propertySpecificKeyframes.insert(0, &defaultStartKeyframe);
numberOfKeyframesWithZeroOffset = 1;
}
// 9. Similarly, if there is no keyframe in property-specific keyframes with a computed keyframe offset of 1, create a new keyframe with a computed
// keyframe offset of 1, a property value set to the neutral value for composition, and a composite operation of add, and append it to the end of
// property-specific keyframes.
if (hasImplicitOneKeyframe) {
propertySpecificKeyframes.append(&defaultEndKeyframe);
numberOfKeyframesWithOneOffset = 1;
}
// 10. Let interval endpoints be an empty sequence of keyframes.
Vector<const Keyframe*> intervalEndpoints;
// 11. Populate interval endpoints by following the steps from the first matching condition from below:
if (iterationProgress < 0 && numberOfKeyframesWithZeroOffset > 1) {
// If iteration progress < 0 and there is more than one keyframe in property-specific keyframes with a computed keyframe offset of 0,
// Add the first keyframe in property-specific keyframes to interval endpoints.
intervalEndpoints.append(propertySpecificKeyframes.first());
} else if (iterationProgress >= 1 && numberOfKeyframesWithOneOffset > 1) {
// If iteration progress ≥ 1 and there is more than one keyframe in property-specific keyframes with a computed keyframe offset of 1,
// Add the last keyframe in property-specific keyframes to interval endpoints.
intervalEndpoints.append(propertySpecificKeyframes.last());
} else {
// Otherwise,
// 1. Append to interval endpoints the last keyframe in property-specific keyframes whose computed keyframe offset is less than or equal
// to iteration progress and less than 1. If there is no such keyframe (because, for example, the iteration progress is negative),
// add the last keyframe whose computed keyframe offset is 0.
// 2. Append to interval endpoints the next keyframe in property-specific keyframes after the one added in the previous step.
size_t indexOfLastKeyframeWithZeroOffset = 0;
int indexOfFirstKeyframeToAddToIntervalEndpoints = -1;
for (size_t i = 0; i < propertySpecificKeyframes.size(); ++i) {
auto offset = propertySpecificKeyframes[i]->offset();
if (!offset)
indexOfLastKeyframeWithZeroOffset = i;
if (offset <= iterationProgress && offset < 1)
indexOfFirstKeyframeToAddToIntervalEndpoints = i;
else
break;
}
if (indexOfFirstKeyframeToAddToIntervalEndpoints >= 0) {
intervalEndpoints.append(propertySpecificKeyframes[indexOfFirstKeyframeToAddToIntervalEndpoints]);
intervalEndpoints.append(propertySpecificKeyframes[indexOfFirstKeyframeToAddToIntervalEndpoints + 1]);
} else {
ASSERT(indexOfLastKeyframeWithZeroOffset < propertySpecificKeyframes.size() - 1);
intervalEndpoints.append(propertySpecificKeyframes[indexOfLastKeyframeWithZeroOffset]);
intervalEndpoints.append(propertySpecificKeyframes[indexOfLastKeyframeWithZeroOffset + 1]);
}
}
return { intervalEndpoints, hasImplicitZeroKeyframe, hasImplicitOneKeyframe };
}
static double transformProgressDuration(const WebAnimationTime& duration)
{
if (auto time = duration.time())
return time->seconds();
return 1.0;
}
void KeyframeInterpolation::interpolateKeyframes(Property property, const KeyframeInterval& interval, double iterationProgress, double currentIteration, const WebAnimationTime& iterationDuration, TimingFunction::Before before, const CompositionCallback& compositionCallback, const AccumulationCallback& accumulationCallback, const InterpolationCallback& interpolationCallback, const RequiresInterpolationForAccumulativeIterationCallback& requiresInterpolationForAccumulativeIterationCallback) const
{
auto& intervalEndpoints = interval.endpoints;
if (intervalEndpoints.isEmpty())
return;
auto& startKeyframe = *intervalEndpoints.first();
auto& endKeyframe = *intervalEndpoints.last();
auto usedInterpolationForAccumulativeIteration = false;
// 12. For each keyframe in interval endpoints:
// If keyframe has a composite operation that is not replace, or keyframe has no composite operation and the
// composite operation of this keyframe effect is not replace, then perform the following steps:
// 1. Let composite operation to use be the composite operation of keyframe, or if it has none, the composite
// operation of this keyframe effect.
// 2. Let value to combine be the property value of target property specified on keyframe.
// 3. Replace the property value of target property on keyframe with the result of combining underlying value
// (Va) and value to combine (Vb) using the procedure for the composite operation to use corresponding to the
// target property’s animation type.
if (isPropertyAdditiveOrCumulative(property)) {
// Only do this for the 0 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
// for composition" which really means we don't want to do anything but rather just use the underlying style which
// is already set on startKeyframe.
if (startKeyframe.offset() || !interval.hasImplicitZeroKeyframe) {
auto startKeyframeCompositeOperation = startKeyframe.compositeOperation().value_or(compositeOperation());
if (startKeyframeCompositeOperation != CompositeOperation::Replace)
compositionCallback(startKeyframe, startKeyframeCompositeOperation);
}
// Only do this for the 1 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
// for composition" which really means we don't want to do anything but rather just use the underlying style which
// is already set on endKeyframe.
if (endKeyframe.offset() != 1 || !interval.hasImplicitOneKeyframe) {
auto endKeyframeCompositeOperation = endKeyframe.compositeOperation().value_or(compositeOperation());
if (endKeyframeCompositeOperation != CompositeOperation::Replace)
compositionCallback(endKeyframe, endKeyframeCompositeOperation);
}
// If this keyframe effect has an iteration composite operation of accumulate,
if (iterationCompositeOperation() == IterationCompositeOperation::Accumulate && currentIteration && requiresInterpolationForAccumulativeIterationCallback()) {
usedInterpolationForAccumulativeIteration = true;
// apply the following step current iteration times:
for (auto i = 0; i < currentIteration; ++i) {
// replace the property value of target property on keyframe with the result of combining the
// property value on the final keyframe in property-specific keyframes (Va) with the property
// value on keyframe (Vb) using the accumulation procedure defined for target property.
if (!startKeyframe.offset() && !interval.hasImplicitZeroKeyframe)
accumulationCallback(startKeyframe);
if (endKeyframe.offset() == 1 && !interval.hasImplicitOneKeyframe)
accumulationCallback(endKeyframe);
}
}
}
// 13. If there is only one keyframe in interval endpoints return the property value of target property on that keyframe.
if (intervalEndpoints.size() == 1) {
interpolationCallback(0, 1, IterationCompositeOperation::Replace);
return;
}
// 14. Let start offset be the computed keyframe offset of the first keyframe in interval endpoints.
auto startOffset = startKeyframe.offset();
// 15. Let end offset be the computed keyframe offset of last keyframe in interval endpoints.
auto endOffset = endKeyframe.offset();
// 16. Let interval distance be the result of evaluating (iteration progress - start offset) / (end offset - start offset).
auto intervalDistance = (iterationProgress - startOffset) / (endOffset - startOffset);
// 17. Let transformed distance be the result of evaluating the timing function associated with the first keyframe in interval endpoints
// passing interval distance as the input progress.
auto transformedDistance = intervalDistance;
if (!iterationDuration.isZero()) {
auto rangeDuration = (endOffset - startOffset) * transformProgressDuration(iterationDuration);
if (RefPtr timingFunction = timingFunctionForKeyframe(startKeyframe))
transformedDistance = timingFunction->transformProgress(intervalDistance, rangeDuration, before);
}
// 18. Return the result of applying the interpolation procedure defined by the animation type of the target property, to the values of the target
// property specified on the two keyframes in interval endpoints taking the first such value as Vstart and the second as Vend and using transformed
// distance as the interpolation parameter p.
auto iterationCompositeOperation = usedInterpolationForAccumulativeIteration ? IterationCompositeOperation::Replace : this->iterationCompositeOperation();
currentIteration = usedInterpolationForAccumulativeIteration ? 0 : currentIteration;
interpolationCallback(transformedDistance, currentIteration, iterationCompositeOperation);
}
} // namespace WebCore