KeyframeEffect.cpp [plain text]
#include "config.h"
#include "KeyframeEffect.h"
#include "Animation.h"
#include "CSSPropertyAnimation.h"
#include "CSSStyleDeclaration.h"
#include "Element.h"
#include "RenderStyle.h"
#include "StyleProperties.h"
#include "StyleResolver.h"
#include "WillChangeData.h"
#include <wtf/UUID.h>
namespace WebCore {
using namespace JSC;
static inline CSSPropertyID IDLAttributeNameToAnimationPropertyName(String idlAttributeName)
{
if (idlAttributeName == "cssFloat")
return CSSPropertyFloat;
return CSSStyleDeclaration::getCSSPropertyIDFromJavaScriptPropertyName(idlAttributeName);
}
static inline void computeMissingKeyframeOffsets(Vector<KeyframeEffect::ProcessedKeyframe>& keyframes)
{
if (keyframes.isEmpty())
return;
if (keyframes.size() > 1 && !keyframes[0].offset)
keyframes[0].offset = 0;
if (!keyframes.last().offset)
keyframes.last().offset = 1;
size_t indexOfLastKeyframeWithNonNullOffset = 0;
for (size_t i = 1; i < keyframes.size(); ++i) {
auto keyframe = keyframes[i];
if (!keyframe.offset)
continue;
if (indexOfLastKeyframeWithNonNullOffset == i - 1)
continue;
double lastNonNullOffset = keyframes[indexOfLastKeyframeWithNonNullOffset].offset.value();
double offsetDelta = keyframe.offset.value() - lastNonNullOffset;
double offsetIncrement = offsetDelta / (i - indexOfLastKeyframeWithNonNullOffset);
size_t indexOfFirstKeyframeWithNullOffset = indexOfLastKeyframeWithNonNullOffset + 1;
for (size_t j = indexOfFirstKeyframeWithNullOffset; j < i; ++j)
keyframes[j].offset = lastNonNullOffset + (j - indexOfLastKeyframeWithNonNullOffset) * offsetIncrement;
indexOfLastKeyframeWithNonNullOffset = i;
}
}
static inline ExceptionOr<void> processIterableKeyframes(ExecState& state, Strong<JSObject>&& keyframesInput, JSValue method, Vector<KeyframeEffect::ProcessedKeyframe>& processedKeyframes)
{
VM& vm = state.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
forEachInIterable(state, keyframesInput.get(), method, [&processedKeyframes](VM& vm, ExecState& state, JSValue nextValue) -> ExceptionOr<void> {
if (!nextValue || !nextValue.isObject())
return Exception { TypeError };
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* keyframe = nextValue.toObject(&state);
PropertyNameArray ownPropertyNames(&vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
JSObject::getOwnPropertyNames(keyframe, &state, ownPropertyNames, EnumerationMode());
size_t numberOfProperties = ownPropertyNames.size();
KeyframeEffect::ProcessedKeyframe keyframeOutput;
String easing = "linear";
std::optional<double> offset;
std::optional<CompositeOperation> composite;
for (size_t j = 0; j < numberOfProperties; ++j) {
auto ownPropertyName = ownPropertyNames[j];
auto ownPropertyRawValue = keyframe->get(&state, ownPropertyName);
if (ownPropertyName == "easing")
easing = convert<IDLDOMString>(state, ownPropertyRawValue);
else if (ownPropertyName == "offset")
offset = convert<IDLNullable<IDLDouble>>(state, ownPropertyRawValue);
else if (ownPropertyName == "composite")
composite = convert<IDLEnumeration<CompositeOperation>>(state, ownPropertyRawValue);
else {
auto cssPropertyId = IDLAttributeNameToAnimationPropertyName(ownPropertyName.string());
if (CSSPropertyAnimation::isPropertyAnimatable(cssPropertyId))
keyframeOutput.cssPropertiesAndValues.set(cssPropertyId, convert<IDLDOMString>(state, ownPropertyRawValue));
}
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
}
keyframeOutput.easing = easing;
keyframeOutput.offset = offset;
keyframeOutput.composite = composite;
processedKeyframes.append(WTFMove(keyframeOutput));
return { };
});
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
return { };
}
static inline ExceptionOr<KeyframeEffect::KeyframeLikeObject> processKeyframeLikeObject(ExecState& state, Strong<JSObject>&& keyframesInput)
{
VM& vm = state.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto baseProperties = convert<IDLDictionary<KeyframeEffect::BasePropertyIndexedKeyframe>>(state, keyframesInput.get());
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
KeyframeEffect::KeyframeLikeObject keyframeOuput;
keyframeOuput.baseProperties = baseProperties;
PropertyNameArray inputProperties(&vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
JSObject::getOwnPropertyNames(keyframesInput.get(), &state, inputProperties, EnumerationMode());
size_t numberOfProperties = inputProperties.size();
for (size_t i = 0; i < numberOfProperties; ++i) {
auto cssPropertyID = IDLAttributeNameToAnimationPropertyName(inputProperties[i].string());
if (!CSSPropertyAnimation::isPropertyAnimatable(cssPropertyID))
continue;
auto rawValue = keyframesInput->get(&state, inputProperties[i]);
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
Vector<String> propertyValues;
if (rawValue.isString())
propertyValues = { rawValue.toWTFString(&state) };
else
propertyValues = convert<IDLSequence<IDLDOMString>>(state, rawValue);
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
keyframeOuput.propertiesAndValues.append({ cssPropertyID, propertyValues });
}
std::sort(keyframeOuput.propertiesAndValues.begin(), keyframeOuput.propertiesAndValues.end(), [](auto& lhs, auto& rhs) {
return getPropertyNameString(lhs.property).utf8() < getPropertyNameString(rhs.property).utf8();
});
return { WTFMove(keyframeOuput) };
}
static inline ExceptionOr<void> processPropertyIndexedKeyframes(ExecState& state, Strong<JSObject>&& keyframesInput, Vector<KeyframeEffect::ProcessedKeyframe>& processedKeyframes)
{
auto processKeyframeLikeObjectResult = processKeyframeLikeObject(state, WTFMove(keyframesInput));
if (processKeyframeLikeObjectResult.hasException())
return processKeyframeLikeObjectResult.releaseException();
auto propertyIndexedKeyframe = processKeyframeLikeObjectResult.returnValue();
for (auto& m : propertyIndexedKeyframe.propertiesAndValues) {
auto propertyName = m.property;
auto propertyValues = m.values;
Vector<KeyframeEffect::ProcessedKeyframe> propertyKeyframes;
for (auto& v : propertyValues) {
KeyframeEffect::ProcessedKeyframe k;
k.cssPropertiesAndValues.set(propertyName, v);
propertyKeyframes.append(k);
}
computeMissingKeyframeOffsets(propertyKeyframes);
for (auto& keyframe : propertyKeyframes)
processedKeyframes.append(keyframe);
}
std::sort(processedKeyframes.begin(), processedKeyframes.end(), [](auto& lhs, auto& rhs) {
return lhs.offset.value() < rhs.offset.value();
});
size_t i = 1;
while (i < processedKeyframes.size()) {
auto& keyframe = processedKeyframes[i];
auto& previousKeyframe = processedKeyframes[i - 1];
if (keyframe.offset.value() != previousKeyframe.offset.value()) {
i++;
continue;
}
auto singleValueInKeyframe = keyframe.cssPropertiesAndValues.begin();
previousKeyframe.cssPropertiesAndValues.set(singleValueInKeyframe->key, singleValueInKeyframe->value);
processedKeyframes.remove(i);
}
Vector<std::optional<double>> offsets;
if (WTF::holds_alternative<Vector<std::optional<double>>>(propertyIndexedKeyframe.baseProperties.offset))
offsets = WTF::get<Vector<std::optional<double>>>(propertyIndexedKeyframe.baseProperties.offset);
else if (WTF::holds_alternative<double>(propertyIndexedKeyframe.baseProperties.offset))
offsets.append(WTF::get<double>(propertyIndexedKeyframe.baseProperties.offset));
else if (WTF::holds_alternative<std::nullptr_t>(propertyIndexedKeyframe.baseProperties.offset))
offsets.append(std::nullopt);
for (size_t i = 0; i < offsets.size() && i < processedKeyframes.size(); ++i)
processedKeyframes[i].offset = offsets[i];
Vector<String> easings;
if (WTF::holds_alternative<Vector<String>>(propertyIndexedKeyframe.baseProperties.easing))
easings = WTF::get<Vector<String>>(propertyIndexedKeyframe.baseProperties.easing);
else if (WTF::holds_alternative<String>(propertyIndexedKeyframe.baseProperties.easing))
easings.append(WTF::get<String>(propertyIndexedKeyframe.baseProperties.easing));
if (easings.isEmpty())
easings.append("linear");
if (easings.size() < processedKeyframes.size()) {
size_t initialNumberOfEasings = easings.size();
for (i = initialNumberOfEasings + 1; i < processedKeyframes.size() - 1; ++i)
easings.append(easings[i % initialNumberOfEasings]);
}
for (size_t i = 0; i < easings.size() && i < processedKeyframes.size(); ++i)
processedKeyframes[i].easing = easings[i];
Vector<CompositeOperation> compositeModes;
if (WTF::holds_alternative<Vector<CompositeOperation>>(propertyIndexedKeyframe.baseProperties.composite))
compositeModes = WTF::get<Vector<CompositeOperation>>(propertyIndexedKeyframe.baseProperties.composite);
else if (WTF::holds_alternative<CompositeOperation>(propertyIndexedKeyframe.baseProperties.composite))
compositeModes.append(WTF::get<CompositeOperation>(propertyIndexedKeyframe.baseProperties.composite));
if (!compositeModes.isEmpty()) {
if (compositeModes.size() < processedKeyframes.size()) {
size_t initialNumberOfCompositeModes = compositeModes.size();
for (i = initialNumberOfCompositeModes + 1; i < processedKeyframes.size() - 1; ++i)
compositeModes.append(compositeModes[i % initialNumberOfCompositeModes]);
}
for (size_t i = 0; i < compositeModes.size() && i < processedKeyframes.size(); ++i)
processedKeyframes[i].composite = compositeModes[i];
}
return { };
}
ExceptionOr<Ref<KeyframeEffect>> KeyframeEffect::create(ExecState& state, Element* target, Strong<JSObject>&& keyframes, std::optional<Variant<double, KeyframeEffectOptions>>&& options)
{
auto keyframeEffect = adoptRef(*new KeyframeEffect(target));
auto setKeyframesResult = keyframeEffect->setKeyframes(state, WTFMove(keyframes));
if (setKeyframesResult.hasException())
return setKeyframesResult.releaseException();
if (options) {
auto optionsValue = options.value();
Variant<double, String> bindingsDuration;
if (WTF::holds_alternative<double>(optionsValue))
bindingsDuration = WTF::get<double>(optionsValue);
else
bindingsDuration = WTF::get<KeyframeEffectOptions>(optionsValue).duration;
auto setBindingsDurationResult = keyframeEffect->timing()->setBindingsDuration(WTFMove(bindingsDuration));
if (setBindingsDurationResult.hasException())
return setBindingsDurationResult.releaseException();
}
return WTFMove(keyframeEffect);
}
KeyframeEffect::KeyframeEffect(Element* target)
: AnimationEffect(KeyframeEffectClass)
, m_target(target)
, m_keyframes(emptyString())
{
}
ExceptionOr<void> KeyframeEffect::setKeyframes(ExecState& state, Strong<JSObject>&& keyframesInput)
{
return processKeyframes(state, WTFMove(keyframesInput));
}
ExceptionOr<void> KeyframeEffect::processKeyframes(ExecState& state, Strong<JSObject>&& keyframesInput)
{
if (!m_target || !keyframesInput.get())
return { };
VM& vm = state.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Vector<ProcessedKeyframe> processedKeyframes;
auto method = keyframesInput.get()->get(&state, vm.propertyNames->iteratorSymbol);
RETURN_IF_EXCEPTION(scope, Exception { TypeError });
if (!method.isUndefined())
processIterableKeyframes(state, WTFMove(keyframesInput), WTFMove(method), processedKeyframes);
else
processPropertyIndexedKeyframes(state, WTFMove(keyframesInput), processedKeyframes);
if (!processedKeyframes.size())
return { };
double lastNonNullOffset = -1;
for (auto& keyframe : processedKeyframes) {
if (!keyframe.offset)
continue;
auto offset = keyframe.offset.value();
if (offset <= lastNonNullOffset || offset < 0 || offset > 1)
return Exception { TypeError };
lastNonNullOffset = offset;
}
computeMissingKeyframeOffsets(processedKeyframes);
KeyframeList keyframeList("keyframe-effect-" + createCanonicalUUIDString());
StyleResolver& styleResolver = m_target->styleResolver();
auto parserContext = CSSParserContext(HTMLStandardMode);
for (auto& keyframe : processedKeyframes) {
StringBuilder cssText;
for (auto it = keyframe.cssPropertiesAndValues.begin(), end = keyframe.cssPropertiesAndValues.end(); it != end; ++it) {
cssText.append(getPropertyNameString(it->key));
cssText.appendLiteral(": ");
cssText.append(it->value);
cssText.appendLiteral("; ");
}
KeyframeValue keyframeValue(keyframe.offset.value(), nullptr);
auto renderStyle = RenderStyle::createPtr();
auto styleProperties = MutableStyleProperties::create();
styleProperties->parseDeclaration(cssText.toString(), parserContext);
unsigned numberOfCSSProperties = styleProperties->propertyCount();
for (unsigned i = 0; i < numberOfCSSProperties; ++i) {
auto cssPropertyId = styleProperties->propertyAt(i).id();
keyframeValue.addProperty(cssPropertyId);
keyframeList.addProperty(cssPropertyId);
styleResolver.applyPropertyToStyle(cssPropertyId, styleProperties->propertyAt(i).value(), WTFMove(renderStyle));
renderStyle = styleResolver.state().takeStyle();
}
keyframeValue.setStyle(RenderStyle::clonePtr(*renderStyle));
keyframeList.insert(WTFMove(keyframeValue));
}
m_keyframes = WTFMove(keyframeList);
computeStackingContextImpact();
return { };
}
void KeyframeEffect::computeStackingContextImpact()
{
m_triggersStackingContext = false;
for (auto cssPropertyId : m_keyframes.properties()) {
if (WillChangeData::propertyCreatesStackingContext(cssPropertyId)) {
m_triggersStackingContext = true;
break;
}
}
}
void KeyframeEffect::applyAtLocalTime(Seconds localTime, RenderStyle& targetStyle)
{
if (!m_target)
return;
if (m_startedAccelerated && localTime >= timing()->duration()) {
m_startedAccelerated = false;
animation()->acceleratedRunningStateDidChange();
}
if (localTime < 0_s || localTime >= timing()->duration())
return;
if (!timing()->duration())
return;
bool needsToStartAccelerated = false;
if (!m_started && !m_startedAccelerated) {
needsToStartAccelerated = shouldRunAccelerated();
m_startedAccelerated = needsToStartAccelerated;
if (needsToStartAccelerated)
animation()->acceleratedRunningStateDidChange();
}
m_started = true;
if (!needsToStartAccelerated && !m_startedAccelerated)
setAnimatedPropertiesInStyle(targetStyle, localTime / timing()->duration());
if (m_triggersStackingContext && targetStyle.hasAutoZIndex())
targetStyle.setZIndex(0);
}
bool KeyframeEffect::shouldRunAccelerated()
{
for (auto cssPropertyId : m_keyframes.properties()) {
if (!CSSPropertyAnimation::animationOfPropertyIsAccelerated(cssPropertyId))
return false;
}
return true;
}
void KeyframeEffect::getAnimatedStyle(std::unique_ptr<RenderStyle>& animatedStyle)
{
if (!animation() || !timing()->duration())
return;
auto localTime = animation()->currentTime();
if (!localTime || localTime < 0_s || localTime >= timing()->duration())
return;
if (!m_keyframes.size())
return;
if (!animatedStyle)
animatedStyle = RenderStyle::clonePtr(renderer()->style());
setAnimatedPropertiesInStyle(*animatedStyle.get(), localTime.value() / timing()->duration());
}
void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, double progress)
{
size_t numberOfKeyframes = m_keyframes.size();
for (auto cssPropertyId : m_keyframes.properties()) {
int startKeyframeIndex = -1;
int endKeyframeIndex = -1;
for (size_t i = 0; i < numberOfKeyframes; ++i) {
auto& keyframe = m_keyframes[i];
if (!keyframe.containsProperty(cssPropertyId))
continue;
if (keyframe.key() > progress) {
endKeyframeIndex = i;
break;
}
startKeyframeIndex = i;
}
double startOffset;
const RenderStyle* startStyle;
if (startKeyframeIndex == -1) {
startOffset = 0;
startStyle = &targetStyle;
} else {
startOffset = m_keyframes[startKeyframeIndex].key();
startStyle = m_keyframes[startKeyframeIndex].style();
}
double endOffset;
const RenderStyle* endStyle;
if (endKeyframeIndex == -1) {
endOffset = 1;
endStyle = &targetStyle;
} else {
endOffset = m_keyframes[endKeyframeIndex].key();
endStyle = m_keyframes[endKeyframeIndex].style();
}
auto progressInRange = (progress - startOffset) / (endOffset - startOffset);
CSSPropertyAnimation::blendProperties(this, cssPropertyId, &targetStyle, startStyle, endStyle, progressInRange);
}
}
void KeyframeEffect::startOrStopAccelerated()
{
auto* renderer = this->renderer();
if (!renderer || !renderer->isComposited())
return;
auto* compositedRenderer = downcast<RenderBoxModelObject>(renderer);
if (m_startedAccelerated) {
auto animation = Animation::create();
animation->setDuration(timing()->duration().value());
compositedRenderer->startAnimation(0, animation.ptr(), m_keyframes);
} else {
compositedRenderer->animationFinished(m_keyframes.animationName());
if (!m_target->document().renderTreeBeingDestroyed())
m_target->invalidateStyleAndLayerComposition();
}
}
RenderElement* KeyframeEffect::renderer() const
{
return m_target ? m_target->renderer() : nullptr;
}
const RenderStyle& KeyframeEffect::currentStyle() const
{
if (auto* renderer = this->renderer())
return renderer->style();
return RenderStyle::defaultStyle();
}
}