blob: 5e5f6d6d80b4dfd9c3c0d1e47528cca99dad54cd [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "config.h"
#include "core/animation/StringKeyframe.h"
#include "core/animation/AngleSVGInterpolation.h"
#include "core/animation/CSSValueInterpolationType.h"
#include "core/animation/ColorInterpolationType.h"
#include "core/animation/ColorStyleInterpolation.h"
#include "core/animation/CompositorAnimations.h"
#include "core/animation/ConstantStyleInterpolation.h"
#include "core/animation/DefaultSVGInterpolation.h"
#include "core/animation/DeferredLegacyStyleInterpolation.h"
#include "core/animation/DoubleStyleInterpolation.h"
#include "core/animation/FilterStyleInterpolation.h"
#include "core/animation/ImageSliceStyleInterpolation.h"
#include "core/animation/ImageStyleInterpolation.h"
#include "core/animation/IntegerOptionalIntegerSVGInterpolation.h"
#include "core/animation/IntegerSVGInterpolation.h"
#include "core/animation/InterpolationType.h"
#include "core/animation/InvalidatableStyleInterpolation.h"
#include "core/animation/LegacyStyleInterpolation.h"
#include "core/animation/LengthBoxStyleInterpolation.h"
#include "core/animation/LengthInterpolationType.h"
#include "core/animation/LengthPairStyleInterpolation.h"
#include "core/animation/LengthSVGInterpolation.h"
#include "core/animation/LengthStyleInterpolation.h"
#include "core/animation/ListSVGInterpolation.h"
#include "core/animation/ListStyleInterpolation.h"
#include "core/animation/NumberInterpolationType.h"
#include "core/animation/NumberOptionalNumberSVGInterpolation.h"
#include "core/animation/NumberSVGInterpolation.h"
#include "core/animation/PaintInterpolationType.h"
#include "core/animation/PathSVGInterpolation.h"
#include "core/animation/PointSVGInterpolation.h"
#include "core/animation/RectSVGInterpolation.h"
#include "core/animation/SVGStrokeDasharrayStyleInterpolation.h"
#include "core/animation/ShadowStyleInterpolation.h"
#include "core/animation/TransformSVGInterpolation.h"
#include "core/animation/VisibilityStyleInterpolation.h"
#include "core/animation/css/CSSAnimations.h"
#include "core/css/CSSPropertyMetadata.h"
#include "core/css/resolver/StyleResolver.h"
#include "core/style/ComputedStyle.h"
#include "core/svg/SVGElement.h"
#include "platform/RuntimeEnabledFeatures.h"
namespace blink {
StringKeyframe::StringKeyframe(const StringKeyframe& copyFrom)
: Keyframe(copyFrom.m_offset, copyFrom.m_composite, copyFrom.m_easing)
, m_propertySet(copyFrom.m_propertySet->mutableCopy())
, m_svgPropertyMap(copyFrom.m_svgPropertyMap)
{
}
void StringKeyframe::setPropertyValue(CSSPropertyID property, const String& value, Element* element, StyleSheetContents* styleSheetContents)
{
ASSERT(property != CSSPropertyInvalid);
if (CSSAnimations::isAnimatableProperty(property))
m_propertySet->setProperty(property, value, false, styleSheetContents);
}
void StringKeyframe::setPropertyValue(CSSPropertyID property, PassRefPtrWillBeRawPtr<CSSValue> value)
{
ASSERT(property != CSSPropertyInvalid);
ASSERT(CSSAnimations::isAnimatableProperty(property));
m_propertySet->setProperty(property, value, false);
}
void StringKeyframe::setPropertyValue(const QualifiedName& attributeName, const String& value, Element* element)
{
ASSERT(element->isSVGElement());
m_svgPropertyMap.set(&attributeName, value);
}
PropertyHandleSet StringKeyframe::properties() const
{
// This is not used in time-critical code, so we probably don't need to
// worry about caching this result.
PropertyHandleSet properties;
for (unsigned i = 0; i < m_propertySet->propertyCount(); ++i)
properties.add(PropertyHandle(m_propertySet->propertyAt(i).id()));
for (const auto& key: m_svgPropertyMap.keys())
properties.add(PropertyHandle(*key));
return properties;
}
PassRefPtr<Keyframe> StringKeyframe::clone() const
{
return adoptRef(new StringKeyframe(*this));
}
PassOwnPtr<Keyframe::PropertySpecificKeyframe> StringKeyframe::createPropertySpecificKeyframe(PropertyHandle property) const
{
if (property.isCSSProperty())
return adoptPtr(new CSSPropertySpecificKeyframe(offset(), &easing(), cssPropertyValue(property.cssProperty()), composite()));
ASSERT(property.isSVGAttribute());
return adoptPtr(new SVGPropertySpecificKeyframe(offset(), &easing(), svgPropertyValue(*property.svgAttribute()), composite()));
}
StringKeyframe::CSSPropertySpecificKeyframe::CSSPropertySpecificKeyframe(double offset, PassRefPtr<TimingFunction> easing, CSSValue* value, EffectModel::CompositeOperation op)
: Keyframe::PropertySpecificKeyframe(offset, easing, op)
, m_value(value)
{ }
StringKeyframe::CSSPropertySpecificKeyframe::CSSPropertySpecificKeyframe(double offset, PassRefPtr<TimingFunction> easing, CSSValue* value)
: Keyframe::PropertySpecificKeyframe(offset, easing, EffectModel::CompositeReplace)
, m_value(value)
{
ASSERT(!isNull(m_offset));
}
bool StringKeyframe::CSSPropertySpecificKeyframe::populateAnimatableValue(CSSPropertyID property, Element& element, const ComputedStyle* baseStyle, bool force) const
{
if (m_animatableValueCache && !force)
return false;
if (!baseStyle && (!m_value || DeferredLegacyStyleInterpolation::interpolationRequiresStyleResolve(*m_value)))
return false;
m_animatableValueCache = StyleResolver::createAnimatableValueSnapshot(element, baseStyle, property, m_value.get());
return true;
}
namespace {
const Vector<const InterpolationType*>* applicableTypesForProperty(CSSPropertyID property)
{
using ApplicableTypesMap = HashMap<CSSPropertyID, const Vector<const InterpolationType*>*>;
DEFINE_STATIC_LOCAL(ApplicableTypesMap, applicableTypesMap, ());
auto entry = applicableTypesMap.find(property);
if (entry != applicableTypesMap.end())
return entry->value;
auto applicableTypes = new Vector<const InterpolationType*>();
switch (property) {
case CSSPropertyBaselineShift:
case CSSPropertyBorderBottomWidth:
case CSSPropertyBorderLeftWidth:
case CSSPropertyBorderRightWidth:
case CSSPropertyBorderTopWidth:
case CSSPropertyBottom:
case CSSPropertyCx:
case CSSPropertyCy:
case CSSPropertyFlexBasis:
case CSSPropertyHeight:
case CSSPropertyLeft:
case CSSPropertyLetterSpacing:
case CSSPropertyMarginBottom:
case CSSPropertyMarginLeft:
case CSSPropertyMarginRight:
case CSSPropertyMarginTop:
case CSSPropertyMaxHeight:
case CSSPropertyMaxWidth:
case CSSPropertyMinHeight:
case CSSPropertyMinWidth:
case CSSPropertyMotionOffset:
case CSSPropertyOutlineOffset:
case CSSPropertyOutlineWidth:
case CSSPropertyPaddingBottom:
case CSSPropertyPaddingLeft:
case CSSPropertyPaddingRight:
case CSSPropertyPaddingTop:
case CSSPropertyPerspective:
case CSSPropertyR:
case CSSPropertyRight:
case CSSPropertyRx:
case CSSPropertyRy:
case CSSPropertyShapeMargin:
case CSSPropertyStrokeDashoffset:
case CSSPropertyStrokeWidth:
case CSSPropertyTop:
case CSSPropertyVerticalAlign:
case CSSPropertyWebkitBorderHorizontalSpacing:
case CSSPropertyWebkitBorderVerticalSpacing:
case CSSPropertyWebkitColumnGap:
case CSSPropertyWebkitColumnRuleWidth:
case CSSPropertyWebkitColumnWidth:
case CSSPropertyWebkitPerspectiveOriginX:
case CSSPropertyWebkitPerspectiveOriginY:
case CSSPropertyWebkitTransformOriginX:
case CSSPropertyWebkitTransformOriginY:
case CSSPropertyWebkitTransformOriginZ:
case CSSPropertyWidth:
case CSSPropertyWordSpacing:
case CSSPropertyX:
case CSSPropertyY:
applicableTypes->append(new LengthInterpolationType(property));
break;
case CSSPropertyFlexGrow:
case CSSPropertyFlexShrink:
case CSSPropertyFillOpacity:
case CSSPropertyFloodOpacity:
case CSSPropertyFontSizeAdjust:
case CSSPropertyOpacity:
case CSSPropertyOrphans:
case CSSPropertyShapeImageThreshold:
case CSSPropertyStopOpacity:
case CSSPropertyStrokeMiterlimit:
case CSSPropertyStrokeOpacity:
case CSSPropertyWebkitColumnCount:
case CSSPropertyWidows:
case CSSPropertyZIndex:
applicableTypes->append(new NumberInterpolationType(property));
break;
case CSSPropertyLineHeight:
applicableTypes->append(new LengthInterpolationType(property));
applicableTypes->append(new NumberInterpolationType(property));
break;
case CSSPropertyBackgroundColor:
case CSSPropertyBorderBottomColor:
case CSSPropertyBorderLeftColor:
case CSSPropertyBorderRightColor:
case CSSPropertyBorderTopColor:
case CSSPropertyColor:
case CSSPropertyFloodColor:
case CSSPropertyLightingColor:
case CSSPropertyOutlineColor:
case CSSPropertyStopColor:
case CSSPropertyTextDecorationColor:
case CSSPropertyWebkitColumnRuleColor:
case CSSPropertyWebkitTextStrokeColor:
applicableTypes->append(new ColorInterpolationType(property));
break;
case CSSPropertyFill:
case CSSPropertyStroke:
applicableTypes->append(new PaintInterpolationType(property));
break;
default:
// TODO(alancutter): Support all interpolable CSS properties here so we can stop falling back to the old StyleInterpolation implementation.
if (CSSPropertyMetadata::isInterpolableProperty(property)) {
delete applicableTypes;
applicableTypesMap.add(property, nullptr);
return nullptr;
}
break;
}
applicableTypes->append(new CSSValueInterpolationType(property));
applicableTypesMap.add(property, applicableTypes);
return applicableTypes;
}
} // namespace
PassRefPtr<Interpolation> StringKeyframe::CSSPropertySpecificKeyframe::createLegacyStyleInterpolation(CSSPropertyID property, Keyframe::PropertySpecificKeyframe& end, Element* element, const ComputedStyle* baseStyle) const
{
CSSValue& fromCSSValue = *m_value.get();
CSSValue& toCSSValue = *toCSSPropertySpecificKeyframe(end).value();
if (DeferredLegacyStyleInterpolation::interpolationRequiresStyleResolve(fromCSSValue) || DeferredLegacyStyleInterpolation::interpolationRequiresStyleResolve(toCSSValue)) {
// FIXME: Handle these cases outside of DeferredLegacyStyleInterpolation.
return DeferredLegacyStyleInterpolation::create(&fromCSSValue, &toCSSValue, property);
}
// FIXME: Remove the use of AnimatableValues and Elements here.
ASSERT(element);
populateAnimatableValue(property, *element, baseStyle, false);
end.populateAnimatableValue(property, *element, baseStyle, false);
return LegacyStyleInterpolation::create(getAnimatableValue(), end.getAnimatableValue(), property);
}
PassRefPtr<Interpolation> StringKeyframe::CSSPropertySpecificKeyframe::maybeCreateInterpolation(PropertyHandle propertyHandle, Keyframe::PropertySpecificKeyframe& end, Element* element, const ComputedStyle* baseStyle) const
{
CSSPropertyID property = propertyHandle.cssProperty();
const Vector<const InterpolationType*>* applicableTypes = applicableTypesForProperty(property);
if (applicableTypes)
return InvalidatableStyleInterpolation::create(*applicableTypes, *this, toCSSPropertySpecificKeyframe(end));
// TODO(alancutter): Remove the remainder of this function.
// FIXME: Refactor this into a generic piece that lives in InterpolationEffect, and a template parameter specific converter.
CSSValue* fromCSSValue = m_value.get();
CSSValue* toCSSValue = toCSSPropertySpecificKeyframe(end).value();
InterpolationRange range = RangeAll;
// FIXME: Remove this flag once we can rely on legacy's behaviour being correct.
bool forceDefaultInterpolation = false;
// FIXME: Remove this check once neutral keyframes are implemented in StringKeyframes.
if (!fromCSSValue || !toCSSValue)
return DeferredLegacyStyleInterpolation::create(fromCSSValue, toCSSValue, property);
ASSERT(fromCSSValue && toCSSValue);
if (!CSSPropertyMetadata::isInterpolableProperty(property)) {
if (fromCSSValue == toCSSValue)
return ConstantStyleInterpolation::create(fromCSSValue, property);
return nullptr;
}
if (fromCSSValue->isCSSWideKeyword() || toCSSValue->isCSSWideKeyword())
return createLegacyStyleInterpolation(property, end, element, baseStyle);
switch (property) {
case CSSPropertyFontSize:
if (LengthStyleInterpolation::canCreateFrom(*fromCSSValue) && LengthStyleInterpolation::canCreateFrom(*toCSSValue))
return LengthStyleInterpolation::create(*fromCSSValue, *toCSSValue, property, RangeNonNegative);
// FIXME: Handle keywords e.g. 'smaller', 'larger'.
if (property == CSSPropertyFontSize)
return createLegacyStyleInterpolation(property, end, element, baseStyle);
// FIXME: Handle keywords e.g. 'baseline', 'sub'.
if (property == CSSPropertyBaselineShift)
return createLegacyStyleInterpolation(property, end, element, baseStyle);
break;
case CSSPropertyMotionRotation: {
RefPtr<Interpolation> interpolation = DoubleStyleInterpolation::maybeCreateFromMotionRotation(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
break;
}
case CSSPropertyVisibility:
if (VisibilityStyleInterpolation::canCreateFrom(*fromCSSValue) && VisibilityStyleInterpolation::canCreateFrom(*toCSSValue) && (VisibilityStyleInterpolation::isVisible(*fromCSSValue) || VisibilityStyleInterpolation::isVisible(*toCSSValue)))
return VisibilityStyleInterpolation::create(*fromCSSValue, *toCSSValue, property);
break;
case CSSPropertyBorderImageSource:
case CSSPropertyListStyleImage:
case CSSPropertyWebkitMaskBoxImageSource:
if (fromCSSValue == toCSSValue)
return ConstantStyleInterpolation::create(fromCSSValue, property);
if (ImageStyleInterpolation::canCreateFrom(*fromCSSValue) && ImageStyleInterpolation::canCreateFrom(*toCSSValue))
return ImageStyleInterpolation::create(*fromCSSValue, *toCSSValue, property);
forceDefaultInterpolation = true;
break;
case CSSPropertyBorderBottomLeftRadius:
case CSSPropertyBorderBottomRightRadius:
case CSSPropertyBorderTopLeftRadius:
case CSSPropertyBorderTopRightRadius:
if (LengthPairStyleInterpolation::canCreateFrom(*fromCSSValue) && LengthPairStyleInterpolation::canCreateFrom(*toCSSValue))
return LengthPairStyleInterpolation::create(*fromCSSValue, *toCSSValue, property, RangeNonNegative);
break;
case CSSPropertyPerspectiveOrigin:
case CSSPropertyTransformOrigin: {
RefPtr<Interpolation> interpolation = ListStyleInterpolation<LengthStyleInterpolation>::maybeCreateFromList(*fromCSSValue, *toCSSValue, property, range);
if (interpolation)
return interpolation.release();
// FIXME: Handle keywords: top, right, left, center, bottom
return createLegacyStyleInterpolation(property, end, element, baseStyle);
}
case CSSPropertyBoxShadow:
case CSSPropertyTextShadow: {
RefPtr<Interpolation> interpolation = ListStyleInterpolation<ShadowStyleInterpolation>::maybeCreateFromList(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
// FIXME: AnimatableShadow incorrectly animates between inset and non-inset values so it will never indicate it needs default interpolation
if (ShadowStyleInterpolation::usesDefaultStyleInterpolation(*fromCSSValue, *toCSSValue)) {
forceDefaultInterpolation = true;
break;
}
// FIXME: Handle interpolation from/to none, unspecified color values
return createLegacyStyleInterpolation(property, end, element, baseStyle);
}
case CSSPropertyClip: {
if (LengthBoxStyleInterpolation::usesDefaultInterpolation(*fromCSSValue, *toCSSValue)) {
forceDefaultInterpolation = true;
break;
}
RefPtr<Interpolation> interpolation = LengthBoxStyleInterpolation::maybeCreateFrom(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
break;
}
case CSSPropertyBorderImageSlice:
case CSSPropertyWebkitMaskBoxImageSlice: {
RefPtr<Interpolation> interpolation = ImageSliceStyleInterpolation::maybeCreate(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
if (ImageSliceStyleInterpolation::usesDefaultInterpolation(*fromCSSValue, *toCSSValue))
forceDefaultInterpolation = true;
break;
}
case CSSPropertyStrokeDasharray: {
RefPtr<Interpolation> interpolation = SVGStrokeDasharrayStyleInterpolation::maybeCreate(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
break;
}
case CSSPropertyWebkitFilter:
case CSSPropertyBackdropFilter: {
RefPtr<Interpolation> interpolation = FilterStyleInterpolation::maybeCreateList(*fromCSSValue, *toCSSValue, property);
if (interpolation)
return interpolation.release();
// FIXME: Support drop shadow interpolation.
return createLegacyStyleInterpolation(property, end, element, baseStyle);
break;
}
case CSSPropertyTranslate: {
RefPtr<Interpolation> interpolation = ListStyleInterpolation<LengthStyleInterpolation>::maybeCreateFromList(*fromCSSValue, *toCSSValue, property, range);
if (interpolation)
return interpolation.release();
// TODO(soonm): Legacy mode is used when from and to cssvaluelist length does not match.
return createLegacyStyleInterpolation(property, end, element, baseStyle);
break;
}
case CSSPropertyScale: {
RefPtr<Interpolation> interpolation = ListStyleInterpolation<DoubleStyleInterpolation>::maybeCreateFromList(*fromCSSValue, *toCSSValue, property, range);
if (interpolation)
return interpolation.release();
// TODO(soonm): Legacy mode is used when from and to cssvaluelist length does not match.
return createLegacyStyleInterpolation(property, end, element, baseStyle);
break;
}
default:
// Fall back to LegacyStyleInterpolation.
return createLegacyStyleInterpolation(property, end, element, baseStyle);
break;
}
if (fromCSSValue == toCSSValue)
return ConstantStyleInterpolation::create(fromCSSValue, property);
if (!forceDefaultInterpolation) {
ASSERT(AnimatableValue::usesDefaultInterpolation(
StyleResolver::createAnimatableValueSnapshot(*element, baseStyle, property, fromCSSValue).get(),
StyleResolver::createAnimatableValueSnapshot(*element, baseStyle, property, toCSSValue).get()));
}
return nullptr;
}
PassOwnPtr<Keyframe::PropertySpecificKeyframe> StringKeyframe::CSSPropertySpecificKeyframe::neutralKeyframe(double offset, PassRefPtr<TimingFunction> easing) const
{
return adoptPtr(new CSSPropertySpecificKeyframe(offset, easing, static_cast<CSSValue*>(0), EffectModel::CompositeAdd));
}
PassOwnPtr<Keyframe::PropertySpecificKeyframe> StringKeyframe::CSSPropertySpecificKeyframe::cloneWithOffset(double offset) const
{
Keyframe::PropertySpecificKeyframe* theClone = new CSSPropertySpecificKeyframe(offset, m_easing, m_value.get());
toCSSPropertySpecificKeyframe(theClone)->m_animatableValueCache = m_animatableValueCache;
return adoptPtr(theClone);
}
SVGPropertySpecificKeyframe::SVGPropertySpecificKeyframe(double offset, PassRefPtr<TimingFunction> easing, const String& value, EffectModel::CompositeOperation op)
: Keyframe::PropertySpecificKeyframe(offset, easing, op)
, m_value(value)
{
}
SVGPropertySpecificKeyframe::SVGPropertySpecificKeyframe(double offset, PassRefPtr<TimingFunction> easing, const String& value)
: Keyframe::PropertySpecificKeyframe(offset, easing, EffectModel::CompositeReplace)
, m_value(value)
{
ASSERT(!isNull(m_offset));
}
PassOwnPtr<Keyframe::PropertySpecificKeyframe> SVGPropertySpecificKeyframe::cloneWithOffset(double offset) const
{
return adoptPtr(new SVGPropertySpecificKeyframe(offset, m_easing, m_value));
}
PassOwnPtr<Keyframe::PropertySpecificKeyframe> SVGPropertySpecificKeyframe::neutralKeyframe(double offset, PassRefPtr<TimingFunction> easing) const
{
return adoptPtr(new SVGPropertySpecificKeyframe(offset, easing, "", EffectModel::CompositeAdd));
}
namespace {
PassRefPtr<Interpolation> createSVGInterpolation(SVGPropertyBase* fromValue, SVGPropertyBase* toValue, SVGAnimatedPropertyBase* attribute)
{
RefPtr<Interpolation> interpolation = nullptr;
ASSERT(fromValue->type() == toValue->type());
switch (fromValue->type()) {
case AnimatedAngle:
if (AngleSVGInterpolation::canCreateFrom(fromValue) && AngleSVGInterpolation::canCreateFrom(toValue))
return AngleSVGInterpolation::create(fromValue, toValue, attribute);
break;
case AnimatedInteger:
return IntegerSVGInterpolation::create(fromValue, toValue, attribute);
case AnimatedIntegerOptionalInteger: {
int min = &attribute->attributeName() == &SVGNames::orderAttr ? 1 : 0;
return IntegerOptionalIntegerSVGInterpolation::create(fromValue, toValue, attribute, min);
}
case AnimatedLength:
return LengthSVGInterpolation::create(fromValue, toValue, attribute);
case AnimatedLengthList:
interpolation = ListSVGInterpolation<LengthSVGInterpolation>::maybeCreate(fromValue, toValue, attribute);
break;
case AnimatedNumber: {
SVGNumberNegativeValuesMode negativeValuesMode = &attribute->attributeName() == &SVGNames::pathLengthAttr ? ForbidNegativeNumbers : AllowNegativeNumbers;
return NumberSVGInterpolation::create(fromValue, toValue, attribute, negativeValuesMode);
}
case AnimatedNumberOptionalNumber:
return NumberOptionalNumberSVGInterpolation::create(fromValue, toValue, attribute);
case AnimatedNumberList:
interpolation = ListSVGInterpolation<NumberSVGInterpolation>::maybeCreate(fromValue, toValue, attribute);
break;
case AnimatedPath:
interpolation = PathSVGInterpolation::maybeCreate(fromValue, toValue, attribute);
break;
case AnimatedPoints:
interpolation = ListSVGInterpolation<PointSVGInterpolation>::maybeCreate(fromValue, toValue, attribute);
break;
case AnimatedRect:
return RectSVGInterpolation::create(fromValue, toValue, attribute);
case AnimatedTransformList:
interpolation = ListSVGInterpolation<TransformSVGInterpolation>::maybeCreate(fromValue, toValue, attribute);
break;
// TODO(ericwilligers): Support more animation types.
default:
break;
}
if (interpolation)
return interpolation.release();
return DefaultSVGInterpolation::create(fromValue, toValue, attribute);
}
} // namespace
PassRefPtr<Interpolation> SVGPropertySpecificKeyframe::maybeCreateInterpolation(PropertyHandle propertyHandle, Keyframe::PropertySpecificKeyframe& end, Element* element, const ComputedStyle* baseStyle) const
{
ASSERT(element);
RefPtrWillBeRawPtr<SVGAnimatedPropertyBase> attribute = toSVGElement(element)->propertyFromAttribute(*propertyHandle.svgAttribute());
ASSERT(attribute);
RefPtrWillBeRawPtr<SVGPropertyBase> fromValue = attribute->currentValueBase()->cloneForAnimation(m_value);
RefPtrWillBeRawPtr<SVGPropertyBase> toValue = attribute->currentValueBase()->cloneForAnimation(toSVGPropertySpecificKeyframe(end).value());
if (!fromValue || !toValue)
return nullptr;
return createSVGInterpolation(fromValue.get(), toValue.get(), attribute.get());
}
} // namespace blink