| /* |
| * Copyright (C) 2017-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 "KeyframeEffect.h" |
| |
| #include "AnimationTimelinesController.h" |
| #include "CSSAnimation.h" |
| #include "CSSKeyframeRule.h" |
| #include "CSSNumericFactory.h" |
| #include "CSSParserContext.h" |
| #include "CSSPropertyNames.h" |
| #include "CSSPropertyParser.h" |
| #include "CSSPropertyParserConsumer+Animations.h" |
| #include "CSSPropertyParserConsumer+Easing.h" |
| #include "CSSSelector.h" |
| #include "CSSSerializationContext.h" |
| #include "CSSStyleProperties.h" |
| #include "CSSTransition.h" |
| #include "CSSUnitValue.h" |
| #include "CSSValue.h" |
| #include "CSSValueKeywords.h" |
| #include "CSSValuePool.h" |
| #include "DocumentInlines.h" |
| #include "Element.h" |
| #include "EventLoop.h" |
| #include "EventTargetInlines.h" |
| #include "FontCascade.h" |
| #include "GeometryUtilities.h" |
| #include "InspectorInstrumentation.h" |
| #include "JSCompositeOperation.h" |
| #include "JSCompositeOperationOrAuto.h" |
| #include "JSDOMConvert.h" |
| #include "JSKeyframeEffect.h" |
| #include "KeyframeEffectStack.h" |
| #include "LocalFrameView.h" |
| #include "Logging.h" |
| #include "MutableStyleProperties.h" |
| #include "PropertyAllowlist.h" |
| #include "Quirks.h" |
| #include "RenderBox.h" |
| #include "RenderBoxModelObject.h" |
| #include "RenderElement.h" |
| #include "RenderObjectInlines.h" |
| #include "RenderStyleInlines.h" |
| #include "Settings.h" |
| #include "StyleAdjuster.h" |
| #include "StyleEasingFunction.h" |
| #include "StyleExtractor.h" |
| #include "StyleInterpolation.h" |
| #include "StylePendingResources.h" |
| #include "StyleProperties.h" |
| #include "StylePropertyShorthand.h" |
| #include "StyleResolver.h" |
| #include "StyleScope.h" |
| #include "StyledElement.h" |
| #include "TimelineRangeOffset.h" |
| #include "TimingFunction.h" |
| #include "TransformOperationData.h" |
| #include "TransformOperationsSharedPrimitivesPrefix.h" |
| #include "TranslateTransformOperation.h" |
| #include "ViewTimeline.h" |
| #include <JavaScriptCore/Exception.h> |
| #include <ranges> |
| #include <wtf/TZoneMallocInlines.h> |
| #include <wtf/UUID.h> |
| #include <wtf/text/TextStream.h> |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| #include "AcceleratedEffect.h" |
| #include "AcceleratedEffectStackUpdater.h" |
| #endif |
| |
| namespace WebCore { |
| using namespace JSC; |
| |
| WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(KeyframeEffect); |
| |
| KeyframeEffect::~KeyframeEffect() |
| { |
| if (m_inTargetEffectStack) { |
| if (auto target = targetStyleable()) { |
| if (auto* keyframeEffectStack = target->keyframeEffectStack()) |
| keyframeEffectStack->removeEffect(*this); |
| } |
| } |
| |
| ASSERT(!m_inTargetEffectStack); |
| } |
| |
| KeyframeEffect::ParsedKeyframe::ParsedKeyframe() |
| : style(MutableStyleProperties::create()) |
| { |
| } |
| |
| KeyframeEffect::ParsedKeyframe::~ParsedKeyframe() = default; |
| |
| static inline void invalidateElement(const std::optional<const Styleable>& styleable) |
| { |
| if (!styleable) |
| return; |
| |
| Ref element = styleable->element; |
| if (!element->document().inStyleRecalc()) |
| element->invalidateStyleForAnimation(); |
| } |
| |
| String KeyframeEffect::CSSPropertyIDToIDLAttributeName(CSSPropertyID property) |
| { |
| // https://drafts.csswg.org/web-animations-1/#animation-property-name-to-idl-attribute-name |
| // 1. If property follows the <custom-property-name> production, return property. |
| |
| // 2. If property refers to the CSS float property, return the string "cssFloat". |
| if (property == CSSPropertyFloat) |
| return "cssFloat"_s; |
| |
| // 3. If property refers to the CSS offset property, return the string "cssOffset". |
| if (property == CSSPropertyOffset) |
| return "cssOffset"_s; |
| |
| // 4. Otherwise, return the result of applying the CSS property to IDL attribute algorithm [CSSOM] to property. |
| return nameForIDL(property); |
| } |
| |
| static inline CSSPropertyID IDLAttributeNameToAnimationPropertyName(const AtomString& idlAttributeName) |
| { |
| // https://drafts.csswg.org/web-animations-1/#idl-attribute-name-to-animation-property-name |
| // 1. If attribute conforms to the <custom-property-name> production, return attribute. |
| |
| // 2. If attribute is the string "cssFloat", then return an animation property representing the CSS float property. |
| if (idlAttributeName == "cssFloat"_s) |
| return CSSPropertyFloat; |
| |
| // 3. If attribute is the string "cssOffset", then return an animation property representing the CSS offset property. |
| if (idlAttributeName == "cssOffset"_s) |
| return CSSPropertyOffset; |
| |
| // If the attribute is the string "fontStretch" return the CSS font-width property that it aliases. |
| if (idlAttributeName == "fontStretch"_s) |
| return CSSPropertyFontWidth; |
| |
| // 4. Otherwise, return the result of applying the IDL attribute to CSS property algorithm [CSSOM] to attribute. |
| auto cssPropertyId = CSSStyleProperties::getCSSPropertyIDFromJavaScriptPropertyName(idlAttributeName); |
| |
| if (cssPropertyId == CSSPropertyInvalid && isCustomPropertyName(idlAttributeName)) |
| return CSSPropertyCustom; |
| |
| // We need to check that converting the property back to IDL form yields the same result such that a property passed |
| // in non-IDL form is rejected, for instance "font-size". |
| if (idlAttributeName != KeyframeEffect::CSSPropertyIDToIDLAttributeName(cssPropertyId)) |
| return CSSPropertyInvalid; |
| |
| return cssPropertyId; |
| } |
| |
| static SingleTimelineRange::Name rangeStringToSingleTimelineRangeName(const String& rangeString) |
| { |
| if (rangeString == "cover"_s) |
| return SingleTimelineRange::Name::Cover; |
| if (rangeString == "contain"_s) |
| return SingleTimelineRange::Name::Contain; |
| if (rangeString == "entry"_s) |
| return SingleTimelineRange::Name::Entry; |
| if (rangeString == "exit"_s) |
| return SingleTimelineRange::Name::Exit; |
| if (rangeString == "entry-crossing"_s) |
| return SingleTimelineRange::Name::EntryCrossing; |
| if (rangeString == "exit-crossing"_s) |
| return SingleTimelineRange::Name::ExitCrossing; |
| return SingleTimelineRange::Name::Normal; |
| } |
| |
| static bool isTimelineRangeOffsetValid(const TimelineRangeOffset& timelineRangeOffset) |
| { |
| if (rangeStringToSingleTimelineRangeName(timelineRangeOffset.rangeName) == SingleTimelineRange::Name::Normal) |
| return false; |
| RefPtr offsetUnitValue = dynamicDowncast<CSSUnitValue>(timelineRangeOffset.offset); |
| return offsetUnitValue && offsetUnitValue->unitEnum() == CSSUnitType::CSS_PERCENTAGE; |
| } |
| |
| static String rangeStringFromSingleTimelineRangeName(SingleTimelineRange::Name rangeName) |
| { |
| switch (rangeName) { |
| case SingleTimelineRange::Name::Normal: |
| return "normal"_s; |
| case SingleTimelineRange::Name::Omitted: |
| return "omitted"_s; |
| case SingleTimelineRange::Name::Cover: |
| return "cover"_s; |
| case SingleTimelineRange::Name::Contain: |
| return "contain"_s; |
| case SingleTimelineRange::Name::Entry: |
| return "entry"_s; |
| case SingleTimelineRange::Name::Exit: |
| return "exit"_s; |
| case SingleTimelineRange::Name::EntryCrossing: |
| return "entry-crossing"_s; |
| case SingleTimelineRange::Name::ExitCrossing: |
| return "exit-crossing"_s; |
| } |
| ASSERT_NOT_REACHED(); |
| return "normal"_s; |
| } |
| |
| static std::optional<Variant<double, TimelineRangeOffset>> doubleOrTimelineRangeOffsetFromString(const String& offsetString, const Document& document) |
| { |
| bool doubleParsingSuccess = true; |
| auto doubleValue = offsetString.toDouble(&doubleParsingSuccess); |
| if (doubleParsingSuccess) |
| return { doubleValue }; |
| |
| CSSParserContext parserContext(document); |
| auto offsets = CSSPropertyParserHelpers::parseKeyframeKeyList(offsetString, parserContext); |
| if (offsets.size() != 1) |
| return { }; |
| |
| auto [rangeCSSValueID, value] = offsets[0]; |
| auto rangeName = SingleTimelineRange::timelineName(rangeCSSValueID); |
| if (rangeName == SingleTimelineRange::Name::Normal) |
| return value; |
| |
| return { TimelineRangeOffset { rangeStringFromSingleTimelineRangeName(rangeName), CSSNumericFactory::percent(value * 100) } }; |
| } |
| |
| static std::optional<KeyframeEffect::KeyframeOffset> validateKeyframeOffset(const KeyframeEffect::KeyframeOffset& offset, const Document& document) |
| { |
| if (auto* doubleValue = std::get_if<double>(&offset)) |
| return *doubleValue; |
| |
| if (auto* timelineRangeOffset = std::get_if<TimelineRangeOffset>(&offset)) { |
| if (!isTimelineRangeOffsetValid(*timelineRangeOffset)) |
| return { }; |
| return *timelineRangeOffset; |
| } |
| |
| if (auto* stringOffset = std::get_if<String>(&offset)) { |
| auto parsedValue = doubleOrTimelineRangeOffsetFromString(*stringOffset, document); |
| if (!parsedValue) |
| return { }; |
| if (auto doubleOffset = std::get_if<double>(&*parsedValue)) |
| return *doubleOffset; |
| return std::get<TimelineRangeOffset>(*parsedValue); |
| } |
| |
| ASSERT(std::holds_alternative<std::nullptr_t>(offset)); |
| return nullptr; |
| }; |
| |
| static double computedOffset(SingleTimelineRange::Name rangeName, double offset, const ViewTimeline* viewTimeline, WebAnimation* animation) |
| { |
| if ((rangeName == SingleTimelineRange::Name::Normal || rangeName == SingleTimelineRange::Name::Omitted)) |
| return offset; |
| |
| if (!viewTimeline) |
| return std::numeric_limits<double>::quiet_NaN(); |
| |
| Ref timeline { *viewTimeline }; |
| |
| auto [namedRangeStartOffset, namedRangeEndOffset] = timeline->offsetIntervalForTimelineRangeName(rangeName); |
| auto namedRangeOffsetDelta = namedRangeEndOffset - namedRangeStartOffset; |
| auto computedOffsetWithinNamedRange = namedRangeStartOffset + offset * namedRangeOffsetDelta; |
| |
| if (!animation) |
| return computedOffsetWithinNamedRange; |
| |
| auto attachmentRange = Ref { *animation }->range(); |
| if (attachmentRange.isDefault()) |
| return computedOffsetWithinNamedRange; |
| |
| auto [attachmentRangeStartOffset, attachmentRangeEndOffset] = timeline->offsetIntervalForAttachmentRange(attachmentRange); |
| auto attachmentRangeOffsetDelta = attachmentRangeEndOffset - attachmentRangeStartOffset; |
| return (computedOffsetWithinNamedRange - attachmentRangeStartOffset) / attachmentRangeOffsetDelta; |
| } |
| |
| static inline void computeMissingKeyframeOffsets(Vector<KeyframeEffect::ParsedKeyframe>& keyframes, const ViewTimeline* viewTimeline, WebAnimation* animation) |
| { |
| // https://drafts.csswg.org/web-animations-1/#compute-missing-keyframe-offsets |
| |
| if (keyframes.isEmpty()) |
| return; |
| |
| Vector<KeyframeEffect::ParsedKeyframe*> keyframesWithDoubleOrNullOffset; |
| |
| // 1. For each keyframe, in keyframes, let the computed keyframe offset of the keyframe be equal to its keyframe offset value. |
| // In our implementation, we only set non-null values to avoid making computedOffset std::optional<double>. Instead, we'll know |
| // that a keyframe hasn't had a computed offset by checking if it has a null offset and a 0 computedOffset, since the first |
| // keyframe will already have a 0 computedOffset. |
| for (auto& keyframe : keyframes) { |
| auto& offset = keyframe.offset; |
| if (auto* timelineRangeOffset = std::get_if<TimelineRangeOffset>(&offset)) { |
| auto rangeName = rangeStringToSingleTimelineRangeName(timelineRangeOffset->rangeName); |
| RefPtr offsetUnitValue = dynamicDowncast<CSSUnitValue>(timelineRangeOffset->offset); |
| ASSERT(offsetUnitValue && offsetUnitValue->unitEnum() == CSSUnitType::CSS_PERCENTAGE); |
| keyframe.computedOffset = computedOffset(rangeName, offsetUnitValue->value() / 100, viewTimeline, animation); |
| } else { |
| keyframesWithDoubleOrNullOffset.append(&keyframe); |
| if (auto* doubleValue = std::get_if<double>(&offset)) |
| keyframe.computedOffset = *doubleValue; |
| else |
| keyframe.computedOffset = std::numeric_limits<double>::quiet_NaN(); |
| } |
| } |
| |
| if (keyframesWithDoubleOrNullOffset.isEmpty()) |
| return; |
| |
| // 2. If keyframes contains more than one keyframe and the computed keyframe offset of the first keyframe in keyframes is null, |
| // set the computed keyframe offset of the first keyframe to 0. |
| if (keyframesWithDoubleOrNullOffset.size() > 1 && std::isnan(keyframesWithDoubleOrNullOffset[0]->computedOffset)) |
| keyframesWithDoubleOrNullOffset[0]->computedOffset = 0; |
| |
| // 3. If the computed keyframe offset of the last keyframe in keyframes is null, set its computed keyframe offset to 1. |
| if (std::isnan(keyframesWithDoubleOrNullOffset.last()->computedOffset)) |
| keyframesWithDoubleOrNullOffset.last()->computedOffset = 1; |
| |
| // 4. For each pair of keyframes A and B where: |
| // - A appears before B in keyframes, and |
| // - A and B have a computed keyframe offset that is not null, and |
| // - all keyframes between A and B have a null computed keyframe offset, |
| // calculate the computed keyframe offset of each keyframe between A and B as follows: |
| // 1. Let offsetk be the computed keyframe offset of a keyframe k. |
| // 2. Let n be the number of keyframes between and including A and B minus 1. |
| // 3. Let index refer to the position of keyframe in the sequence of keyframes between A and B such that the first keyframe after A has an index of 1. |
| // 4. Set the computed keyframe offset of keyframe to offsetA + (offsetB − offsetA) × index / n. |
| |
| size_t indexOfLastKeyframeWithNonNullOffset = 0; |
| for (size_t i = 1; i < keyframesWithDoubleOrNullOffset.size(); ++i) { |
| auto& keyframe = *keyframesWithDoubleOrNullOffset[i]; |
| // Keyframes with a null offset that don't yet have a non-zero computed offset are keyframes |
| // with an offset that needs to be computed. |
| if (std::isnan(keyframe.computedOffset)) |
| continue; |
| if (indexOfLastKeyframeWithNonNullOffset != i - 1) { |
| double lastNonNullOffset = keyframesWithDoubleOrNullOffset[indexOfLastKeyframeWithNonNullOffset]->computedOffset; |
| double offsetDelta = keyframe.computedOffset - lastNonNullOffset; |
| double offsetIncrement = offsetDelta / (i - indexOfLastKeyframeWithNonNullOffset); |
| size_t indexOfFirstKeyframeWithNullOffset = indexOfLastKeyframeWithNonNullOffset + 1; |
| for (size_t j = indexOfFirstKeyframeWithNullOffset; j < i; ++j) |
| keyframesWithDoubleOrNullOffset[j]->computedOffset = lastNonNullOffset + (j - indexOfLastKeyframeWithNonNullOffset) * offsetIncrement; |
| } |
| indexOfLastKeyframeWithNonNullOffset = i; |
| } |
| } |
| |
| static inline ExceptionOr<KeyframeEffect::KeyframeLikeObject> processKeyframeLikeObject(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput, bool allowLists) |
| { |
| // https://drafts.csswg.org/web-animations-1/#process-a-keyframe-like-object |
| |
| VM& vm = lexicalGlobalObject.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| // 1. Run the procedure to convert an ECMAScript value to a dictionary type [WEBIDL] with keyframe input as the ECMAScript value as follows: |
| // |
| // If allow lists is true, use the following dictionary type: |
| // |
| // dictionary BasePropertyIndexedKeyframe { |
| // (double? or sequence<double?>) offset = []; |
| // (DOMString or sequence<DOMString>) easing = []; |
| // (CompositeOperationOrAuto or sequence<CompositeOperationOrAuto>) composite = []; |
| // }; |
| // |
| // Otherwise, use the following dictionary type: |
| // |
| // dictionary BaseKeyframe { |
| // double? offset = null; |
| // DOMString easing = "linear"; |
| // CompositeOperationOrAuto composite = "auto"; |
| // }; |
| // |
| // Store the result of this procedure as keyframe output. |
| KeyframeEffect::BasePropertyIndexedKeyframe baseProperties; |
| if (allowLists) { |
| auto basePropertiesConversionResult = convert<IDLDictionary<KeyframeEffect::BasePropertyIndexedKeyframe>>(lexicalGlobalObject, keyframesInput.get()); |
| if (basePropertiesConversionResult.hasException(scope)) [[unlikely]] |
| return Exception { ExceptionCode::TypeError }; |
| baseProperties = basePropertiesConversionResult.releaseReturnValue(); |
| |
| // Convert string offsets to TimelineRangeOffset in case we were provided with a list of offsets. |
| if (auto* offsets = std::get_if<Vector<KeyframeEffect::KeyframeOffset>>(&baseProperties.offset)) { |
| for (auto& offset : *offsets) { |
| auto* stringOffset = std::get_if<String>(&offset); |
| if (!stringOffset) |
| continue; |
| if (auto parsedValue = doubleOrTimelineRangeOffsetFromString(*stringOffset, document)) { |
| if (auto doubleOffset = std::get_if<double>(&*parsedValue)) |
| offset = *doubleOffset; |
| else |
| offset = std::get<TimelineRangeOffset>(*parsedValue); |
| } else |
| return Exception { ExceptionCode::TypeError }; |
| } |
| } |
| } else { |
| auto baseKeyframeConversionResult = convert<IDLDictionary<KeyframeEffect::BaseKeyframe>>(lexicalGlobalObject, keyframesInput.get()); |
| if (baseKeyframeConversionResult.hasException(scope)) [[unlikely]] |
| return Exception { ExceptionCode::TypeError }; |
| |
| auto baseKeyframe = baseKeyframeConversionResult.releaseReturnValue(); |
| auto* baseKeyframeOffset = &baseKeyframe.offset; |
| if (auto* doubleValue = std::get_if<double>(baseKeyframeOffset)) |
| baseProperties.offset = *doubleValue; |
| else if (auto* timelineRangeOffsetValue = std::get_if<TimelineRangeOffset>(baseKeyframeOffset)) { |
| if (!isTimelineRangeOffsetValid(*timelineRangeOffsetValue)) { |
| throwException(&lexicalGlobalObject, scope, JSC::Exception::create(vm, createTypeError(&lexicalGlobalObject))); |
| return Exception { ExceptionCode::TypeError }; |
| } |
| baseProperties.offset = *timelineRangeOffsetValue; |
| } else if (auto* stringOffset = std::get_if<String>(baseKeyframeOffset)) { |
| if (auto parsedValue = doubleOrTimelineRangeOffsetFromString(*stringOffset, document)) { |
| if (auto doubleOffset = std::get_if<double>(&*parsedValue)) |
| baseProperties.offset = *doubleOffset; |
| else |
| baseProperties.offset = std::get<TimelineRangeOffset>(*parsedValue); |
| } else { |
| throwException(&lexicalGlobalObject, scope, JSC::Exception::create(vm, createTypeError(&lexicalGlobalObject))); |
| return Exception { ExceptionCode::TypeError }; |
| } |
| } else |
| baseProperties.offset = nullptr; |
| baseProperties.easing = baseKeyframe.easing; |
| baseProperties.composite = baseKeyframe.composite; |
| } |
| |
| KeyframeEffect::KeyframeLikeObject keyframeOuput; |
| keyframeOuput.baseProperties = baseProperties; |
| |
| // 2. Build up a list of animatable properties as follows: |
| // |
| // 1. Let animatable properties be a list of property names (including shorthand properties that have longhand sub-properties |
| // that are animatable) that can be animated by the implementation. |
| // 2. Convert each property name in animatable properties to the equivalent IDL attribute by applying the animation property |
| // name to IDL attribute name algorithm. |
| |
| // 3. Let input properties be the result of calling the EnumerableOwnNames operation with keyframe input as the object. |
| PropertyNameArray inputProperties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); |
| JSObject::getOwnPropertyNames(keyframesInput.get(), &lexicalGlobalObject, inputProperties, DontEnumPropertiesMode::Exclude); |
| |
| auto isDirectionAwareShorthand = [](CSSPropertyID property) { |
| for (auto longhand : shorthandForProperty(property)) { |
| if (CSSProperty::isDirectionAwareProperty(longhand)) |
| return true; |
| } |
| return false; |
| }; |
| |
| // 4. Make up a new list animation properties that consists of all of the properties that are in both input properties and animatable |
| // properties, or which are in input properties and conform to the <custom-property-name> production. |
| Vector<JSC::Identifier> logicalShorthands; |
| Vector<JSC::Identifier> physicalShorthands; |
| Vector<JSC::Identifier> logicalLonghands; |
| Vector<JSC::Identifier> physicalLonghands; |
| for (auto& inputProperty : inputProperties) { |
| auto cssProperty = IDLAttributeNameToAnimationPropertyName(inputProperty.string()); |
| if (!isExposed(cssProperty, &document.settings())) |
| cssProperty = CSSPropertyInvalid; |
| auto resolvedCSSProperty = CSSProperty::resolveDirectionAwareProperty(cssProperty, WritingMode()); |
| if (Style::Interpolation::canInterpolate(resolvedCSSProperty)) { |
| if (isDirectionAwareShorthand(cssProperty)) |
| logicalShorthands.append(inputProperty); |
| else if (isShorthand(cssProperty)) |
| physicalShorthands.append(inputProperty); |
| else if (resolvedCSSProperty != cssProperty) |
| logicalLonghands.append(inputProperty); |
| else |
| physicalLonghands.append(inputProperty); |
| } |
| } |
| |
| // 5. Sort animation properties in ascending order by the Unicode codepoints that define each property name. |
| auto sortPropertiesInAscendingOrder = [](auto& properties) { |
| std::ranges::sort(properties, codePointCompareLessThan, &JSC::Identifier::string); |
| }; |
| sortPropertiesInAscendingOrder(logicalShorthands); |
| sortPropertiesInAscendingOrder(physicalShorthands); |
| sortPropertiesInAscendingOrder(logicalLonghands); |
| sortPropertiesInAscendingOrder(physicalLonghands); |
| |
| Vector<JSC::Identifier> animationProperties; |
| animationProperties.appendVector(logicalShorthands); |
| animationProperties.appendVector(physicalShorthands); |
| animationProperties.appendVector(logicalLonghands); |
| animationProperties.appendVector(physicalLonghands); |
| |
| // 6. For each property name in animation properties, |
| size_t numberOfAnimationProperties = animationProperties.size(); |
| for (size_t i = 0; i < numberOfAnimationProperties; ++i) { |
| // 1. Let raw value be the result of calling the [[Get]] internal method on keyframe input, with property name as the property |
| // key and keyframe input as the receiver. |
| auto rawValue = keyframesInput->get(&lexicalGlobalObject, animationProperties[i]); |
| |
| // 2. Check the completion record of raw value. |
| RETURN_IF_EXCEPTION(scope, Exception { ExceptionCode::TypeError }); |
| |
| // 3. Convert raw value to a DOMString or sequence of DOMStrings property values as follows: |
| Vector<String> propertyValues; |
| if (allowLists) { |
| // If allow lists is true, |
| // Let property values be the result of converting raw value to IDL type (DOMString or sequence<DOMString>) |
| // using the procedures defined for converting an ECMAScript value to an IDL value [WEBIDL]. |
| // If property values is a single DOMString, replace property values with a sequence of DOMStrings with the original value of property |
| // Values as the only element. |
| auto propertyValuesConversionResult = convert<IDLUnion<IDLDOMString, IDLSequence<IDLDOMString>>>(lexicalGlobalObject, rawValue); |
| if (propertyValuesConversionResult.hasException(scope)) [[unlikely]] |
| return Exception { ExceptionCode::TypeError }; |
| |
| propertyValues = WTF::switchOn(propertyValuesConversionResult.releaseReturnValue(), |
| [](String&& value) -> Vector<String> { |
| return { WTFMove(value) }; |
| }, |
| [](Vector<String>&& values) -> Vector<String> { |
| return values; |
| } |
| ); |
| } else { |
| // Otherwise, |
| // Let property values be the result of converting raw value to a DOMString using the procedure for converting an ECMAScript value to a DOMString. |
| auto propertyValuesConversionResult = convert<IDLDOMString>(lexicalGlobalObject, rawValue); |
| if (propertyValuesConversionResult.hasException(scope)) [[unlikely]] |
| return Exception { ExceptionCode::TypeError }; |
| |
| propertyValues = { propertyValuesConversionResult.releaseReturnValue() }; |
| } |
| |
| // 4. Calculate the normalized property name as the result of applying the IDL attribute name to animation property name algorithm to property name. |
| auto propertyName = animationProperties[i].string(); |
| auto cssPropertyID = IDLAttributeNameToAnimationPropertyName(propertyName); |
| ASSERT(isExposed(cssPropertyID, &document.settings())); |
| |
| // 5. Add a property to to keyframe output with normalized property name as the property name, and property values as the property value. |
| if (cssPropertyID == CSSPropertyCustom) |
| keyframeOuput.propertiesAndValues.append({ cssPropertyID, propertyName, propertyValues }); |
| else |
| keyframeOuput.propertiesAndValues.append({ cssPropertyID, emptyAtom(), propertyValues }); |
| } |
| |
| // 7. Return keyframe output. |
| return { WTFMove(keyframeOuput) }; |
| } |
| |
| static inline ExceptionOr<void> processIterableKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput, JSValue method, Vector<KeyframeEffect::ParsedKeyframe>& parsedKeyframes) |
| { |
| CSSParserContext parserContext(document); |
| |
| // 1. Let iter be GetIterator(object, method). |
| forEachInIterable(lexicalGlobalObject, keyframesInput.get(), method, [&parsedKeyframes, &document, &parserContext](VM& vm, JSGlobalObject& lexicalGlobalObject, JSValue nextValue) -> ExceptionOr<void> { |
| // Steps 2 through 5 are already implemented by forEachInIterable(). |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| // 6. If Type(nextItem) is not Undefined, Null or Object, then throw a TypeError and abort these steps. |
| if (!nextValue.isUndefinedOrNull() && !nextValue.isObject()) { |
| throwException(&lexicalGlobalObject, scope, JSC::Exception::create(vm, createTypeError(&lexicalGlobalObject))); |
| return { }; |
| } |
| |
| if (!nextValue.isObject()) { |
| parsedKeyframes.append({ }); |
| return { }; |
| } |
| |
| // 7. Append to processed keyframes the result of running the procedure to process a keyframe-like object passing nextItem |
| // as the keyframe input and with the allow lists flag set to false. |
| auto processKeyframeLikeObjectResult = processKeyframeLikeObject(lexicalGlobalObject, document, Strong<JSObject>(vm, nextValue.toObject(&lexicalGlobalObject)), false); |
| if (processKeyframeLikeObjectResult.hasException()) |
| return processKeyframeLikeObjectResult.releaseException(); |
| auto keyframeLikeObject = processKeyframeLikeObjectResult.returnValue(); |
| |
| KeyframeEffect::ParsedKeyframe keyframeOutput; |
| |
| // When calling processKeyframeLikeObject() with the "allow lists" flag set to false, the only offset |
| // alternatives we should expect are double and nullptr. |
| if (auto* doubleValue = std::get_if<double>(&keyframeLikeObject.baseProperties.offset)) |
| keyframeOutput.offset = *doubleValue; |
| else if (auto* timelineRangeOffset = std::get_if<TimelineRangeOffset>(&keyframeLikeObject.baseProperties.offset)) { |
| if (!isTimelineRangeOffsetValid(*timelineRangeOffset)) { |
| throwException(&lexicalGlobalObject, scope, JSC::Exception::create(vm, createTypeError(&lexicalGlobalObject))); |
| return Exception { ExceptionCode::TypeError }; |
| } |
| keyframeOutput.offset = *timelineRangeOffset; |
| } |
| |
| // When calling processKeyframeLikeObject() with the "allow lists" flag set to false, the only easing |
| // alternative we should expect is String. |
| ASSERT(std::holds_alternative<String>(keyframeLikeObject.baseProperties.easing)); |
| keyframeOutput.easing = std::get<String>(keyframeLikeObject.baseProperties.easing); |
| |
| // When calling processKeyframeLikeObject() with the "allow lists" flag set to false, the only composite |
| // alternatives we should expect is CompositeOperationAuto. |
| ASSERT(std::holds_alternative<CompositeOperationOrAuto>(keyframeLikeObject.baseProperties.composite)); |
| keyframeOutput.composite = std::get<CompositeOperationOrAuto>(keyframeLikeObject.baseProperties.composite); |
| |
| for (auto& propertyAndValue : keyframeLikeObject.propertiesAndValues) { |
| auto cssPropertyId = propertyAndValue.property; |
| // When calling processKeyframeLikeObject() with the "allow lists" flag set to false, |
| // there should only ever be a single value for a given property. |
| ASSERT(propertyAndValue.values.size() == 1); |
| auto stringValue = propertyAndValue.values[0]; |
| if (cssPropertyId == CSSPropertyCustom) { |
| auto customProperty = propertyAndValue.customProperty; |
| if (keyframeOutput.style->setCustomProperty(customProperty, stringValue, parserContext)) |
| keyframeOutput.customStyleStrings.set(customProperty, stringValue); |
| } else if (keyframeOutput.style->setProperty(cssPropertyId, stringValue, parserContext)) |
| keyframeOutput.styleStrings.set(cssPropertyId, stringValue); |
| } |
| |
| parsedKeyframes.append(WTFMove(keyframeOutput)); |
| |
| return { }; |
| }); |
| |
| return { }; |
| } |
| |
| static inline ExceptionOr<void> processPropertyIndexedKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput, Vector<KeyframeEffect::ParsedKeyframe>& parsedKeyframes, Vector<String>& unusedEasings) |
| { |
| // 1. Let property-indexed keyframe be the result of running the procedure to process a keyframe-like object passing object as the keyframe input. |
| auto processKeyframeLikeObjectResult = processKeyframeLikeObject(lexicalGlobalObject, document, WTFMove(keyframesInput), true); |
| if (processKeyframeLikeObjectResult.hasException()) |
| return processKeyframeLikeObjectResult.releaseException(); |
| auto propertyIndexedKeyframe = processKeyframeLikeObjectResult.returnValue(); |
| |
| CSSParserContext parserContext(document); |
| |
| // 2. For each member, m, in property-indexed keyframe, perform the following steps: |
| for (auto& m : propertyIndexedKeyframe.propertiesAndValues) { |
| // 1. Let property name be the key for m. |
| auto propertyName = m.property; |
| // 2. If property name is “composite”, or “easing”, or “offset”, skip the remaining steps in this loop and continue from the next member in property-indexed |
| // keyframe after m. |
| // We skip this test since we split those properties and the actual CSS properties that we're currently iterating over. |
| // 3. Let property values be the value for m. |
| auto propertyValues = m.values; |
| // 4. Let property keyframes be an empty sequence of keyframes. |
| Vector<KeyframeEffect::ParsedKeyframe> propertyKeyframes; |
| // 5. For each value, v, in property values perform the following steps: |
| for (auto& v : propertyValues) { |
| // 1. Let k be a new keyframe with a null keyframe offset. |
| KeyframeEffect::ParsedKeyframe k; |
| // 2. Add the property-value pair, property name → v, to k. |
| if (propertyName == CSSPropertyCustom) { |
| auto customProperty = m.customProperty; |
| if (k.style->setCustomProperty(customProperty, v, parserContext)) |
| k.customStyleStrings.set(customProperty, v); |
| } else if (k.style->setProperty(propertyName, v, parserContext)) |
| k.styleStrings.set(propertyName, v); |
| // 3. Append k to property keyframes. |
| propertyKeyframes.append(WTFMove(k)); |
| } |
| // 6. Apply the procedure to compute missing keyframe offsets to property keyframes. |
| computeMissingKeyframeOffsets(propertyKeyframes, nullptr, nullptr); |
| |
| // 7. Add keyframes in property keyframes to processed keyframes. |
| parsedKeyframes.appendVector(propertyKeyframes); |
| } |
| |
| // 3. Sort processed keyframes by the computed keyframe offset of each keyframe in increasing order. |
| std::ranges::sort(parsedKeyframes, [](auto& lhs, auto& rhs) { |
| if (!std::isnan(lhs.computedOffset) && !std::isnan(rhs.computedOffset)) |
| return lhs.computedOffset < rhs.computedOffset; |
| // This will sort nullopt values prior to other values. |
| return !std::isnan(lhs.computedOffset); |
| }); |
| |
| // 4. Merge adjacent keyframes in processed keyframes when they have equal computed keyframe offsets. |
| size_t i = 1; |
| while (i < parsedKeyframes.size()) { |
| auto& keyframe = parsedKeyframes[i]; |
| auto& previousKeyframe = parsedKeyframes[i - 1]; |
| // If the offsets of this keyframe and the previous keyframe are different, |
| // this means that the two keyframes should not be merged and we can move |
| // on to the next keyframe. |
| if (keyframe.computedOffset != previousKeyframe.computedOffset) { |
| i++; |
| continue; |
| } |
| // Otherwise, both this keyframe and the previous keyframe should be merged. |
| // Unprocessed keyframes in parsedKeyframes at this stage have at most a single |
| // property in cssPropertiesAndValues, so just set this on the previous keyframe. |
| // In case an invalid or null value was originally provided, then the property |
| // was not set and the property count is 0, in which case there is nothing to merge. |
| if (keyframe.styleStrings.size()) { |
| previousKeyframe.style->mergeAndOverrideOnConflict(keyframe.style); |
| for (auto& [property, value] : keyframe.styleStrings) |
| previousKeyframe.styleStrings.set(property, value); |
| } |
| if (keyframe.customStyleStrings.size()) { |
| previousKeyframe.style->mergeAndOverrideOnConflict(keyframe.style); |
| for (auto& [customProperty, value] : keyframe.customStyleStrings) |
| previousKeyframe.customStyleStrings.set(customProperty, value); |
| } |
| // Since we've processed this keyframe, we can remove it and keep i the same |
| // so that we process the next keyframe in the next loop iteration. |
| parsedKeyframes.removeAt(i); |
| } |
| |
| // 5. Let offsets be a sequence of nullable double values assigned based on the type of the “offset” member of the property-indexed keyframe as follows: |
| // - sequence<double?>, the value of “offset” as-is. |
| // - double?, a sequence of length one with the value of “offset” as its single item, i.e. « offset », |
| Vector<KeyframeEffect::KeyframeOffset> offsets; |
| auto* sourceOffsets = &propertyIndexedKeyframe.baseProperties.offset; |
| if (auto* vectorOfKeyframeOffsets = std::get_if<Vector<KeyframeEffect::KeyframeOffset>>(sourceOffsets)) { |
| for (auto& keyframeOffset : *vectorOfKeyframeOffsets) { |
| auto validatedOffset = validateKeyframeOffset(keyframeOffset, document); |
| if (!validatedOffset) |
| return Exception { ExceptionCode::TypeError }; |
| offsets.append(*validatedOffset); |
| } |
| } else if (auto* doubleValue = std::get_if<double>(sourceOffsets)) |
| offsets.append(*doubleValue); |
| else if (auto* timelineRangeOffset = std::get_if<TimelineRangeOffset>(sourceOffsets)) { |
| if (!isTimelineRangeOffsetValid(*timelineRangeOffset)) |
| return Exception { ExceptionCode::TypeError }; |
| offsets.append(*timelineRangeOffset); |
| } else if (auto* stringOffset = std::get_if<String>(sourceOffsets)) { |
| if (auto parsedValue = doubleOrTimelineRangeOffsetFromString(*stringOffset, document)) { |
| if (auto doubleOffset = std::get_if<double>(&*parsedValue)) |
| offsets.append(*doubleOffset); |
| else |
| offsets.append(std::get<TimelineRangeOffset>(*parsedValue)); |
| } else |
| return Exception { ExceptionCode::TypeError }; |
| } else |
| offsets.append(nullptr); |
| |
| // 6. Assign each value in offsets to the keyframe offset of the keyframe with corresponding position in property keyframes until the end of either sequence is reached. |
| for (size_t i = 0; i < offsets.size() && i < parsedKeyframes.size(); ++i) |
| parsedKeyframes[i].offset = offsets[i]; |
| |
| // 7. Let easings be a sequence of DOMString values assigned based on the type of the “easing” member of the property-indexed keyframe as follows: |
| // - sequence<DOMString>, the value of “easing” as-is. |
| // - DOMString, a sequence of length one with the value of “easing” as its single item, i.e. « easing », |
| Vector<String> easings; |
| if (std::holds_alternative<Vector<String>>(propertyIndexedKeyframe.baseProperties.easing)) |
| easings = std::get<Vector<String>>(propertyIndexedKeyframe.baseProperties.easing); |
| else if (std::holds_alternative<String>(propertyIndexedKeyframe.baseProperties.easing)) |
| easings.append(std::get<String>(propertyIndexedKeyframe.baseProperties.easing)); |
| |
| // 8. If easings is an empty sequence, let it be a sequence of length one containing the single value “linear”, i.e. « "linear" ». |
| if (easings.isEmpty()) |
| easings.append("linear"_s); |
| |
| // 9. If easings has fewer items than property keyframes, repeat the elements in easings successively starting from the beginning of the list until easings has as many |
| // items as property keyframes. |
| if (easings.size() < parsedKeyframes.size()) { |
| size_t initialNumberOfEasings = easings.size(); |
| for (i = initialNumberOfEasings; i < parsedKeyframes.size(); ++i) |
| easings.append(easings[i % initialNumberOfEasings]); |
| } |
| |
| // 10. If easings has more items than property keyframes, store the excess items as unused easings. |
| while (easings.size() > parsedKeyframes.size()) |
| unusedEasings.append(easings.takeLast()); |
| |
| // 11. Assign each value in easings to a property named “easing” on the keyframe with the corresponding position in property keyframes until the end of property keyframes |
| // is reached. |
| for (size_t i = 0; i < parsedKeyframes.size(); ++i) |
| parsedKeyframes[i].easing = easings[i]; |
| |
| // 12. If the “composite” member of the property-indexed keyframe is not an empty sequence: |
| Vector<CompositeOperationOrAuto> compositeModes; |
| if (std::holds_alternative<Vector<CompositeOperationOrAuto>>(propertyIndexedKeyframe.baseProperties.composite)) |
| compositeModes = std::get<Vector<CompositeOperationOrAuto>>(propertyIndexedKeyframe.baseProperties.composite); |
| else if (std::holds_alternative<CompositeOperationOrAuto>(propertyIndexedKeyframe.baseProperties.composite)) |
| compositeModes.append(std::get<CompositeOperationOrAuto>(propertyIndexedKeyframe.baseProperties.composite)); |
| if (!compositeModes.isEmpty()) { |
| // 1. Let composite modes be a sequence of CompositeOperationOrAuto values assigned from the “composite” member of property-indexed keyframe. If that member is a single |
| // CompositeOperationOrAuto value operation, let composite modes be a sequence of length one, with the value of the “composite” as its single item. |
| // 2. As with easings, if composite modes has fewer items than processed keyframes, repeat the elements in composite modes successively starting from the beginning of |
| // the list until composite modes has as many items as processed keyframes. |
| if (compositeModes.size() < parsedKeyframes.size()) { |
| size_t initialNumberOfCompositeModes = compositeModes.size(); |
| for (i = initialNumberOfCompositeModes; i < parsedKeyframes.size(); ++i) |
| compositeModes.append(compositeModes[i % initialNumberOfCompositeModes]); |
| } |
| // 3. Assign each value in composite modes that is not auto to the keyframe-specific composite operation on the keyframe with the corresponding position in processed |
| // keyframes until the end of processed keyframes is reached. |
| for (size_t i = 0; i < compositeModes.size() && i < parsedKeyframes.size(); ++i) { |
| if (compositeModes[i] != CompositeOperationOrAuto::Auto) |
| parsedKeyframes[i].composite = compositeModes[i]; |
| } |
| } |
| |
| return { }; |
| } |
| |
| ExceptionOr<Ref<KeyframeEffect>> KeyframeEffect::create(JSGlobalObject& lexicalGlobalObject, Document& document, Element* target, Strong<JSObject>&& keyframes, std::optional<Variant<double, KeyframeEffectOptions>>&& options) |
| { |
| auto keyframeEffect = adoptRef(*new KeyframeEffect(target, { })); |
| keyframeEffect->m_document = document; |
| |
| if (options) { |
| OptionalEffectTiming timing; |
| auto optionsValue = options.value(); |
| if (std::holds_alternative<double>(optionsValue)) { |
| Variant<double, String> duration = std::get<double>(optionsValue); |
| timing.duration = duration; |
| } else { |
| auto keyframeEffectOptions = std::get<KeyframeEffectOptions>(optionsValue); |
| |
| auto setPseudoElementResult = keyframeEffect->setPseudoElement(keyframeEffectOptions.pseudoElement); |
| if (setPseudoElementResult.hasException()) |
| return setPseudoElementResult.releaseException(); |
| |
| auto convertedDuration = keyframeEffectOptions.durationAsDoubleOrString(); |
| if (!convertedDuration) |
| return Exception { ExceptionCode::TypeError }; |
| |
| timing = { |
| *convertedDuration, |
| keyframeEffectOptions.iterations, |
| keyframeEffectOptions.delay, |
| keyframeEffectOptions.endDelay, |
| keyframeEffectOptions.iterationStart, |
| keyframeEffectOptions.easing, |
| keyframeEffectOptions.fill, |
| keyframeEffectOptions.direction |
| }; |
| |
| keyframeEffect->setComposite(keyframeEffectOptions.composite); |
| keyframeEffect->setIterationComposite(keyframeEffectOptions.iterationComposite); |
| } |
| auto updateTimingResult = keyframeEffect->updateTiming(document, timing); |
| if (updateTimingResult.hasException()) |
| return updateTimingResult.releaseException(); |
| } |
| |
| auto processKeyframesResult = keyframeEffect->processKeyframes(lexicalGlobalObject, document, WTFMove(keyframes)); |
| if (processKeyframesResult.hasException()) |
| return processKeyframesResult.releaseException(); |
| |
| return keyframeEffect; |
| } |
| |
| Ref<KeyframeEffect> KeyframeEffect::create(Ref<KeyframeEffect>&& source) |
| { |
| auto keyframeEffect = adoptRef(*new KeyframeEffect(nullptr, { })); |
| keyframeEffect->copyPropertiesFromSource(WTFMove(source)); |
| return keyframeEffect; |
| } |
| |
| Ref<KeyframeEffect> KeyframeEffect::create(const Element& target, const std::optional<Style::PseudoElementIdentifier>& pseudoElementIdentifier) |
| { |
| return adoptRef(*new KeyframeEffect(const_cast<Element*>(&target), pseudoElementIdentifier)); |
| } |
| |
| KeyframeEffect::KeyframeEffect(Element* target, const std::optional<Style::PseudoElementIdentifier>& pseudoElementIdentifier) |
| : m_target(target) |
| , m_pseudoElementIdentifier(pseudoElementIdentifier) |
| { |
| if (m_target) |
| m_document = m_target->document(); |
| } |
| |
| void KeyframeEffect::copyPropertiesFromSource(Ref<KeyframeEffect>&& source) |
| { |
| m_target = source->m_target; |
| m_pseudoElementIdentifier = source->m_pseudoElementIdentifier; |
| m_document = source->m_document; |
| m_compositeOperation = source->m_compositeOperation; |
| m_iterationCompositeOperation = source->m_iterationCompositeOperation; |
| |
| Vector<ParsedKeyframe> parsedKeyframes; |
| for (auto& sourceParsedKeyframe : source->m_parsedKeyframes) { |
| ParsedKeyframe parsedKeyframe; |
| parsedKeyframe.easing = sourceParsedKeyframe.easing; |
| parsedKeyframe.offset = sourceParsedKeyframe.offset; |
| parsedKeyframe.composite = sourceParsedKeyframe.composite; |
| parsedKeyframe.styleStrings = sourceParsedKeyframe.styleStrings; |
| parsedKeyframe.customStyleStrings = sourceParsedKeyframe.customStyleStrings; |
| parsedKeyframe.computedOffset = sourceParsedKeyframe.computedOffset; |
| parsedKeyframe.timingFunction = sourceParsedKeyframe.timingFunction; |
| parsedKeyframe.style = sourceParsedKeyframe.style->mutableCopy(); |
| parsedKeyframes.append(WTFMove(parsedKeyframe)); |
| } |
| m_parsedKeyframes = WTFMove(parsedKeyframes); |
| |
| setFill(source->fill()); |
| setDelay(source->specifiedDelay()); |
| setEndDelay(source->specifiedEndDelay()); |
| setDirection(source->direction()); |
| setIterations(source->iterations()); |
| setTimingFunction(source->timingFunction()); |
| setIterationStart(source->iterationStart()); |
| setIterationDuration(source->specifiedIterationDuration()); |
| |
| BlendingKeyframes blendingKeyframes(m_blendingKeyframes.identifier()); |
| blendingKeyframes.copyKeyframes(source->m_blendingKeyframes); |
| setBlendingKeyframes(WTFMove(blendingKeyframes)); |
| } |
| |
| static TimelineRangeOffset timelineRangeOffsetFromSpecifiedOffset(const BlendingKeyframe::Offset& specifiedOffset) |
| { |
| auto name = rangeStringFromSingleTimelineRangeName(specifiedOffset.name); |
| return TimelineRangeOffset { name, CSSNumericFactory::percent(specifiedOffset.value * 100) }; |
| } |
| |
| auto KeyframeEffect::getKeyframes() -> Vector<ComputedKeyframe> |
| { |
| // https://drafts.csswg.org/web-animations-1/#dom-keyframeeffectreadonly-getkeyframes |
| |
| if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation())) |
| styleOriginatedAnimation->flushPendingStyleChanges(); |
| |
| updateComputedKeyframeOffsetsIfNeeded(); |
| |
| Vector<ComputedKeyframe> computedKeyframes; |
| |
| if (!m_parsedKeyframes.isEmpty() || m_animationType == WebAnimationType::WebAnimation || !m_blendingKeyframes.containsAnimatableCSSProperty()) { |
| for (size_t i = 0; i < m_parsedKeyframes.size(); ++i) { |
| auto& parsedKeyframe = m_parsedKeyframes[i]; |
| ComputedKeyframe computedKeyframe { parsedKeyframe }; |
| for (auto& [cssPropertyId, stringValue] : computedKeyframe.styleStrings) { |
| if (cssPropertyId == CSSPropertyCustom) |
| continue; |
| if (auto cssValue = parsedKeyframe.style->getPropertyCSSValue(cssPropertyId)) |
| stringValue = cssValue->cssText(CSS::defaultSerializationContext()); |
| } |
| computedKeyframe.easing = timingFunctionForKeyframeAtIndex(i)->cssText(); |
| computedKeyframes.append(WTFMove(computedKeyframe)); |
| } |
| return computedKeyframes; |
| } |
| |
| RefPtr target = m_target; |
| auto* lastStyleChangeEventStyle = targetStyleable()->lastStyleChangeEventStyle(); |
| auto& elementStyle = lastStyleChangeEventStyle ? *lastStyleChangeEventStyle : currentStyle(); |
| |
| Style::Extractor computedStyleExtractor { target.get(), false, m_pseudoElementIdentifier }; |
| |
| BlendingKeyframes computedBlendingKeyframes(m_blendingKeyframes.identifier()); |
| computedBlendingKeyframes.copyKeyframes(m_blendingKeyframes); |
| |
| if (computedBlendingKeyframes.hasKeyframeNotUsingRangeOffset() || activeViewTimeline()) |
| computedBlendingKeyframes.fillImplicitKeyframes(*this, elementStyle); |
| |
| auto keyframeRules = [&]() -> const Vector<Ref<StyleRuleKeyframe>> { |
| RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation()); |
| if (!cssAnimation) |
| return { }; |
| |
| if (!m_target || !m_target->isConnected()) |
| return { }; |
| |
| Ref backingAnimation = cssAnimation->backingAnimation(); |
| auto* styleScope = Style::Scope::forOrdinal(*m_target, backingAnimation->name().scopeOrdinal); |
| if (!styleScope) |
| return { }; |
| |
| return styleScope->resolver().keyframeRulesForName(computedBlendingKeyframes.keyframesName(), backingAnimation->timingFunction()); |
| }(); |
| |
| auto matchingStyleRuleKeyframe = [&](const BlendingKeyframe& keyframe) -> StyleRuleKeyframe* { |
| auto* cssAnimation = dynamicDowncast<CSSAnimation>(animation()); |
| if (!cssAnimation) |
| return nullptr; |
| |
| auto& backingAnimation = cssAnimation->backingAnimation(); |
| auto defaultCompositeOperation = backingAnimation.compositeOperation(); |
| RefPtr defaultTimingFunction = backingAnimation.timingFunction(); |
| |
| auto compositeOperation = keyframe.compositeOperation().value_or(defaultCompositeOperation); |
| RefPtr timingFunction = keyframe.timingFunction(); |
| if (!timingFunction) |
| timingFunction = defaultTimingFunction; |
| |
| auto compositeOperationForStyleRuleKeyframe = [&](Ref<StyleRuleKeyframe>& styleRuleKeyframe) { |
| if (auto compositeOperationCSSValue = styleRuleKeyframe->properties().getPropertyCSSValue(CSSPropertyAnimationComposition)) { |
| if (auto compositeOperation = toCompositeOperation(*compositeOperationCSSValue)) |
| return *compositeOperation; |
| } |
| return defaultCompositeOperation; |
| }; |
| |
| auto timingFunctionForStyleRuleKeyframe = [&](Ref<StyleRuleKeyframe>& styleRuleKeyframe) -> RefPtr<const TimingFunction> { |
| if (auto timingFunctionCSSValue = styleRuleKeyframe->properties().getPropertyCSSValue(CSSPropertyAnimationTimingFunction)) { |
| if (auto timingFunction = Style::createTimingFunctionDeprecated(*timingFunctionCSSValue)) |
| return timingFunction; |
| } |
| if (defaultTimingFunction) |
| return defaultTimingFunction; |
| return &CubicBezierTimingFunction::defaultTimingFunction(); |
| }; |
| |
| auto& specifiedOffset = keyframe.specifiedOffset(); |
| StyleRuleKeyframe::Key key { SingleTimelineRange::valueID(specifiedOffset.name), specifiedOffset.value }; |
| |
| for (auto& keyframeRule : keyframeRules) { |
| if (compositeOperationForStyleRuleKeyframe(keyframeRule) != compositeOperation) |
| continue; |
| if (timingFunctionForStyleRuleKeyframe(keyframeRule) != timingFunction) |
| continue; |
| for (auto keyframeRuleKey : keyframeRule->keys()) { |
| if (keyframeRuleKey == key) |
| return keyframeRule.ptr(); |
| } |
| } |
| return nullptr; |
| }; |
| |
| auto styleProperties = MutableStyleProperties::create(); |
| if (m_animationType == WebAnimationType::CSSAnimation && m_target->isConnected()) { |
| auto matchingRules = m_target->styleResolver().pseudoStyleRulesForElement(target.get(), m_pseudoElementIdentifier, Style::Resolver::AllCSSRules); |
| for (auto& matchedRule : matchingRules) |
| styleProperties->mergeAndOverrideOnConflict(matchedRule->properties()); |
| if (RefPtr target = dynamicDowncast<StyledElement>(*m_target); target && !m_pseudoElementIdentifier) { |
| if (RefPtr inlineProperties = target->inlineStyle()) |
| styleProperties->mergeAndOverrideOnConflict(*inlineProperties); |
| } |
| } |
| |
| Vector<ComputedKeyframe> computedKeyframesWithTimelineRangeOffset; |
| Vector<ComputedKeyframe> computedKeyframesWithDoubleOffset; |
| |
| for (auto& keyframe : computedBlendingKeyframes) { |
| auto& style = *keyframe.style(); |
| RefPtr keyframeRule = matchingStyleRuleKeyframe(keyframe); |
| |
| ComputedKeyframe computedKeyframe; |
| computedKeyframe.offset = [&] -> KeyframeOffset { |
| if (keyframe.usesRangeOffset()) |
| return timelineRangeOffsetFromSpecifiedOffset(keyframe.specifiedOffset()); |
| return keyframe.specifiedOffset().value; |
| }(); |
| computedKeyframe.computedOffset = keyframe.offset(); |
| // For CSS transitions, all keyframes should return "linear" since the effect's global timing function applies. |
| computedKeyframe.easing = is<CSSTransition>(animation()) ? "linear"_s : timingFunctionForBlendingKeyframe(keyframe)->cssText(); |
| |
| if (auto compositeOperation = keyframe.compositeOperation()) |
| computedKeyframe.composite = toCompositeOperationOrAuto(*compositeOperation); |
| |
| auto addPropertyToKeyframe = [&](CSSPropertyID cssPropertyId) { |
| String styleString = emptyString(); |
| if (keyframeRule) { |
| if (auto cssValue = keyframeRule->properties().getPropertyCSSValue(cssPropertyId)) { |
| if (!cssValue->hasVariableReferences()) |
| styleString = keyframeRule->properties().getPropertyValue(cssPropertyId); |
| } |
| } |
| if (styleString.isEmpty()) { |
| if (auto cssValue = styleProperties->getPropertyCSSValue(cssPropertyId)) { |
| if (!cssValue->hasVariableReferences()) |
| styleString = styleProperties->getPropertyValue(cssPropertyId); |
| } |
| } |
| if (styleString.isEmpty()) |
| styleString = computedStyleExtractor.propertyValueSerializationInStyle(style, cssPropertyId, CSS::defaultSerializationContext(), CSSValuePool::singleton(), nullptr, Style::ExtractorState::PropertyValueType::Computed); |
| computedKeyframe.styleStrings.set(cssPropertyId, styleString); |
| }; |
| |
| auto addCustomPropertyToKeyframe = [&](const AtomString& customProperty) { |
| String styleString = emptyString(); |
| if (keyframeRule) { |
| if (auto cssValue = keyframeRule->properties().getCustomPropertyCSSValue(customProperty)) { |
| if (!cssValue->hasVariableReferences()) |
| styleString = keyframeRule->properties().getCustomPropertyValue(customProperty); |
| } |
| } |
| if (styleString.isEmpty()) { |
| if (auto cssValue = styleProperties->getCustomPropertyCSSValue(customProperty)) { |
| if (!cssValue->hasVariableReferences()) |
| styleString = styleProperties->getCustomPropertyValue(customProperty); |
| } |
| } |
| if (styleString.isEmpty()) { |
| if (RefPtr cssValue = style.customPropertyValue(customProperty)) |
| styleString = cssValue->propertyValueSerialization(CSS::defaultSerializationContext(), style); |
| } |
| computedKeyframe.customStyleStrings.set(customProperty, styleString); |
| }; |
| |
| for (auto property : keyframe.properties()) { |
| WTF::switchOn(property, |
| [&] (CSSPropertyID cssProperty) { |
| addPropertyToKeyframe(cssProperty); |
| }, |
| [&] (const AtomString& customProperty) { |
| if (m_animationType != WebAnimationType::CSSAnimation) |
| addCustomPropertyToKeyframe(customProperty); |
| } |
| ); |
| } |
| |
| // FIXME: this is so that we mimic the Chrome behavior since this isn't |
| // spec'd out, but it makes little sense to me. Items ought to be sorted |
| // by computed offset just like BlendingKeyframes would organize its keyframes. |
| // https://github.com/w3c/csswg-drafts/issues/11467 |
| if (std::holds_alternative<double>(computedKeyframe.offset)) |
| computedKeyframesWithDoubleOffset.append(WTFMove(computedKeyframe)); |
| else |
| computedKeyframesWithTimelineRangeOffset.append(WTFMove(computedKeyframe)); |
| } |
| |
| computedKeyframes.appendVector(WTFMove(computedKeyframesWithDoubleOffset)); |
| computedKeyframes.appendVector(WTFMove(computedKeyframesWithTimelineRangeOffset)); |
| |
| return computedKeyframes; |
| } |
| |
| ExceptionOr<void> KeyframeEffect::setBindingsKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput) |
| { |
| auto retVal = setKeyframes(lexicalGlobalObject, document, WTFMove(keyframesInput)); |
| if (!retVal.hasException()) { |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) |
| cssAnimation->effectKeyframesWereSetUsingBindings(); |
| } |
| return retVal; |
| } |
| |
| ExceptionOr<void> KeyframeEffect::setKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput) |
| { |
| auto processKeyframesResult = processKeyframes(lexicalGlobalObject, document, WTFMove(keyframesInput)); |
| if (!processKeyframesResult.hasException() && animation()) { |
| animation()->effectTimingDidChange(); |
| |
| // Need a full style invalidation since the new keyframes may interact differently with the base style. |
| if (auto target = targetStyleable()) |
| target->element.invalidateStyleInternal(); |
| } |
| |
| return processKeyframesResult; |
| } |
| |
| void KeyframeEffect::keyframesRuleDidChange() |
| { |
| ASSERT(is<CSSAnimation>(animation())); |
| clearBlendingKeyframes(); |
| invalidate(); |
| } |
| |
| void KeyframeEffect::customPropertyRegistrationDidChange(const AtomString& customProperty) |
| { |
| // If the registration of a custom property is changed, we should recompute keyframes |
| // at the next opportunity as the initial value, inherited value, etc. could have changed. |
| if (!m_blendingKeyframes.properties().contains(customProperty)) |
| return; |
| |
| clearBlendingKeyframes(); |
| invalidate(); |
| } |
| |
| ExceptionOr<void> KeyframeEffect::processKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput) |
| { |
| Ref protectedDocument { document }; |
| |
| // 1. If object is null, return an empty sequence of keyframes. |
| if (!keyframesInput.get()) |
| return { }; |
| |
| VM& vm = lexicalGlobalObject.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| // 2. Let processed keyframes be an empty sequence of keyframes. |
| Vector<ParsedKeyframe> parsedKeyframes; |
| |
| // 3. Let method be the result of GetMethod(object, @@iterator). |
| auto method = keyframesInput.get()->get(&lexicalGlobalObject, vm.propertyNames->iteratorSymbol); |
| |
| // 4. Check the completion record of method. |
| RETURN_IF_EXCEPTION(scope, Exception { ExceptionCode::TypeError }); |
| |
| // 5. Perform the steps corresponding to the first matching condition from below, |
| Vector<String> unusedEasings; |
| if (!method.isUndefined()) { |
| auto retVal = processIterableKeyframes(lexicalGlobalObject, document, WTFMove(keyframesInput), WTFMove(method), parsedKeyframes); |
| if (retVal.hasException()) |
| return retVal.releaseException(); |
| } else { |
| auto retVal = processPropertyIndexedKeyframes(lexicalGlobalObject, document, WTFMove(keyframesInput), parsedKeyframes, unusedEasings); |
| if (retVal.hasException()) |
| return retVal.releaseException(); |
| } |
| |
| // 6. If processed keyframes is not loosely sorted by offset, throw a TypeError and abort these steps. |
| // 7. If there exist any keyframe in processed keyframes whose keyframe offset is non-null and less than |
| // zero or greater than one, throw a TypeError and abort these steps. |
| double lastNonNullOffset = -1; |
| for (auto& keyframe : parsedKeyframes) { |
| auto* doubleOffset = std::get_if<double>(&keyframe.offset); |
| if (!doubleOffset) |
| continue; |
| auto offset = *doubleOffset; |
| if (offset < lastNonNullOffset || offset < 0 || offset > 1) |
| return Exception { ExceptionCode::TypeError }; |
| lastNonNullOffset = offset; |
| } |
| |
| // We take a slight detour from the spec text and compute the missing keyframe offsets right away |
| // since they can be computed up-front. |
| computeMissingKeyframeOffsets(parsedKeyframes, activeViewTimeline(), animation()); |
| |
| CSSParserContext parserContext(document); |
| |
| // 8. For each frame in processed keyframes, perform the following steps: |
| for (auto& keyframe : parsedKeyframes) { |
| // Let the timing function of frame be the result of parsing the “easing” property on frame using the CSS syntax |
| // defined for the easing property of the AnimationEffectTiming interface. |
| // If parsing the “easing” property fails, throw a TypeError and abort this procedure. |
| |
| // FIXME: Determine the how calc() and relative units should be resolved and switch to the non-deprecated parsing function. |
| auto timingFunctionResult = CSSPropertyParserHelpers::parseEasingFunctionDeprecated(keyframe.easing, parserContext); |
| if (!timingFunctionResult) |
| return Exception { ExceptionCode::TypeError }; |
| keyframe.timingFunction = WTFMove(timingFunctionResult); |
| } |
| |
| // 9. Parse each of the values in unused easings using the CSS syntax defined for easing property of the |
| // AnimationEffectTiming interface, and if any of the values fail to parse, throw a TypeError |
| // and abort this procedure. |
| for (auto& easing : unusedEasings) { |
| // FIXME: Determine the how calc() and relative units should be resolved and switch to the non-deprecated parsing function. |
| auto timingFunctionResult = CSSPropertyParserHelpers::parseEasingFunctionDeprecated(easing, parserContext); |
| if (!timingFunctionResult) |
| return Exception { ExceptionCode::TypeError }; |
| } |
| |
| m_parsedKeyframes = WTFMove(parsedKeyframes); |
| |
| clearBlendingKeyframes(); |
| |
| invalidate(); |
| |
| return { }; |
| } |
| |
| static BlendingKeyframe::Offset specifiedOffsetForParsedKeyframe(const KeyframeEffect::ParsedKeyframe& keyframe) |
| { |
| if (auto* timelineRangeOffset = std::get_if<TimelineRangeOffset>(&keyframe.offset)) { |
| auto rangeName = rangeStringToSingleTimelineRangeName(timelineRangeOffset->rangeName); |
| RefPtr offsetUnitValue = dynamicDowncast<CSSUnitValue>(timelineRangeOffset->offset); |
| ASSERT(offsetUnitValue && offsetUnitValue->unitEnum() == CSSUnitType::CSS_PERCENTAGE); |
| return { rangeName, offsetUnitValue->value() / 100 }; |
| } |
| |
| ASSERT(!std::isnan(keyframe.computedOffset)); |
| return keyframe.computedOffset; |
| } |
| |
| void KeyframeEffect::updateBlendingKeyframes(RenderStyle& elementStyle, const Style::ResolutionContext& resolutionContext) |
| { |
| updateComputedKeyframeOffsetsIfNeeded(); |
| |
| if (!m_blendingKeyframes.isEmpty() || !m_target) |
| return; |
| |
| BlendingKeyframes blendingKeyframes(m_blendingKeyframes.identifier()); |
| Ref styleResolver = m_target->styleResolver(); |
| |
| for (auto& keyframe : m_parsedKeyframes) { |
| BlendingKeyframe blendingKeyframe(specifiedOffsetForParsedKeyframe(keyframe), nullptr); |
| blendingKeyframe.setTimingFunction(keyframe.timingFunction->clone()); |
| |
| switch (keyframe.composite) { |
| case CompositeOperationOrAuto::Replace: |
| blendingKeyframe.setCompositeOperation(CompositeOperation::Replace); |
| break; |
| case CompositeOperationOrAuto::Add: |
| blendingKeyframe.setCompositeOperation(CompositeOperation::Add); |
| break; |
| case CompositeOperationOrAuto::Accumulate: |
| blendingKeyframe.setCompositeOperation(CompositeOperation::Accumulate); |
| break; |
| case CompositeOperationOrAuto::Auto: |
| break; |
| } |
| |
| auto keyframeRule = StyleRuleKeyframe::create(keyframe.style->immutableCopyIfNeeded()); |
| blendingKeyframe.setStyle(styleResolver->styleForKeyframe(*m_target, elementStyle, resolutionContext, keyframeRule.get(), blendingKeyframe)); |
| blendingKeyframes.insert(WTFMove(blendingKeyframe)); |
| blendingKeyframes.updatePropertiesMetadata(keyframeRule->properties()); |
| } |
| |
| setBlendingKeyframes(WTFMove(blendingKeyframes)); |
| } |
| |
| const HashSet<AnimatableCSSProperty>& KeyframeEffect::animatedProperties() |
| { |
| if (!m_blendingKeyframes.isEmpty()) |
| return m_blendingKeyframes.properties(); |
| |
| if (m_animatedProperties.isEmpty()) { |
| for (auto& keyframe : m_parsedKeyframes) { |
| for (auto keyframeCustomProperty : keyframe.customStyleStrings.keys()) |
| m_animatedProperties.add(keyframeCustomProperty); |
| for (auto keyframeProperty : keyframe.styleStrings.keys()) |
| m_animatedProperties.add(keyframeProperty); |
| } |
| } |
| |
| return m_animatedProperties; |
| } |
| |
| bool KeyframeEffect::animatesProperty(const AnimatableCSSProperty& property) const |
| { |
| if (!m_blendingKeyframes.isEmpty()) |
| return m_blendingKeyframes.containsProperty(property); |
| |
| return WTF::switchOn(property, |
| [&](CSSPropertyID cssProperty) { |
| return m_parsedKeyframes.findIf([&](const auto& keyframe) { |
| for (auto keyframeProperty : keyframe.styleStrings.keys()) { |
| if (keyframeProperty == cssProperty) |
| return true; |
| } |
| return false; |
| }); |
| }, |
| [&](const AtomString& customProperty) { |
| return m_parsedKeyframes.findIf([&](const auto& keyframe) { |
| for (auto keyframeProperty : keyframe.customStyleStrings.keys()) { |
| if (keyframeProperty == customProperty) |
| return true; |
| } |
| return false; |
| }); |
| }) != notFound; |
| } |
| |
| bool KeyframeEffect::forceLayoutIfNeeded() |
| { |
| if (!m_needsForcedLayout || !m_target) |
| return false; |
| |
| auto* renderer = this->renderer(); |
| if (!renderer || !renderer->parent()) |
| return false; |
| |
| ASSERT(document()); |
| RefPtr frameView = document()->view(); |
| if (!frameView) |
| return false; |
| |
| frameView->forceLayout(); |
| return true; |
| } |
| |
| |
| void KeyframeEffect::clearBlendingKeyframes() |
| { |
| m_animationType = WebAnimationType::WebAnimation; |
| m_blendingKeyframes.clear(); |
| } |
| |
| void KeyframeEffect::setBlendingKeyframes(BlendingKeyframes&& blendingKeyframes) |
| { |
| CanBeAcceleratedMutationScope mutationScope(this); |
| |
| m_blendingKeyframes = WTFMove(blendingKeyframes); |
| m_animatedProperties.clear(); |
| |
| m_needsComputedKeyframeOffsetsUpdate = true; |
| |
| computedNeedsForcedLayout(); |
| computeStackingContextImpact(); |
| computeAcceleratedPropertiesState(); |
| computeSomeKeyframesUseStepsOrLinearTimingFunctionWithPoints(); |
| computeHasImplicitKeyframeForAcceleratedProperty(); |
| computeHasKeyframeComposingAcceleratedProperty(); |
| computeHasAcceleratedPropertyOverriddenByCascadeProperty(); |
| computeHasReferenceFilter(); |
| computeHasSizeDependentTransform(); |
| analyzeAcceleratedProperties(); |
| |
| checkForMatchingTransformFunctionLists(); |
| |
| updateAcceleratedAnimationIfNecessary(); |
| } |
| |
| void KeyframeEffect::analyzeAcceleratedProperties() |
| { |
| m_acceleratedProperties.clear(); |
| m_acceleratedPropertiesWithImplicitKeyframe.clear(); |
| |
| ASSERT(document()); |
| auto& settings = document()->settings(); |
| for (auto& property : m_blendingKeyframes.properties()) { |
| if (!Style::Interpolation::isAccelerated(property, settings)) |
| continue; |
| m_acceleratedProperties.add(property); |
| if (m_blendingKeyframes.hasImplicitKeyframeForProperty(property)) |
| m_acceleratedPropertiesWithImplicitKeyframe.add(property); |
| } |
| } |
| |
| void KeyframeEffect::checkForMatchingTransformFunctionLists() |
| { |
| if (m_blendingKeyframes.size() < 2 || !m_blendingKeyframes.containsProperty(CSSPropertyTransform)) { |
| m_transformFunctionListsMatchPrefix = 0; |
| return; |
| } |
| |
| TransformOperationsSharedPrimitivesPrefix prefix; |
| for (const auto& keyframe : m_blendingKeyframes) |
| prefix.update(keyframe.style()->transform()); |
| |
| m_transformFunctionListsMatchPrefix = prefix.primitives().size(); |
| } |
| |
| std::optional<unsigned> KeyframeEffect::transformFunctionListPrefix() const |
| { |
| auto isTransformFunctionListsMatchPrefixRelevant = [&]() { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| // The prefix is only relevant if the animation is fully replaced. |
| if (m_compositeOperation != CompositeOperation::Replace || m_hasKeyframeComposingAcceleratedProperty) |
| return false; |
| } |
| #endif |
| // The CoreAnimation animation code can only use direct function interpolation when all keyframes share the same |
| // prefix of shared transform function primitives, whereas software animations simply calls blend(...) which can do |
| // direct interpolation based on the function list of any two particular keyframes. The prefix serves as a way to |
| // make sure that the results of blend(...) can be made to return the same results as rendered by the hardware |
| // animation code. |
| return !preventsAcceleration(); |
| }; |
| |
| return isTransformFunctionListsMatchPrefixRelevant() ? std::optional<unsigned>(m_transformFunctionListsMatchPrefix) : std::nullopt; |
| } |
| |
| void KeyframeEffect::computeStyleOriginatedAnimationBlendingKeyframes(const RenderStyle* oldStyle, const RenderStyle& newStyle, const Style::ResolutionContext& resolutionContext) |
| { |
| ASSERT(is<StyleOriginatedAnimation>(animation())); |
| if (is<CSSAnimation>(animation())) |
| computeCSSAnimationBlendingKeyframes(newStyle, resolutionContext); |
| else if (is<CSSTransition>(animation())) { |
| ASSERT(oldStyle); |
| computeCSSTransitionBlendingKeyframes(*oldStyle, newStyle); |
| } |
| } |
| |
| void KeyframeEffect::computeCSSAnimationBlendingKeyframes(const RenderStyle& unanimatedStyle, const Style::ResolutionContext& resolutionContext) |
| { |
| ASSERT(document()); |
| |
| Ref backingAnimation = downcast<CSSAnimation>(*animation()).backingAnimation(); |
| |
| BlendingKeyframes blendingKeyframes(AtomString { backingAnimation->name().name }); |
| if (m_target) { |
| if (auto* styleScope = Style::Scope::forOrdinal(*m_target, backingAnimation->name().scopeOrdinal)) |
| styleScope->resolver().keyframeStylesForAnimation(*m_target, unanimatedStyle, resolutionContext, blendingKeyframes, backingAnimation->timingFunction()); |
| |
| // Ensure resource loads for all the frames. |
| for (auto& keyframe : blendingKeyframes) { |
| if (auto* style = const_cast<RenderStyle*>(keyframe.style())) |
| Style::loadPendingResources(*style, *document(), m_target.get()); |
| } |
| } |
| |
| m_animationType = WebAnimationType::CSSAnimation; |
| setBlendingKeyframes(WTFMove(blendingKeyframes)); |
| } |
| |
| void KeyframeEffect::computeCSSTransitionBlendingKeyframes(const RenderStyle& oldStyle, const RenderStyle& newStyle) |
| { |
| ASSERT(document()); |
| |
| if (m_blendingKeyframes.size()) |
| return; |
| |
| auto property = downcast<CSSTransition>(animation())->property(); |
| |
| auto toStyle = RenderStyle::clonePtr(newStyle); |
| if (m_target) |
| Style::loadPendingResources(*toStyle, *document(), m_target.get()); |
| |
| BlendingKeyframes blendingKeyframes(m_blendingKeyframes.identifier()); |
| |
| BlendingKeyframe fromBlendingKeyframe(0, RenderStyle::clonePtr(oldStyle)); |
| fromBlendingKeyframe.addProperty(property); |
| blendingKeyframes.insert(WTFMove(fromBlendingKeyframe)); |
| |
| BlendingKeyframe toBlendingKeyframe(1, WTFMove(toStyle)); |
| toBlendingKeyframe.addProperty(property); |
| blendingKeyframes.insert(WTFMove(toBlendingKeyframe)); |
| |
| m_animationType = WebAnimationType::CSSTransition; |
| setBlendingKeyframes(WTFMove(blendingKeyframes)); |
| } |
| |
| void KeyframeEffect::computedNeedsForcedLayout() |
| { |
| m_needsForcedLayout = [&]() { |
| if (is<CSSTransition>(animation())) |
| return false; |
| return m_blendingKeyframes.hasWidthDependentTransform() || m_blendingKeyframes.hasHeightDependentTransform(); |
| }(); |
| } |
| |
| void KeyframeEffect::computeStackingContextImpact() |
| { |
| m_triggersStackingContext = false; |
| for (auto property : m_blendingKeyframes.properties()) { |
| if (std::holds_alternative<CSSPropertyID>(property) && WillChangeData::propertyCreatesStackingContext(std::get<CSSPropertyID>(property))) { |
| m_triggersStackingContext = true; |
| break; |
| } |
| } |
| } |
| |
| void KeyframeEffect::updateIsAssociatedWithProgressBasedTimeline() |
| { |
| auto wasAssociatedWithProgressBasedTimeline = m_isAssociatedWithProgressBasedTimeline; |
| |
| m_isAssociatedWithProgressBasedTimeline = [&] { |
| if (RefPtr animation = this->animation()) { |
| if (RefPtr timeline = animation->timeline()) |
| return timeline->isProgressBased(); |
| } |
| return false; |
| }(); |
| |
| if (wasAssociatedWithProgressBasedTimeline != m_isAssociatedWithProgressBasedTimeline) |
| updateAcceleratedAnimationIfNecessary(); |
| } |
| |
| void KeyframeEffect::animationTimelineDidChange(const AnimationTimeline* timeline) |
| { |
| AnimationEffect::animationTimelineDidChange(timeline); |
| |
| updateIsAssociatedWithProgressBasedTimeline(); |
| |
| updateEffectStackMembership(); |
| |
| m_needsComputedKeyframeOffsetsUpdate = true; |
| } |
| |
| void KeyframeEffect::animationRelevancyDidChange() |
| { |
| updateEffectStackMembership(); |
| } |
| |
| void KeyframeEffect::updateEffectStackMembership() |
| { |
| auto target = targetStyleable(); |
| if (!target) |
| return; |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| StackMembershipMutationScope stackMembershipMutationScope(*this); |
| #endif |
| |
| bool isRelevant = animation() && animation()->isRelevant(); |
| if (isRelevant && !m_inTargetEffectStack) |
| target->ensureKeyframeEffectStack().addEffect(*this); |
| else if (!isRelevant && m_inTargetEffectStack) |
| target->ensureKeyframeEffectStack().removeEffect(*this); |
| } |
| |
| void KeyframeEffect::setAnimation(WebAnimation* animation) |
| { |
| bool animationChanged = animation != this->animation(); |
| |
| AnimationEffect::setAnimation(animation); |
| |
| if (!animationChanged) |
| return; |
| |
| if (m_animationType == WebAnimationType::CSSAnimation) |
| clearBlendingKeyframes(); |
| updateEffectStackMembership(); |
| |
| updateIsAssociatedWithProgressBasedTimeline(); |
| } |
| |
| const std::optional<const Styleable> KeyframeEffect::targetStyleable() const |
| { |
| if (m_target) |
| return Styleable(*m_target, m_pseudoElementIdentifier); |
| return std::nullopt; |
| } |
| |
| bool KeyframeEffect::targetsPseudoElement() const |
| { |
| return m_target.get() && m_pseudoElementIdentifier; |
| } |
| |
| void KeyframeEffect::setTarget(RefPtr<Element>&& newTarget) |
| { |
| if (m_target == newTarget) |
| return; |
| |
| auto& previousTargetStyleable = targetStyleable(); |
| RefPtr<Element> protector; |
| if (previousTargetStyleable) |
| protector = previousTargetStyleable->element; |
| m_target = WTFMove(newTarget); |
| didChangeTargetStyleable(previousTargetStyleable); |
| } |
| |
| const String KeyframeEffect::pseudoElement() const |
| { |
| // https://drafts.csswg.org/web-animations/#dom-keyframeeffect-pseudoelement |
| |
| // The target pseudo-selector. null if this effect has no effect target or if the effect target is an element (i.e. not a pseudo-element). |
| // When the effect target is a pseudo-element, this specifies the pseudo-element selector (e.g. ::before). |
| if (targetsPseudoElement()) |
| return pseudoElementIdentifierAsString(m_pseudoElementIdentifier); |
| return { }; |
| } |
| |
| ExceptionOr<void> KeyframeEffect::setPseudoElement(const String& pseudoElement) |
| { |
| // https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement |
| auto [parsed, pseudoElementIdentifier] = pseudoElementIdentifierFromString(pseudoElement, document()); |
| if (!parsed) |
| return Exception { ExceptionCode::SyntaxError, "Parsing pseudo-element selector failed"_s }; |
| |
| if (m_pseudoElementIdentifier == pseudoElementIdentifier) |
| return { }; |
| |
| auto& previousTargetStyleable = targetStyleable(); |
| m_pseudoElementIdentifier = pseudoElementIdentifier; |
| didChangeTargetStyleable(previousTargetStyleable); |
| |
| return { }; |
| } |
| |
| void KeyframeEffect::didChangeTargetStyleable(const std::optional<const Styleable>& previousTargetStyleable) |
| { |
| auto newTargetStyleable = targetStyleable(); |
| |
| if (RefPtr effectAnimation = animation()) |
| effectAnimation->effectTargetDidChange(previousTargetStyleable, newTargetStyleable); |
| |
| clearBlendingKeyframes(); |
| |
| // We need to invalidate the effect now that the target has changed |
| // to ensure the effect's styles are applied to the new target right away. |
| invalidate(); |
| |
| // Likewise, we need to invalidate styles on the previous target so that |
| // any animated styles are removed immediately. |
| invalidateElement(previousTargetStyleable); |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| StackMembershipMutationScope stackMembershipMutationScope(*this); |
| #endif |
| |
| if (previousTargetStyleable) |
| previousTargetStyleable->ensureKeyframeEffectStack().removeEffect(*this); |
| |
| if (newTargetStyleable) |
| newTargetStyleable->ensureKeyframeEffectStack().addEffect(*this); |
| } |
| |
| OptionSet<AnimationImpact> KeyframeEffect::apply(RenderStyle& targetStyle, const Style::ResolutionContext& resolutionContext) |
| { |
| OptionSet<AnimationImpact> impact; |
| if (!m_target) |
| return impact; |
| |
| updateBlendingKeyframes(targetStyle, resolutionContext); |
| |
| auto computedTiming = getComputedTiming(); |
| |
| if (m_phaseAtLastApplication != computedTiming.phase) { |
| m_phaseAtLastApplication = computedTiming.phase; |
| impact.add(AnimationImpact::RequiresRecomposite); |
| } |
| |
| if (auto target = targetStyleable()) |
| InspectorInstrumentation::willApplyKeyframeEffect(*target, *this, computedTiming); |
| |
| if (!computedTiming.progress) |
| return impact; |
| |
| ASSERT(computedTiming.currentIteration); |
| setAnimatedPropertiesInStyle(targetStyle, computedTiming); |
| return impact; |
| } |
| |
| bool KeyframeEffect::isRunningAccelerated() const |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| if (!m_inTargetEffectStack || !canBeAccelerated()) |
| return false; |
| RefPtr animation = this->animation(); |
| ASSERT(animation); |
| return !animation->isSuspended() && animation->playState() == WebAnimation::PlayState::Running; |
| } |
| #endif |
| return m_runningAccelerated == RunningAccelerated::Yes; |
| } |
| |
| bool KeyframeEffect::isCurrentlyAffectingProperty(CSSPropertyID property, Accelerated accelerated) const |
| { |
| if (accelerated == Accelerated::Yes && !isRunningAccelerated() && !isAboutToRunAccelerated()) |
| return false; |
| |
| if (!m_blendingKeyframes.properties().contains(property)) |
| return false; |
| |
| if (m_pseudoElementIdentifier && m_pseudoElementIdentifier->pseudoId == PseudoId::Marker && !Style::isValidMarkerStyleProperty(property)) |
| return false; |
| |
| return m_phaseAtLastApplication == AnimationEffectPhase::Active; |
| } |
| |
| bool KeyframeEffect::isRunningAcceleratedAnimationForProperty(CSSPropertyID property) const |
| { |
| if (!isRunningAccelerated()) |
| return false; |
| |
| ASSERT(document()); |
| return Style::Interpolation::isAccelerated(property, document()->settings()) && m_blendingKeyframes.properties().contains(property); |
| } |
| |
| static bool propertiesContainTransformRelatedProperty(const HashSet<AnimatableCSSProperty>& properties) |
| { |
| return properties.contains(CSSPropertyTranslate) |
| || properties.contains(CSSPropertyScale) |
| || properties.contains(CSSPropertyRotate) |
| || properties.contains(CSSPropertyTransform); |
| } |
| |
| bool KeyframeEffect::isRunningAcceleratedTransformRelatedAnimation() const |
| { |
| return isRunningAccelerated() && propertiesContainTransformRelatedProperty(m_blendingKeyframes.properties()); |
| } |
| |
| void KeyframeEffect::invalidate() |
| { |
| LOG_WITH_STREAM(Animations, stream << "KeyframeEffect::invalidate on element " << ValueOrNull(m_target.get())); |
| invalidateElement(targetStyleable()); |
| } |
| |
| void KeyframeEffect::computeAcceleratedPropertiesState() |
| { |
| bool hasSomeAcceleratedProperties = false; |
| bool hasSomeUnacceleratedProperties = false; |
| |
| if (RefPtr document = this->document()) { |
| auto& settings = document->settings(); |
| for (auto property : m_blendingKeyframes.properties()) { |
| // If any animated property can be accelerated, then the animation should run accelerated. |
| if (Style::Interpolation::isAccelerated(property, settings)) |
| hasSomeAcceleratedProperties = true; |
| else |
| hasSomeUnacceleratedProperties = true; |
| if (hasSomeAcceleratedProperties && hasSomeUnacceleratedProperties) |
| break; |
| } |
| } |
| |
| if (!hasSomeAcceleratedProperties) |
| m_acceleratedPropertiesState = AcceleratedProperties::None; |
| else if (hasSomeUnacceleratedProperties) |
| m_acceleratedPropertiesState = AcceleratedProperties::Some; |
| else |
| m_acceleratedPropertiesState = AcceleratedProperties::All; |
| } |
| |
| static bool isLinearTimingFunctionWithPoints(const TimingFunction* timingFunction) |
| { |
| auto* linearTimingFunction = dynamicDowncast<LinearTimingFunction>(timingFunction); |
| return linearTimingFunction && !linearTimingFunction->points().isEmpty(); |
| } |
| |
| void KeyframeEffect::computeSomeKeyframesUseStepsOrLinearTimingFunctionWithPoints() |
| { |
| m_someKeyframesUseStepsTimingFunction = false; |
| m_someKeyframesUseLinearTimingFunctionWithPoints = false; |
| |
| // If we're dealing with a CSS Animation and it specifies a default steps() or linear() timing function, |
| // we need to check that any of the specified keyframes either does not have an explicit timing |
| // function or specifies an explicit steps() or linear() timing function. |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) { |
| RefPtr defaultTimingFunction = cssAnimation->backingAnimation().timingFunction(); |
| auto defaultTimingFunctionIsSteps = is<StepsTimingFunction>(defaultTimingFunction); |
| auto defaultTimingFunctionIsLinearWithPoints = isLinearTimingFunctionWithPoints(defaultTimingFunction.get()); |
| if (defaultTimingFunctionIsSteps || defaultTimingFunctionIsLinearWithPoints) { |
| for (auto& keyframe : m_blendingKeyframes) { |
| RefPtr timingFunction = keyframe.timingFunction(); |
| if (defaultTimingFunctionIsSteps && !m_someKeyframesUseStepsTimingFunction) |
| m_someKeyframesUseStepsTimingFunction = !timingFunction || is<StepsTimingFunction>(timingFunction); |
| else if (defaultTimingFunctionIsLinearWithPoints && !m_someKeyframesUseLinearTimingFunctionWithPoints) |
| m_someKeyframesUseLinearTimingFunctionWithPoints = !timingFunction || isLinearTimingFunctionWithPoints(timingFunction.get()); |
| if (defaultTimingFunctionIsSteps == m_someKeyframesUseStepsTimingFunction && defaultTimingFunctionIsLinearWithPoints == m_someKeyframesUseLinearTimingFunctionWithPoints) |
| break; |
| } |
| return; |
| } |
| } |
| |
| // For any other type of animation, we just need to check whether any of the keyframes specify |
| // an explicit steps() or linear() timing function. |
| for (auto& keyframe : m_blendingKeyframes) { |
| RefPtr timingFunction = keyframe.timingFunction(); |
| if (!m_someKeyframesUseStepsTimingFunction && is<StepsTimingFunction>(timingFunction)) |
| m_someKeyframesUseStepsTimingFunction = true; |
| if (!m_someKeyframesUseLinearTimingFunctionWithPoints && isLinearTimingFunctionWithPoints(timingFunction.get())) |
| m_someKeyframesUseLinearTimingFunctionWithPoints = true; |
| if (m_someKeyframesUseStepsTimingFunction && m_someKeyframesUseLinearTimingFunctionWithPoints) |
| return; |
| } |
| } |
| |
| bool KeyframeEffect::hasImplicitKeyframes() const |
| { |
| auto numberOfKeyframes = m_parsedKeyframes.size(); |
| |
| // If we have no keyframes, then there cannot be any implicit keyframes. |
| if (!numberOfKeyframes) |
| return false; |
| |
| // If we have a single keyframe, then there has to be at least one implicit keyframe. |
| if (numberOfKeyframes == 1) |
| return true; |
| |
| // If we have two or more keyframes, then we have implicit keyframes if the first and last |
| // keyframes don't have 0 and 1 respectively as their computed offset. |
| return m_parsedKeyframes[0].computedOffset || m_parsedKeyframes[numberOfKeyframes - 1].computedOffset != 1; |
| } |
| |
| void KeyframeEffect::getAnimatedStyle(std::unique_ptr<RenderStyle>& animatedStyle) |
| { |
| if (!renderer() || !animation()) |
| return; |
| |
| // In case we are running accelerated, we want to use a live, non-cached current time so that any geometry |
| // computation that may rely on this computed style has the most current information. Indeed, when using |
| // accelerated effects, we may not update animations in WebCore and thus will fail to have a meaningful |
| // cached current time. |
| auto useCachedCurrentTime = isRunningAccelerated() ? UseCachedCurrentTime::No : UseCachedCurrentTime::Yes; |
| auto computedTiming = getComputedTiming(useCachedCurrentTime); |
| LOG_WITH_STREAM(Animations, stream << "KeyframeEffect " << this << " getAnimatedStyle - progress " << computedTiming.progress); |
| if (!computedTiming.progress) |
| return; |
| |
| if (!animatedStyle) { |
| if (auto* style = targetStyleable()->lastStyleChangeEventStyle()) |
| animatedStyle = RenderStyle::clonePtr(*style); |
| else |
| animatedStyle = RenderStyle::clonePtr(renderer()->style()); |
| } |
| |
| ASSERT(computedTiming.currentIteration); |
| setAnimatedPropertiesInStyle(*animatedStyle.get(), computedTiming); |
| } |
| |
| void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, const ComputedEffectTiming& computedTiming) |
| { |
| ASSERT(computedTiming.progress); |
| ASSERT(computedTiming.currentIteration); |
| |
| auto iterationProgress = *computedTiming.progress; |
| auto currentIteration = *computedTiming.currentIteration; |
| auto before = computedTiming.before; |
| |
| auto& properties = m_blendingKeyframes.properties(); |
| |
| // In the case of CSS Transitions we already know that there are only two keyframes, one where offset=0 and one where offset=1, |
| // and only a single CSS property so we can simply blend based on the style available on those keyframes with the provided iteration |
| // progress which already accounts for the transition's timing function. |
| if (m_animationType == WebAnimationType::CSSTransition) { |
| ASSERT(properties.size() == 1); |
| Style::Interpolation::interpolate(*properties.begin(), targetStyle, *m_blendingKeyframes[0].style(), *m_blendingKeyframes[1].style(), iterationProgress, m_compositeOperation, *this); |
| return; |
| } |
| |
| // 4.4.3. The effect value of a keyframe effect |
| // https://drafts.csswg.org/web-animations-1/#the-effect-value-of-a-keyframe-animation-effect |
| // |
| // The effect value of a single property referenced by a keyframe effect as one of its target properties, |
| // for a given iteration progress, current iteration and underlying value is calculated as follows. |
| |
| updateBlendingKeyframes(targetStyle, { nullptr }); |
| if (m_blendingKeyframes.isEmpty()) |
| return; |
| |
| BlendingKeyframe propertySpecificKeyframeWithZeroOffset(0, RenderStyle::clonePtr(targetStyle)); |
| BlendingKeyframe propertySpecificKeyframeWithOneOffset(1, RenderStyle::clonePtr(targetStyle)); |
| |
| for (auto property : properties) { |
| auto interval = interpolationKeyframes(property, iterationProgress, propertySpecificKeyframeWithZeroOffset, propertySpecificKeyframeWithOneOffset); |
| if (interval.endpoints.isEmpty()) |
| continue; |
| |
| auto* startBlendingKeyframe = dynamicDowncast<BlendingKeyframe>(interval.endpoints.first()); |
| auto* endBlendingKeyframe = dynamicDowncast<BlendingKeyframe>(interval.endpoints.last()); |
| |
| if (!startBlendingKeyframe || !endBlendingKeyframe) { |
| ASSERT_NOT_REACHED(); |
| continue; |
| } |
| |
| auto startKeyframeStyle = RenderStyle::clone(*startBlendingKeyframe->style()); |
| auto endKeyframeStyle = RenderStyle::clone(*endBlendingKeyframe->style()); |
| |
| KeyframeInterpolation::CompositionCallback composeProperty = [&] (const KeyframeInterpolation::Keyframe& keyframe, CompositeOperation compositeOperation) { |
| auto* blendingKeyframe = dynamicDowncast<BlendingKeyframe>(keyframe); |
| if (!blendingKeyframe) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (blendingKeyframe->offset() == startBlendingKeyframe->offset()) |
| Style::Interpolation::interpolate(property, startKeyframeStyle, targetStyle, *blendingKeyframe->style(), 1, compositeOperation, *this); |
| else |
| Style::Interpolation::interpolate(property, endKeyframeStyle, targetStyle, *blendingKeyframe->style(), 1, compositeOperation, *this); |
| }; |
| |
| KeyframeInterpolation::AccumulationCallback accumulateProperty = [&](const KeyframeInterpolation::Keyframe& keyframe) { |
| auto* blendingKeyframe = dynamicDowncast<BlendingKeyframe>(keyframe); |
| if (!blendingKeyframe) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (blendingKeyframe->offset() == startBlendingKeyframe->offset()) |
| Style::Interpolation::interpolate(property, startKeyframeStyle, *endBlendingKeyframe->style(), startKeyframeStyle, 1, CompositeOperation::Accumulate, *this); |
| else |
| Style::Interpolation::interpolate(property, endKeyframeStyle, *endBlendingKeyframe->style(), endKeyframeStyle, 1, CompositeOperation::Accumulate, *this); |
| }; |
| |
| KeyframeInterpolation::InterpolationCallback interpolateProperty = [&](double intervalProgress, double currentIteration, IterationCompositeOperation iterationCompositeOperation) { |
| Style::Interpolation::interpolate(property, targetStyle, startKeyframeStyle, endKeyframeStyle, intervalProgress, CompositeOperation::Replace, iterationCompositeOperation, currentIteration, *this); |
| }; |
| |
| KeyframeInterpolation::RequiresInterpolationForAccumulativeIterationCallback requiresInterpolationForAccumulativeIterationCallback = [&]() { |
| return Style::Interpolation::requiresInterpolationForAccumulativeIteration(property, startKeyframeStyle, endKeyframeStyle, *this); |
| }; |
| |
| interpolateKeyframes(property, interval, iterationProgress, currentIteration, iterationDuration(), before, composeProperty, accumulateProperty, interpolateProperty, requiresInterpolationForAccumulativeIterationCallback); |
| } |
| |
| // In case one of the animated properties has its value set to "inherit" in one of the keyframes, |
| // let's mark the resulting animated style as having an explicitly inherited property such that |
| // a future style update accounts for this in a future call to TreeResolver::determineResolutionType(). |
| if (m_blendingKeyframes.hasExplicitlyInheritedKeyframeProperty()) |
| targetStyle.setHasExplicitlyInheritedProperties(); |
| } |
| |
| const TimingFunction* KeyframeEffect::timingFunctionForBlendingKeyframe(const BlendingKeyframe& keyframe) const |
| { |
| if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation())) { |
| // If we're dealing with a CSS Animation, the timing function is specified either on the keyframe itself. |
| if (is<CSSAnimation>(styleOriginatedAnimation)) { |
| if (auto* timingFunction = keyframe.timingFunction()) |
| return timingFunction; |
| } |
| |
| // Failing that, or for a CSS Transition, the timing function is inherited from the backing Animation object. |
| return styleOriginatedAnimation->backingAnimation().timingFunction(); |
| } |
| |
| return keyframe.timingFunction(); |
| } |
| |
| const TimingFunction* KeyframeEffect::timingFunctionForKeyframeAtIndex(size_t index) const |
| { |
| if (!m_parsedKeyframes.isEmpty()) { |
| if (index >= m_parsedKeyframes.size()) |
| return nullptr; |
| return m_parsedKeyframes[index].timingFunction.get(); |
| } |
| |
| if (index >= m_blendingKeyframes.size()) |
| return nullptr; |
| return timingFunctionForBlendingKeyframe(m_blendingKeyframes[index]); |
| } |
| |
| bool KeyframeEffect::canBeAccelerated() const |
| { |
| if (!animation()) |
| return false; |
| |
| if (m_acceleratedPropertiesState == AcceleratedProperties::None) |
| return false; |
| |
| if (m_isAssociatedWithProgressBasedTimeline) |
| return false; |
| |
| if (m_hasAcceleratedPropertyOverriddenByCascadeProperty) |
| return false; |
| |
| if (m_hasReferenceFilter) |
| return false; |
| |
| if (m_animatesSizeAndSizeDependentTransform) |
| return false; |
| |
| if (m_blendingKeyframes.hasDiscreteTransformInterval()) |
| return false; |
| |
| if (RefPtr document = this->document()) { |
| if (document->quirks().shouldPreventKeyframeEffectAcceleration(*this)) |
| return false; |
| } |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return true; |
| #endif |
| |
| if (m_someKeyframesUseStepsTimingFunction || is<StepsTimingFunction>(timingFunction())) |
| return false; |
| |
| if (m_someKeyframesUseLinearTimingFunctionWithPoints || isLinearTimingFunctionWithPoints(timingFunction())) |
| return false; |
| |
| if (m_compositeOperation != CompositeOperation::Replace) |
| return false; |
| |
| if (m_hasKeyframeComposingAcceleratedProperty) |
| return false; |
| |
| return true; |
| } |
| |
| bool KeyframeEffect::preventsAcceleration() const |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return false; |
| #endif |
| |
| // We cannot run accelerated transform animations if a motion path is applied |
| // to an element, either through the underlying style, or through a keyframe. |
| if (auto target = targetStyleable()) { |
| if (auto* lastStyleChangeEventStyle = target->lastStyleChangeEventStyle()) { |
| if (lastStyleChangeEventStyle->hasOffsetPath()) |
| return true; |
| } |
| } |
| |
| if (animatesProperty(CSSPropertyOffsetAnchor) |
| || animatesProperty(CSSPropertyOffsetDistance) |
| || animatesProperty(CSSPropertyOffsetPath) |
| || animatesProperty(CSSPropertyOffsetPosition) |
| || animatesProperty(CSSPropertyOffsetRotate)) { |
| return true; |
| } |
| |
| if (m_acceleratedPropertiesState == AcceleratedProperties::None) |
| return false; |
| |
| return !canBeAccelerated() || m_runningAccelerated == RunningAccelerated::Failed; |
| } |
| |
| void KeyframeEffect::updateAcceleratedActions() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return; |
| #endif |
| |
| auto* renderer = this->renderer(); |
| if (!renderer || !renderer->isComposited()) |
| return; |
| |
| if (!canBeAccelerated()) |
| return; |
| |
| auto computedTiming = getComputedTiming(); |
| |
| // If we're not already running accelerated, the only thing we're interested in is whether we need to start the animation |
| // which we need to do once we're in the active phase. Otherwise, there's no change in accelerated state to consider. |
| bool isActive = computedTiming.phase == AnimationEffectPhase::Active; |
| if (m_runningAccelerated == RunningAccelerated::NotStarted) { |
| if (isActive && animation()->playState() == WebAnimation::PlayState::Running) |
| addPendingAcceleratedAction(AcceleratedAction::Play); |
| return; |
| } |
| |
| // If we're no longer active, we need to remove the accelerated animation. |
| if (!isActive) { |
| addPendingAcceleratedAction(AcceleratedAction::Stop); |
| return; |
| } |
| |
| auto playState = animation()->playState(); |
| // The only thing left to consider is whether we need to pause or resume the animation following a change of play-state. |
| if (playState == WebAnimation::PlayState::Paused) { |
| if (m_lastRecordedAcceleratedAction != AcceleratedAction::Pause) { |
| if (m_lastRecordedAcceleratedAction == AcceleratedAction::Stop) |
| addPendingAcceleratedAction(AcceleratedAction::Play); |
| addPendingAcceleratedAction(AcceleratedAction::Pause); |
| } |
| } else if (playState == WebAnimation::PlayState::Running && isActive) { |
| if (m_lastRecordedAcceleratedAction != AcceleratedAction::Play) |
| addPendingAcceleratedAction(AcceleratedAction::Play); |
| } |
| } |
| |
| void KeyframeEffect::addPendingAcceleratedAction(AcceleratedAction action) |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return; |
| #endif |
| |
| if (m_runningAccelerated == RunningAccelerated::Prevented || m_runningAccelerated == RunningAccelerated::Failed) |
| return; |
| |
| if (action == m_lastRecordedAcceleratedAction) |
| return; |
| |
| if (action == AcceleratedAction::Stop) |
| m_pendingAcceleratedActions.clear(); |
| m_pendingAcceleratedActions.append(action); |
| if (action != AcceleratedAction::UpdateProperties && action != AcceleratedAction::TransformChange) |
| m_lastRecordedAcceleratedAction = action; |
| animation()->acceleratedStateDidChange(); |
| } |
| |
| void KeyframeEffect::animationDidTick() |
| { |
| invalidate(); |
| updateAcceleratedActions(); |
| |
| if (RefPtr viewTimeline = activeViewTimeline()) |
| computeMissingKeyframeOffsets(m_parsedKeyframes, viewTimeline.get(), animation()); |
| } |
| |
| void KeyframeEffect::animationDidChangeTimingProperties() |
| { |
| computeSomeKeyframesUseStepsOrLinearTimingFunctionWithPoints(); |
| updateAcceleratedAnimationIfNecessary(); |
| invalidate(); |
| } |
| |
| void KeyframeEffect::updateAcceleratedAnimationIfNecessary() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| if (canBeAccelerated()) |
| updateAssociatedThreadedEffectStack(); |
| return; |
| } |
| #endif |
| |
| if (isRunningAccelerated() || isAboutToRunAccelerated()) { |
| if (canBeAccelerated()) |
| addPendingAcceleratedAction(AcceleratedAction::UpdateProperties); |
| else { |
| abilityToBeAcceleratedDidChange(); |
| addPendingAcceleratedAction(AcceleratedAction::Stop); |
| } |
| } else if (canBeAccelerated()) |
| m_runningAccelerated = RunningAccelerated::NotStarted; |
| } |
| |
| void KeyframeEffect::animationDidFinish() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| updateAcceleratedAnimationIfNecessary(); |
| #endif |
| } |
| |
| void KeyframeEffect::animationPlaybackRateDidChange() |
| { |
| AnimationEffect::animationPlaybackRateDidChange(); |
| |
| updateAcceleratedAnimationIfNecessary(); |
| } |
| |
| void KeyframeEffect::transformRelatedPropertyDidChange() |
| { |
| ASSERT(isRunningAcceleratedTransformRelatedAnimation()); |
| auto hasTransformRelatedPropertyWithImplicitKeyframe = propertiesContainTransformRelatedProperty(m_acceleratedPropertiesWithImplicitKeyframe); |
| addPendingAcceleratedAction(hasTransformRelatedPropertyWithImplicitKeyframe ? AcceleratedAction::UpdateProperties : AcceleratedAction::TransformChange); |
| } |
| |
| std::optional<KeyframeEffect::RecomputationReason> KeyframeEffect::recomputeKeyframesIfNecessary(const RenderStyle* previousUnanimatedStyle, const RenderStyle& unanimatedStyle, const Style::ResolutionContext& resolutionContext) |
| { |
| if (m_animationType == WebAnimationType::CSSTransition) |
| return { }; |
| |
| auto fontSizeChanged = [&]() { |
| return previousUnanimatedStyle && previousUnanimatedStyle->computedFontSize() != unanimatedStyle.computedFontSize(); |
| }; |
| |
| auto fontWeightChanged = [&]() { |
| return m_blendingKeyframes.usesRelativeFontWeight() && previousUnanimatedStyle |
| && previousUnanimatedStyle->fontWeight() != unanimatedStyle.fontWeight(); |
| }; |
| |
| auto cssVariableChanged = [&]() { |
| if (previousUnanimatedStyle && m_blendingKeyframes.hasCSSVariableReferences()) { |
| if (!previousUnanimatedStyle->customPropertiesEqual(unanimatedStyle)) |
| return true; |
| } |
| return false; |
| }; |
| |
| auto hasPropertyExplicitlySetToInherit = [&]() { |
| return !m_blendingKeyframes.propertiesSetToInherit().isEmpty(); |
| }; |
| |
| auto propertySetToCurrentColorChanged = [&]() { |
| // If the "color" property itself is set to "currentcolor" on a keyframe, we always recompute keyframes. |
| if (m_blendingKeyframes.hasColorSetToCurrentColor()) |
| return true; |
| // For all other color-related properties set to "currentcolor" on a keyframe, it's sufficient to check |
| // whether the value "color" resolves to has changed since the last style resolution. |
| return m_blendingKeyframes.hasPropertySetToCurrentColor() && previousUnanimatedStyle |
| && previousUnanimatedStyle->color() != unanimatedStyle.color(); |
| }; |
| |
| auto logicalPropertyChanged = [&]() { |
| if (!previousUnanimatedStyle) |
| return false; |
| |
| if (previousUnanimatedStyle->writingMode() == unanimatedStyle.writingMode()) |
| return false; |
| |
| if (!m_blendingKeyframes.isEmpty()) |
| return m_blendingKeyframes.containsDirectionAwareProperty(); |
| |
| for (auto& keyframe : m_parsedKeyframes) { |
| for (auto property : keyframe.styleStrings.keys()) { |
| if (CSSProperty::isDirectionAwareProperty(property)) |
| return true; |
| } |
| } |
| |
| return false; |
| }(); |
| |
| auto usesAnchorFunctions = m_blendingKeyframes.usesAnchorFunctions(); |
| |
| if (logicalPropertyChanged || fontSizeChanged() || fontWeightChanged() || cssVariableChanged() || hasPropertyExplicitlySetToInherit() || propertySetToCurrentColorChanged() || usesAnchorFunctions) { |
| switch (m_animationType) { |
| case WebAnimationType::CSSTransition: |
| ASSERT_NOT_REACHED(); |
| break; |
| case WebAnimationType::CSSAnimation: |
| computeCSSAnimationBlendingKeyframes(unanimatedStyle, resolutionContext); |
| break; |
| case WebAnimationType::WebAnimation: |
| clearBlendingKeyframes(); |
| break; |
| } |
| |
| return logicalPropertyChanged ? KeyframeEffect::RecomputationReason::LogicalPropertyChange : KeyframeEffect::RecomputationReason::Other; |
| } |
| |
| return { }; |
| } |
| |
| void KeyframeEffect::animationWasCanceled() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| updateAcceleratedAnimationIfNecessary(); |
| return; |
| } |
| #endif |
| |
| if (isRunningAccelerated() || isAboutToRunAccelerated()) |
| addPendingAcceleratedAction(AcceleratedAction::Stop); |
| } |
| |
| void KeyframeEffect::wasAddedToEffectStack() |
| { |
| m_inTargetEffectStack = true; |
| invalidate(); |
| } |
| |
| void KeyframeEffect::wasRemovedFromEffectStack() |
| { |
| m_inTargetEffectStack = false; |
| |
| if (!canBeAccelerated()) |
| return; |
| |
| #if ENABLE(THREADED_ANIMATIONS) |
| if (canHaveAcceleratedRepresentation()) |
| return; |
| #endif |
| |
| // If the effect was running accelerated, we need to mark it for removal straight away |
| // since it will not be invalidated by a future call to KeyframeEffectStack::applyPendingAcceleratedActions(). |
| ASSERT(animation()); |
| if (isRunningAccelerated() || isAboutToRunAccelerated()) { |
| Ref animation = *this->animation(); |
| bool isFinishingNaturally = animation->hasPendingFinishNotification() || animation->playState() == WebAnimation::PlayState::Finished; |
| |
| m_pendingAcceleratedActions.clear(); |
| m_pendingAcceleratedActions.append(AcceleratedAction::Stop); |
| |
| if (isFinishingNaturally) { |
| // Don't immediately stop animations that are finishing naturally - delay cleanup via microtask |
| // to allow the finished promise callback to observe the final animation state (e.g., layer tree). |
| // Only immediately stop animations removed mid-flight. |
| if (RefPtr context = animation->scriptExecutionContext()) { |
| context->eventLoop().queueMicrotask([protectedThis = Ref { *this }] { |
| protectedThis->applyPendingAcceleratedActions(); |
| }); |
| } |
| } else |
| applyPendingAcceleratedActions(); |
| } |
| } |
| |
| void KeyframeEffect::willChangeRenderer() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| updateAcceleratedAnimationIfNecessary(); |
| return; |
| } |
| #endif |
| |
| if (isRunningAccelerated() || isAboutToRunAccelerated()) |
| addPendingAcceleratedAction(AcceleratedAction::Stop); |
| } |
| |
| void KeyframeEffect::animationSuspensionStateDidChange(bool animationIsSuspended) |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| updateAssociatedThreadedEffectStack(); |
| return; |
| } |
| #endif |
| |
| if (isRunningAccelerated() || isAboutToRunAccelerated()) |
| addPendingAcceleratedAction(animationIsSuspended ? AcceleratedAction::Pause : AcceleratedAction::Play); |
| } |
| |
| void KeyframeEffect::applyPendingAcceleratedActionsOrUpdateTimingProperties() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return; |
| #endif |
| |
| if (m_pendingAcceleratedActions.isEmpty()) { |
| if (!canBeAccelerated() || getComputedTiming().phase != AnimationEffectPhase::Active) |
| return; |
| m_pendingAcceleratedActions.append(AcceleratedAction::UpdateProperties); |
| m_lastRecordedAcceleratedAction = AcceleratedAction::Play; |
| applyPendingAcceleratedActions(); |
| m_pendingAcceleratedActions.clear(); |
| } else |
| applyPendingAcceleratedActions(); |
| } |
| |
| void KeyframeEffect::applyPendingAcceleratedActions() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return; |
| #endif |
| |
| CanBeAcceleratedMutationScope mutationScope(this); |
| |
| // Once an accelerated animation has been committed, we no longer want to force a layout. |
| // This should have been performed by a call to forceLayoutIfNeeded() prior to applying |
| // pending accelerated actions. |
| m_needsForcedLayout = false; |
| |
| if (m_pendingAcceleratedActions.isEmpty()) |
| return; |
| |
| auto* renderer = this->renderer(); |
| if (!renderer || !renderer->isComposited()) { |
| // The renderer may no longer be composited because the accelerated animation ended before we had a chance to update it, |
| // in which case if we asked for the animation to stop, we can discard the current set of accelerated actions. |
| if (m_lastRecordedAcceleratedAction == AcceleratedAction::Stop) { |
| m_pendingAcceleratedActions.clear(); |
| m_runningAccelerated = RunningAccelerated::NotStarted; |
| } |
| return; |
| } |
| |
| auto pendingAcceleratedActions = m_pendingAcceleratedActions; |
| m_pendingAcceleratedActions.clear(); |
| |
| auto timeOffset = [&] { |
| // To simplify the code we use a default of 0s for an unresolved current time since for a Stop action that is acceptable. |
| auto cssNumberishTimeOffset = animation()->currentTime().value_or(0_s) - delay(); |
| ASSERT(cssNumberishTimeOffset.time()); |
| return cssNumberishTimeOffset.time()->seconds(); |
| }; |
| |
| auto startAnimation = [&]() -> RunningAccelerated { |
| if (isRunningAccelerated()) |
| renderer->animationFinished(m_blendingKeyframes); |
| |
| ASSERT(m_target); |
| auto* effectStack = m_target->keyframeEffectStack(m_pseudoElementIdentifier); |
| ASSERT(effectStack); |
| |
| if ((m_blendingKeyframes.hasWidthDependentTransform() && effectStack->containsProperty(CSSPropertyWidth)) |
| || (m_blendingKeyframes.hasHeightDependentTransform() && effectStack->containsProperty(CSSPropertyHeight))) |
| return RunningAccelerated::Prevented; |
| |
| if (!effectStack->allowsAcceleration()) |
| return RunningAccelerated::Prevented; |
| |
| if (!m_hasImplicitKeyframeForAcceleratedProperty) |
| return renderer->startAnimation(timeOffset(), backingAnimationForCompositedRenderer(), m_blendingKeyframes) ? RunningAccelerated::Yes : RunningAccelerated::Failed; |
| |
| // We need to resolve all animations up to this point to ensure any forward-filling |
| // effect is accounted for when computing the "from" value for the accelerated animation. |
| auto underlyingStyle = [&]() { |
| if (auto* lastStyleChangeEventStyle = m_target->lastStyleChangeEventStyle(m_pseudoElementIdentifier)) |
| return RenderStyle::clonePtr(*lastStyleChangeEventStyle); |
| return RenderStyle::clonePtr(renderer->style()); |
| }(); |
| |
| for (const auto& effect : effectStack->sortedEffects()) { |
| if (this == effect.get()) |
| break; |
| auto computedTiming = effect->getComputedTiming(); |
| if (computedTiming.progress) |
| effect->setAnimatedPropertiesInStyle(*underlyingStyle, computedTiming); |
| } |
| |
| BlendingKeyframes explicitKeyframes(m_blendingKeyframes.identifier()); |
| explicitKeyframes.copyKeyframes(m_blendingKeyframes); |
| explicitKeyframes.fillImplicitKeyframes(*this, *underlyingStyle); |
| return renderer->startAnimation(timeOffset(), backingAnimationForCompositedRenderer(), explicitKeyframes) ? RunningAccelerated::Yes : RunningAccelerated::Failed; |
| }; |
| |
| for (const auto& action : pendingAcceleratedActions) { |
| switch (action) { |
| case AcceleratedAction::Play: |
| m_runningAccelerated = startAnimation(); |
| LOG_WITH_STREAM(Animations, stream << "KeyframeEffect " << this << " applyPendingAcceleratedActions " << m_blendingKeyframes.acceleratedAnimationName() << " Play, started accelerated: " << isRunningAccelerated()); |
| if (!isRunningAccelerated()) { |
| m_lastRecordedAcceleratedAction = AcceleratedAction::Stop; |
| return; |
| } |
| break; |
| case AcceleratedAction::Pause: |
| renderer->animationPaused(timeOffset(), m_blendingKeyframes); |
| break; |
| case AcceleratedAction::UpdateProperties: |
| m_runningAccelerated = startAnimation(); |
| LOG_WITH_STREAM(Animations, stream << "KeyframeEffect " << this << " applyPendingAcceleratedActions " << m_blendingKeyframes.acceleratedAnimationName() << " UpdateProperties, started accelerated: " << isRunningAccelerated()); |
| if (animation()->playState() == WebAnimation::PlayState::Paused) |
| renderer->animationPaused(timeOffset(), m_blendingKeyframes); |
| break; |
| case AcceleratedAction::Stop: |
| ASSERT(document()); |
| renderer->animationFinished(m_blendingKeyframes); |
| if (!document()->renderTreeBeingDestroyed()) |
| m_target->invalidateStyleAndLayerComposition(); |
| m_runningAccelerated = canBeAccelerated() ? RunningAccelerated::NotStarted : RunningAccelerated::Prevented; |
| break; |
| case AcceleratedAction::TransformChange: |
| renderer->transformRelatedPropertyDidChange(); |
| break; |
| } |
| } |
| } |
| |
| Ref<const Animation> KeyframeEffect::backingAnimationForCompositedRenderer() |
| { |
| Ref effectAnimation = *animation(); |
| |
| // FIXME: The iterationStart and endDelay AnimationEffectTiming properties do not have |
| // corresponding Animation properties. |
| auto animation = Animation::create(); |
| animation->setDuration(iterationDuration().time()->seconds()); |
| animation->setDelay(delay().time()->seconds()); |
| animation->setIterationCount(iterations()); |
| animation->setTimingFunction(timingFunction()->clone()); |
| animation->setPlaybackRate(effectAnimation->playbackRate()); |
| animation->setCompositeOperation(m_compositeOperation); |
| |
| switch (fill()) { |
| case FillMode::None: |
| case FillMode::Auto: |
| animation->setFillMode(AnimationFillMode::None); |
| break; |
| case FillMode::Backwards: |
| animation->setFillMode(AnimationFillMode::Backwards); |
| break; |
| case FillMode::Forwards: |
| animation->setFillMode(AnimationFillMode::Forwards); |
| break; |
| case FillMode::Both: |
| animation->setFillMode(AnimationFillMode::Both); |
| break; |
| } |
| |
| switch (direction()) { |
| case PlaybackDirection::Normal: |
| animation->setDirection(Animation::Direction::Normal); |
| break; |
| case PlaybackDirection::Alternate: |
| animation->setDirection(Animation::Direction::Alternate); |
| break; |
| case PlaybackDirection::Reverse: |
| animation->setDirection(Animation::Direction::Reverse); |
| break; |
| case PlaybackDirection::AlternateReverse: |
| animation->setDirection(Animation::Direction::AlternateReverse); |
| break; |
| } |
| |
| // In the case of CSS Animations, we must set the default timing function for keyframes to match |
| // the current value set for animation-timing-function on the target element which affects only |
| // keyframes and not the animation-wide timing. |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(effectAnimation)) |
| animation->setDefaultTimingFunctionForKeyframes(cssAnimation->backingAnimation().timingFunction()); |
| |
| return animation; |
| } |
| |
| Document* KeyframeEffect::document() const |
| { |
| if (m_document) |
| return m_document.get(); |
| return m_target ? &m_target->document() : nullptr; |
| } |
| |
| RenderElement* KeyframeEffect::renderer() const |
| { |
| if (auto target = targetStyleable()) |
| return target->renderer(); |
| return nullptr; |
| } |
| |
| const RenderStyle& KeyframeEffect::currentStyle() const |
| { |
| if (auto* renderer = this->renderer()) |
| return renderer->style(); |
| return RenderStyle::defaultStyleSingleton(); |
| } |
| |
| bool KeyframeEffect::computeExtentOfTransformAnimation(LayoutRect& bounds) const |
| { |
| ASSERT(m_blendingKeyframes.containsProperty(CSSPropertyTransform)); |
| |
| auto* box = dynamicDowncast<RenderBox>(renderer()); |
| if (!box) |
| return true; // Non-boxes don't get transformed; |
| |
| auto rendererBox = snapRectToDevicePixels(box->borderBoxRect(), box->document().deviceScaleFactor()); |
| |
| LayoutRect cumulativeBounds; |
| |
| auto* implicitStyle = [&]() { |
| if (auto target = targetStyleable()) { |
| if (auto* lastStyleChangeEventStyle = target->lastStyleChangeEventStyle()) |
| return lastStyleChangeEventStyle; |
| } |
| return &box->style(); |
| }(); |
| |
| auto addStyleToCumulativeBounds = [&](const RenderStyle* style) -> bool { |
| auto keyframeBounds = bounds; |
| |
| bool canCompute; |
| if (transformFunctionListPrefix() > 0) |
| canCompute = computeTransformedExtentViaTransformList(rendererBox, *style, keyframeBounds); |
| else |
| canCompute = computeTransformedExtentViaMatrix(rendererBox, *style, keyframeBounds); |
| |
| if (!canCompute) |
| return false; |
| |
| cumulativeBounds.unite(keyframeBounds); |
| return true; |
| }; |
| |
| for (const auto& keyframe : m_blendingKeyframes) { |
| const auto* keyframeStyle = keyframe.style(); |
| |
| // FIXME: maybe for style-originated animations we always say it's true for the first and last keyframe. |
| if (!keyframe.animatesProperty(CSSPropertyTransform)) { |
| // If the first keyframe is missing transform style, use the current style. |
| if (!keyframe.offset()) |
| keyframeStyle = implicitStyle; |
| else |
| continue; |
| } |
| |
| if (!addStyleToCumulativeBounds(keyframeStyle)) |
| return false; |
| } |
| |
| if (m_blendingKeyframes.hasImplicitKeyframes()) { |
| if (!addStyleToCumulativeBounds(implicitStyle)) |
| return false; |
| } |
| |
| bounds = cumulativeBounds; |
| return true; |
| } |
| |
| bool KeyframeEffect::computeTransformedExtentViaTransformList(const FloatRect& rendererBox, const RenderStyle& style, LayoutRect& bounds) const |
| { |
| FloatRect floatBounds = bounds; |
| FloatPoint transformOrigin; |
| |
| bool applyTransformOrigin = style.transform().hasTransformOfType<TransformOperation::Type::Rotate>() || style.transform().affectedByTransformOrigin(); |
| if (applyTransformOrigin) { |
| transformOrigin = style.computeTransformOrigin(rendererBox).xy(); |
| // Ignore transformOriginZ because we'll bail if we encounter any 3D transforms. |
| floatBounds.moveBy(-transformOrigin); |
| } |
| |
| for (const auto& operation : style.transform()) { |
| if (operation->type() == TransformOperation::Type::Rotate) { |
| // For now, just treat this as a full rotation. This could take angle into account to reduce inflation. |
| floatBounds = boundsOfRotatingRect(floatBounds); |
| } else { |
| TransformationMatrix transform; |
| operation->apply(transform, rendererBox.size()); |
| if (!transform.isAffine()) |
| return false; |
| |
| if (operation->type() == TransformOperation::Type::Matrix || operation->type() == TransformOperation::Type::Matrix3D) { |
| TransformationMatrix::Decomposed2Type toDecomp; |
| // Any rotation prevents us from using a simple start/end rect union. |
| if (!transform.decompose2(toDecomp) || toDecomp.angle) |
| return false; |
| } |
| |
| floatBounds = transform.mapRect(floatBounds); |
| } |
| } |
| |
| if (applyTransformOrigin) |
| floatBounds.moveBy(transformOrigin); |
| |
| bounds = LayoutRect(floatBounds); |
| return true; |
| } |
| |
| bool KeyframeEffect::computeTransformedExtentViaMatrix(const FloatRect& rendererBox, const RenderStyle& style, LayoutRect& bounds) const |
| { |
| TransformationMatrix transform; |
| style.applyTransform(transform, TransformOperationData(rendererBox, renderer())); |
| if (!transform.isAffine()) |
| return false; |
| |
| TransformationMatrix::Decomposed2Type fromDecomp; |
| // Any rotation prevents us from using a simple start/end rect union. |
| if (!transform.decompose2(fromDecomp) || fromDecomp.angle) |
| return false; |
| |
| bounds = LayoutRect(transform.mapRect(bounds)); |
| return true; |
| } |
| |
| bool KeyframeEffect::requiresPseudoElement() const |
| { |
| return m_animationType == WebAnimationType::WebAnimation && targetsPseudoElement(); |
| } |
| |
| std::optional<double> KeyframeEffect::progressUntilNextStep(double iterationProgress) const |
| { |
| ASSERT(iterationProgress >= 0 && iterationProgress <= 1); |
| |
| if (auto progress = AnimationEffect::progressUntilNextStep(iterationProgress)) |
| return progress; |
| |
| if (!is<LinearTimingFunction>(timingFunction()) || !m_someKeyframesUseStepsTimingFunction) |
| return std::nullopt; |
| |
| if (m_blendingKeyframes.isEmpty()) |
| return std::nullopt; |
| |
| auto progressUntilNextStepInInterval = [iterationProgress](double intervalStartProgress, double intervalEndProgress, const TimingFunction* timingFunction) -> std::optional<double> { |
| auto* stepsTimingFunction = dynamicDowncast<StepsTimingFunction>(timingFunction); |
| if (!stepsTimingFunction) |
| return std::nullopt; |
| |
| auto numberOfSteps = stepsTimingFunction->numberOfSteps(); |
| auto intervalProgress = intervalEndProgress - intervalStartProgress; |
| auto iterationProgressMappedToCurrentInterval = (iterationProgress - intervalStartProgress) / intervalProgress; |
| auto nextStepProgress = ceil(iterationProgressMappedToCurrentInterval * numberOfSteps) / numberOfSteps; |
| return (nextStepProgress - iterationProgressMappedToCurrentInterval) * intervalProgress; |
| }; |
| |
| for (size_t i = 0; i < m_blendingKeyframes.size(); ++i) { |
| auto intervalEndProgress = m_blendingKeyframes[i].offset(); |
| // We can stop once we find a keyframe for which the progress is more than the provided iteration progress. |
| if (intervalEndProgress <= iterationProgress) |
| continue; |
| |
| // In case we're on the first keyframe, then this means we are dealing with an implicit 0% keyframe. |
| // This will be a linear timing function unless we're dealing with a CSS Animation which might have |
| // the default timing function for its keyframes defined on its backing Animation object. |
| if (!i) { |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) |
| return progressUntilNextStepInInterval(0, intervalEndProgress, cssAnimation->backingAnimation().timingFunction()); |
| return std::nullopt; |
| } |
| |
| return progressUntilNextStepInInterval(m_blendingKeyframes[i - 1].offset(), intervalEndProgress, timingFunctionForKeyframeAtIndex(i - 1)); |
| } |
| |
| // If we end up here, then this means we are dealing with an implicit 100% keyframe. |
| // This will be a linear timing function unless we're dealing with a CSS Animation which might have |
| // the default timing function for its keyframes defined on its backing Animation object. |
| auto& lastExplicitKeyframe = m_blendingKeyframes[m_blendingKeyframes.size() - 1]; |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) |
| return progressUntilNextStepInInterval(lastExplicitKeyframe.offset(), 1, cssAnimation->backingAnimation().timingFunction()); |
| |
| // In any other case, we are not dealing with an interval with a steps() timing function. |
| return std::nullopt; |
| } |
| |
| bool KeyframeEffect::ticksContinuouslyWhileActive() const |
| { |
| auto doesNotAffectStyles = m_blendingKeyframes.isEmpty() || m_blendingKeyframes.properties().isEmpty(); |
| if (doesNotAffectStyles) |
| return false; |
| |
| auto targetHasDisplayContents = [&]() { |
| return m_target && !m_pseudoElementIdentifier && m_target->hasDisplayContents(); |
| }; |
| if (!renderer() && !m_blendingKeyframes.properties().contains(CSSPropertyDisplay) && !targetHasDisplayContents()) |
| return false; |
| |
| if (isCompletelyAccelerated() && isRunningAccelerated()) { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| return !m_acceleratedRepresentation || !m_acceleratedRepresentation->disallowedProperties().isEmpty(); |
| #endif |
| return false; |
| } |
| |
| return true; |
| } |
| |
| Seconds KeyframeEffect::timeToNextTick(const BasicEffectTiming& timing) |
| { |
| // CSS Animations need to trigger "animationiteration" events even if there is no need to |
| // update styles while animating, so if we're dealing with one we must wait until the next iteration. |
| // We only do this in case any CSS Animation event was registered since, in the general case, there's |
| // a good chance that no such event listeners were registered and we can avoid some unnecessary |
| // animation resolution scheduling. |
| ASSERT(document()); |
| if (timing.phase == AnimationEffectPhase::Active && is<CSSAnimation>(animation()) |
| && document()->hasListenerType(Document::ListenerType::CSSAnimation) |
| && !ticksContinuouslyWhileActive()) { |
| if (auto iterationProgress = getComputedTiming().simpleIterationProgress) |
| return iterationDuration() * (1 - *iterationProgress); |
| } |
| |
| return AnimationEffect::timeToNextTick(timing); |
| } |
| |
| void KeyframeEffect::setIterationComposite(IterationCompositeOperation iterationCompositeOperation) |
| { |
| if (m_iterationCompositeOperation == iterationCompositeOperation) |
| return; |
| |
| m_iterationCompositeOperation = iterationCompositeOperation; |
| invalidate(); |
| } |
| |
| void KeyframeEffect::setComposite(CompositeOperation compositeOperation) |
| { |
| if (m_compositeOperation == compositeOperation) |
| return; |
| |
| CanBeAcceleratedMutationScope mutationScope(this); |
| m_compositeOperation = compositeOperation; |
| invalidate(); |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) |
| updateAcceleratedAnimationIfNecessary(); |
| #endif |
| } |
| |
| CompositeOperation KeyframeEffect::bindingsComposite() const |
| { |
| if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation())) |
| styleOriginatedAnimation->flushPendingStyleChanges(); |
| return composite(); |
| } |
| |
| void KeyframeEffect::setBindingsComposite(CompositeOperation compositeOperation) |
| { |
| setComposite(compositeOperation); |
| if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation())) |
| cssAnimation->effectCompositeOperationWasSetUsingBindings(); |
| } |
| |
| void KeyframeEffect::computeHasImplicitKeyframeForAcceleratedProperty() |
| { |
| m_hasImplicitKeyframeForAcceleratedProperty = [&]() { |
| if (m_acceleratedPropertiesState == AcceleratedProperties::None) |
| return false; |
| |
| ASSERT(document()); |
| auto& settings = document()->settings(); |
| |
| if (!m_blendingKeyframes.isEmpty()) { |
| // We make a list of all animated properties and consider them all |
| // implicit until proven otherwise as we iterate through all keyframes. |
| auto implicitZeroProperties = m_blendingKeyframes.properties(); |
| auto implicitOneProperties = m_blendingKeyframes.properties(); |
| for (auto& keyframe : m_blendingKeyframes) { |
| // If the keyframe is for 0% or 100%, let's remove all of its properties from |
| // our list of implicit properties. |
| if (!implicitZeroProperties.isEmpty() && !keyframe.offset()) { |
| for (auto property : keyframe.properties()) |
| implicitZeroProperties.remove(property); |
| } |
| if (!implicitOneProperties.isEmpty() && keyframe.offset() == 1) { |
| for (auto property : keyframe.properties()) |
| implicitOneProperties.remove(property); |
| } |
| } |
| // The only properties left are known to be implicit properties, so we must |
| // check them for any accelerated property. |
| for (auto implicitProperty : implicitZeroProperties) { |
| if (Style::Interpolation::isAccelerated(implicitProperty, settings)) |
| return true; |
| } |
| for (auto implicitProperty : implicitOneProperties) { |
| if (Style::Interpolation::isAccelerated(implicitProperty, settings)) |
| return true; |
| } |
| return false; |
| } |
| |
| // We may not have computed keyframes yet, so we should check our parsed keyframes in the |
| // same way we checked computed keyframes. |
| for (auto& keyframe : m_parsedKeyframes) { |
| // We keep three property lists, one which contains all properties seen across keyframes |
| // which will be filtered eventually to only contain implicit properties, one containing |
| // properties seen on the 0% keyframe and one containing properties seen on the 100% keyframe. |
| HashSet<CSSPropertyID> implicitProperties; |
| HashSet<CSSPropertyID> explicitZeroProperties; |
| HashSet<CSSPropertyID> explicitOneProperties; |
| auto styleProperties = keyframe.style; |
| for (auto propertyReference : styleProperties.get()) { |
| auto computedOffset = keyframe.computedOffset; |
| if (std::isnan(computedOffset)) |
| continue; |
| auto property = propertyReference.id(); |
| // All properties may end up being implicit. |
| implicitProperties.add(property); |
| if (!computedOffset) |
| explicitZeroProperties.add(property); |
| else if (computedOffset == 1) |
| explicitOneProperties.add(property); |
| } |
| // Let's remove all properties found on the 0% and 100% keyframes from the list of potential implicit properties. |
| for (auto explicitProperty : explicitZeroProperties) |
| implicitProperties.remove(explicitProperty); |
| for (auto explicitProperty : explicitOneProperties) |
| implicitProperties.remove(explicitProperty); |
| // At this point all properties left in implicitProperties are known to be implicit, |
| // so we must check them for any accelerated property. |
| for (auto implicitProperty : implicitProperties) { |
| if (Style::Interpolation::isAccelerated(implicitProperty, settings)) |
| return true; |
| } |
| } |
| return false; |
| }(); |
| } |
| |
| void KeyframeEffect::computeHasKeyframeComposingAcceleratedProperty() |
| { |
| m_hasKeyframeComposingAcceleratedProperty = [&]() { |
| if (m_acceleratedPropertiesState == AcceleratedProperties::None) |
| return false; |
| |
| ASSERT(document()); |
| auto& settings = document()->settings(); |
| |
| if (!m_blendingKeyframes.isEmpty()) { |
| for (auto& keyframe : m_blendingKeyframes) { |
| // If we find a keyframe with a composite operation, we check whether one |
| // of its properties is accelerated. |
| if (auto keyframeComposite = keyframe.compositeOperation()) { |
| if (*keyframeComposite != CompositeOperation::Replace) { |
| for (auto property : keyframe.properties()) { |
| if (Style::Interpolation::isAccelerated(property, settings)) |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| // We may not have computed keyframes yet, so we should check our parsed keyframes in the |
| // same way we checked computed keyframes. |
| for (auto& keyframe : m_parsedKeyframes) { |
| if (keyframe.composite != CompositeOperationOrAuto::Add && keyframe.composite != CompositeOperationOrAuto::Accumulate) |
| continue; |
| auto styleProperties = keyframe.style; |
| for (auto property : styleProperties.get()) { |
| if (Style::Interpolation::isAccelerated(property.id(), settings)) |
| return true; |
| } |
| } |
| return false; |
| }(); |
| } |
| |
| void KeyframeEffect::computeHasAcceleratedPropertyOverriddenByCascadeProperty() |
| { |
| if (!m_inTargetEffectStack) |
| return; |
| |
| ASSERT(m_target); |
| auto* effectStack = m_target->keyframeEffectStack(m_pseudoElementIdentifier); |
| if (!effectStack) |
| return; |
| |
| auto relevantAcceleratedPropertiesOverriddenByCascade = effectStack->acceleratedPropertiesOverriddenByCascade().intersectionWith(animatedProperties()); |
| m_hasAcceleratedPropertyOverriddenByCascadeProperty = !relevantAcceleratedPropertiesOverriddenByCascade.isEmpty(); |
| } |
| |
| void KeyframeEffect::computeHasReferenceFilter() |
| { |
| m_hasReferenceFilter = [&]() { |
| if (m_blendingKeyframes.isEmpty()) |
| return false; |
| |
| auto animatesFilterProperty = [&]() { |
| if (m_blendingKeyframes.containsProperty(CSSPropertyFilter)) |
| return true; |
| if (m_blendingKeyframes.containsProperty(CSSPropertyWebkitBackdropFilter) || m_blendingKeyframes.containsProperty(CSSPropertyBackdropFilter)) |
| return true; |
| return false; |
| }(); |
| |
| if (!animatesFilterProperty) |
| return false; |
| |
| auto styleContainsFilter = [](const RenderStyle& style) { |
| if (style.filter().hasReferenceFilter()) |
| return true; |
| if (style.backdropFilter().hasReferenceFilter()) |
| return true; |
| return false; |
| }; |
| |
| if (auto target = targetStyleable()) { |
| if (auto* style = target->lastStyleChangeEventStyle()) { |
| if (m_blendingKeyframes.hasImplicitKeyframes() && styleContainsFilter(*style)) |
| return true; |
| } |
| } |
| |
| for (auto& keyframe : m_blendingKeyframes) { |
| if (auto* style = keyframe.style()) { |
| if (styleContainsFilter(*style)) |
| return true; |
| } |
| } |
| |
| return false; |
| }(); |
| } |
| |
| void KeyframeEffect::computeHasSizeDependentTransform() |
| { |
| m_animatesSizeAndSizeDependentTransform = (m_blendingKeyframes.hasWidthDependentTransform() && m_blendingKeyframes.containsProperty(CSSPropertyWidth)) |
| || (m_blendingKeyframes.hasHeightDependentTransform() && m_blendingKeyframes.containsProperty(CSSPropertyHeight)); |
| |
| // If this is a ::view-transition-group pseudo element with the UA-generated transform |
| // and width/height animations, then prevent the transform component from being applied |
| // asynchronously to ensure they remain synchronized. Since the transform usually animates |
| // the position at the same time as the size animates, even slight desynchronizations look |
| // stuttery. |
| if (auto target = targetStyleable()) { |
| if (target->pseudoElementIdentifier && target->pseudoElementIdentifier->pseudoId == PseudoId::ViewTransitionGroup) |
| m_animatesSizeAndSizeDependentTransform |= ((m_blendingKeyframes.containsProperty(CSSPropertyWidth) || m_blendingKeyframes.containsProperty(CSSPropertyHeight)) && m_blendingKeyframes.containsProperty(CSSPropertyTransform)); |
| } |
| } |
| |
| void KeyframeEffect::effectStackNoLongerPreventsAcceleration() |
| { |
| if (m_runningAccelerated == RunningAccelerated::Failed) |
| return; |
| |
| if (m_runningAccelerated == RunningAccelerated::Prevented) |
| m_runningAccelerated = RunningAccelerated::NotStarted; |
| |
| updateAcceleratedActions(); |
| } |
| |
| void KeyframeEffect::effectStackNoLongerAllowsAcceleration() |
| { |
| addPendingAcceleratedAction(AcceleratedAction::Stop); |
| } |
| |
| void KeyframeEffect::effectStackNoLongerAllowsAccelerationDuringAcceleratedActionApplication() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| #endif |
| |
| m_pendingAcceleratedActions.append(AcceleratedAction::Stop); |
| m_lastRecordedAcceleratedAction = AcceleratedAction::Stop; |
| applyPendingAcceleratedActions(); |
| m_pendingAcceleratedActions.clear(); |
| } |
| |
| void KeyframeEffect::abilityToBeAcceleratedDidChange() |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| updateAssociatedThreadedEffectStack(); |
| return; |
| } |
| #endif |
| |
| if (!m_inTargetEffectStack) |
| return; |
| |
| ASSERT(m_target); |
| if (auto* effectStack = m_target->keyframeEffectStack(m_pseudoElementIdentifier)) |
| effectStack->effectAbilityToBeAcceleratedDidChange(*this); |
| } |
| |
| void KeyframeEffect::acceleratedPropertiesOverriddenByCascadeDidChange() |
| { |
| CanBeAcceleratedMutationScope mutationScope(this); |
| computeHasAcceleratedPropertyOverriddenByCascadeProperty(); |
| } |
| |
| KeyframeEffect::CanBeAcceleratedMutationScope::CanBeAcceleratedMutationScope(KeyframeEffect* effect) |
| : m_effect(effect) |
| { |
| ASSERT(effect); |
| m_couldOriginallyPreventAcceleration = effect->preventsAcceleration(); |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| m_couldOriginallyBeAccelerated = effect->canBeAccelerated(); |
| #endif |
| } |
| |
| KeyframeEffect::CanBeAcceleratedMutationScope::~CanBeAcceleratedMutationScope() |
| { |
| if (!m_effect) |
| return; |
| |
| if (m_couldOriginallyPreventAcceleration != m_effect->preventsAcceleration()) |
| m_effect->abilityToBeAcceleratedDidChange(); |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| else if (m_couldOriginallyBeAccelerated != m_effect->canBeAccelerated()) |
| m_effect->abilityToBeAcceleratedDidChange(); |
| #endif |
| } |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| static bool acceleratedPropertyDidChange(AnimatableCSSProperty property, const RenderStyle& previousStyle, const RenderStyle& currentStyle, const Settings& settings) |
| { |
| #if ASSERT_ENABLED |
| ASSERT(Style::Interpolation::isAccelerated(property, settings)); |
| #else |
| UNUSED_PARAM(settings); |
| #endif |
| ASSERT(std::holds_alternative<CSSPropertyID>(property)); |
| |
| switch (std::get<CSSPropertyID>(property)) { |
| case CSSPropertyOpacity: |
| return previousStyle.opacity() != currentStyle.opacity(); |
| case CSSPropertyTransform: |
| return previousStyle.transform() != currentStyle.transform(); |
| case CSSPropertyTranslate: |
| return previousStyle.translate() != currentStyle.translate(); |
| case CSSPropertyScale: |
| return previousStyle.scale() != currentStyle.scale(); |
| case CSSPropertyRotate: |
| return previousStyle.rotate() != currentStyle.rotate(); |
| case CSSPropertyOffsetPath: |
| return previousStyle.offsetPath() != currentStyle.offsetPath(); |
| case CSSPropertyOffsetDistance: |
| return previousStyle.offsetDistance() != currentStyle.offsetDistance(); |
| case CSSPropertyOffsetPosition: |
| return previousStyle.offsetPosition() != currentStyle.offsetPosition(); |
| case CSSPropertyOffsetAnchor: |
| return previousStyle.offsetAnchor() != currentStyle.offsetAnchor(); |
| case CSSPropertyOffsetRotate: |
| return previousStyle.offsetRotate() != currentStyle.offsetRotate(); |
| case CSSPropertyFilter: |
| return previousStyle.filter() != currentStyle.filter(); |
| case CSSPropertyBackdropFilter: |
| case CSSPropertyWebkitBackdropFilter: |
| return previousStyle.backdropFilter() != currentStyle.backdropFilter(); |
| default: |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| |
| return false; |
| } |
| #endif |
| |
| void KeyframeEffect::lastStyleChangeEventStyleDidChange(const RenderStyle* previousStyle, const RenderStyle* currentStyle) |
| { |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| if (threadedAnimationResolutionEnabled()) { |
| if (!isRunningAccelerated()) |
| return; |
| |
| if ((previousStyle && !currentStyle) || (!previousStyle && currentStyle)) { |
| updateAssociatedThreadedEffectStack(); |
| return; |
| } |
| |
| ASSERT(document()); |
| auto& settings = document()->settings(); |
| |
| ASSERT(previousStyle && currentStyle); |
| for (auto property : CSSProperty::allAcceleratedAnimationProperties(settings)) { |
| if (acceleratedPropertyDidChange(property, *previousStyle, *currentStyle, settings)) { |
| updateAssociatedThreadedEffectStack(); |
| return; |
| } |
| } |
| |
| return; |
| } |
| #endif |
| |
| auto hasMotionPath = [](const RenderStyle* style) { |
| return style && style->hasOffsetPath(); |
| }; |
| |
| if (hasMotionPath(previousStyle) != hasMotionPath(currentStyle)) |
| abilityToBeAcceleratedDidChange(); |
| } |
| |
| bool KeyframeEffect::preventsAnimationReadiness() const |
| { |
| // https://drafts.csswg.org/web-animations-1/#ready |
| // An animation cannot be ready if it's associated with a document that does not have a browsing |
| // context since this will prevent the first frame of the animmation from being rendered. |
| return document() && !document()->hasBrowsingContext(); |
| } |
| |
| #if ENABLE(THREADED_ANIMATION_RESOLUTION) |
| KeyframeEffect::StackMembershipMutationScope::StackMembershipMutationScope(KeyframeEffect& effect) |
| : m_effect(&effect) |
| { |
| if (effect.m_target) { |
| m_originalTarget = effect.m_target; |
| m_originalPseudoElementIdentifier = effect.m_pseudoElementIdentifier; |
| } |
| } |
| |
| KeyframeEffect::StackMembershipMutationScope::~StackMembershipMutationScope() |
| { |
| auto originalTargetStyleable = [&]() -> const std::optional<const Styleable> { |
| if (m_originalTarget) |
| return Styleable(*m_originalTarget, m_originalPseudoElementIdentifier); |
| return std::nullopt; |
| }(); |
| |
| RefPtr effect = m_effect; |
| if (effect->isRunningAccelerated()) { |
| if (originalTargetStyleable != effect->targetStyleable()) |
| effect->updateAssociatedThreadedEffectStack(originalTargetStyleable); |
| effect->updateAssociatedThreadedEffectStack(); |
| } |
| } |
| |
| bool KeyframeEffect::threadedAnimationResolutionEnabled() const |
| { |
| auto* document = this->document(); |
| return document && document->settings().threadedAnimationResolutionEnabled(); |
| } |
| |
| void KeyframeEffect::updateAssociatedThreadedEffectStack(const std::optional<const Styleable>& previousTarget) |
| { |
| if (!threadedAnimationResolutionEnabled()) |
| return; |
| |
| ASSERT(document()); |
| if (!document()->page()) |
| return; |
| |
| ASSERT(document()->timelinesController()); |
| auto& acceleratedEffectStackUpdater = CheckedPtr { document()->timelinesController() }->acceleratedEffectStackUpdater(); |
| if (previousTarget) |
| acceleratedEffectStackUpdater.updateEffectStackForTarget(*previousTarget); |
| if (auto currentTarget = targetStyleable()) |
| acceleratedEffectStackUpdater.updateEffectStackForTarget(*currentTarget); |
| |
| if (RefPtr animation = this->animation()) |
| animation->acceleratedStateDidChange(); |
| } |
| #endif |
| |
| const KeyframeInterpolation::Keyframe& KeyframeEffect::keyframeAtIndex(size_t index) const |
| { |
| ASSERT(index < m_blendingKeyframes.size()); |
| return m_blendingKeyframes[index]; |
| } |
| |
| const TimingFunction* KeyframeEffect::timingFunctionForKeyframe(const KeyframeInterpolation::Keyframe& keyframe) const |
| { |
| if (auto* blendingKeyframe = dynamicDowncast<BlendingKeyframe>(keyframe)) |
| return timingFunctionForBlendingKeyframe(*blendingKeyframe); |
| |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| bool KeyframeEffect::isPropertyAdditiveOrCumulative(KeyframeInterpolation::Property property) const |
| { |
| return WTF::switchOn(property, [&](AnimatableCSSProperty& animatableCSSProperty) { |
| return Style::Interpolation::isAdditiveOrCumulative(animatableCSSProperty); |
| }, [] (auto&) { |
| ASSERT_NOT_REACHED(); |
| return false; |
| }); |
| } |
| |
| const ViewTimeline* KeyframeEffect::activeViewTimeline() |
| { |
| RefPtr animation = this->animation(); |
| if (!animation) |
| return nullptr; |
| |
| RefPtr viewTimeline = dynamicDowncast<ViewTimeline>(animation->timeline()); |
| if (viewTimeline && viewTimeline->currentTime()) |
| return viewTimeline.get(); |
| |
| return nullptr; |
| } |
| |
| void KeyframeEffect::animationProgressBasedTimelineSourceDidChangeMetrics(const TimelineRange& animationAttachmentRange) |
| { |
| AnimationEffect::animationProgressBasedTimelineSourceDidChangeMetrics(animationAttachmentRange); |
| m_needsComputedKeyframeOffsetsUpdate = true; |
| } |
| |
| void KeyframeEffect::updateComputedKeyframeOffsetsIfNeeded() |
| { |
| if (!m_needsComputedKeyframeOffsetsUpdate) |
| return; |
| |
| // FIXME: also call this when metrics of the view timeline changes. |
| |
| RefPtr animation = this->animation(); |
| if (!animation) |
| return; |
| |
| RefPtr viewTimeline = dynamicDowncast<ViewTimeline>(animation->timeline()); |
| if (viewTimeline && !viewTimeline->currentTime()) |
| return; |
| |
| if (!m_parsedKeyframes.isEmpty()) |
| computeMissingKeyframeOffsets(m_parsedKeyframes, viewTimeline.get(), animation.get()); |
| |
| m_blendingKeyframes.updatedComputedOffsets([&](auto& specifiedOffset) { |
| return computedOffset(specifiedOffset.name, specifiedOffset.value, viewTimeline.get(), animation.get()); |
| }); |
| |
| m_needsComputedKeyframeOffsetsUpdate = false; |
| }; |
| |
| } // namespace WebCore |