LibWeb: Reimplement transform interpolation according to spec

We had a partial implementation of transformation function interpolation
that did not support numerical interpolation of simple functions (e.g.
`scale(0)` -> `scale(1)`). This refactors the interpolation to follow
the spec more closely.

Gains us 267 WPT subtest passes in `css/css-transforms`.

Fixes #6774.
This commit is contained in:
Jelle Raaijmakers 2025-11-14 13:41:32 +01:00 committed by Jelle Raaijmakers
parent 911ecf1450
commit e4dc2663ba
Notes: github-actions[bot] 2025-11-18 13:37:43 +00:00
6 changed files with 610 additions and 89 deletions

View file

@ -4,6 +4,7 @@
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -613,7 +614,7 @@ ValueComparingRefPtr<StyleValue const> interpolate_property(DOM::Element& elemen
return interpolate_repeatable_list(element, calculation_context, from, to, delta, allow_discrete);
case AnimationType::Custom: {
if (property_id == PropertyID::Transform) {
if (auto interpolated_transform = interpolate_transform(element, from, to, delta, allow_discrete))
if (auto interpolated_transform = interpolate_transform(element, calculation_context, from, to, delta, allow_discrete))
return *interpolated_transform;
// https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
@ -756,83 +757,8 @@ bool property_values_are_transitionable(PropertyID property_id, StyleValue const
return true;
}
// A null return value means the interpolated matrix was not invertible or otherwise invalid
RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete)
static Optional<FloatMatrix4x4> interpolate_matrices(FloatMatrix4x4 const& from, FloatMatrix4x4 const& to, float delta)
{
// Note that the spec uses column-major notation, so all the matrix indexing is reversed.
static constexpr auto make_transformation = [](TransformationStyleValue const& transformation) -> Optional<Transformation> {
Vector<TransformValue> values;
for (auto const& value : transformation.values()) {
switch (value->type()) {
case StyleValue::Type::Angle:
values.append(AngleOrCalculated { value->as_angle().angle() });
break;
case StyleValue::Type::Calculated: {
auto& calculated = value->as_calculated();
if (calculated.resolves_to_angle()) {
values.append(AngleOrCalculated { calculated });
} else if (calculated.resolves_to_length()) {
values.append(LengthPercentage { calculated });
} else if (calculated.resolves_to_number() || calculated.resolves_to_percentage()) {
values.append(NumberPercentage { calculated });
} else {
dbgln("Calculation `{}` inside {} transform-function is not a recognized type", calculated.to_string(SerializationMode::Normal), to_string(transformation.transform_function()));
return {};
}
break;
}
case StyleValue::Type::Length:
values.append(LengthPercentage { value->as_length().length() });
break;
case StyleValue::Type::Percentage:
values.append(LengthPercentage { value->as_percentage().percentage() });
break;
case StyleValue::Type::Number:
values.append(NumberPercentage { Number(Number::Type::Number, value->as_number().number()) });
break;
default:
return {};
}
}
return Transformation { transformation.transform_function(), move(values) };
};
static constexpr auto transformation_style_value_to_matrix = [](DOM::Element& element, TransformationStyleValue const& value) -> Optional<FloatMatrix4x4> {
auto transformation = make_transformation(value);
if (!transformation.has_value())
return {};
Optional<Painting::PaintableBox const&> paintable_box;
if (auto layout_node = element.layout_node()) {
if (auto* paintable = as_if<Painting::PaintableBox>(layout_node->first_paintable()))
paintable_box = *paintable;
}
if (auto matrix = transformation->to_matrix(paintable_box); !matrix.is_error())
return matrix.value();
return {};
};
static constexpr auto style_value_to_matrix = [](DOM::Element& element, StyleValue const& value) -> FloatMatrix4x4 {
if (value.is_transformation())
return transformation_style_value_to_matrix(element, value.as_transformation()).value_or(FloatMatrix4x4::identity());
// This encompasses both the allowed value "none" and any invalid values
if (!value.is_value_list())
return FloatMatrix4x4::identity();
auto matrix = FloatMatrix4x4::identity();
for (auto const& value_element : value.as_value_list().values()) {
if (value_element->is_transformation()) {
if (auto value_matrix = transformation_style_value_to_matrix(element, value_element->as_transformation()); value_matrix.has_value())
matrix = matrix * value_matrix.value();
}
}
return matrix;
};
struct DecomposedValues {
FloatVector3 translation;
FloatVector3 scale;
@ -1037,20 +963,370 @@ RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, StyleValue
};
};
auto from_matrix = style_value_to_matrix(element, from);
auto to_matrix = style_value_to_matrix(element, to);
auto from_decomposed = decompose(from_matrix);
auto to_decomposed = decompose(to_matrix);
auto from_decomposed = decompose(from);
auto to_decomposed = decompose(to);
if (!from_decomposed.has_value() || !to_decomposed.has_value())
return {};
auto interpolated_decomposed = interpolate(from_decomposed.value(), to_decomposed.value(), delta);
auto interpolated = recompose(interpolated_decomposed);
return recompose(interpolated_decomposed);
}
StyleValueVector values;
values.ensure_capacity(16);
for (int i = 0; i < 16; i++)
values.append(NumberStyleValue::create(static_cast<double>(interpolated[i % 4, i / 4])));
return StyleValueList::create({ TransformationStyleValue::create(PropertyID::Transform, TransformFunction::Matrix3d, move(values)) }, StyleValueList::Separator::Comma);
// https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, CalculationContext const& calculation_context,
StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete)
{
// * If both Va and Vb are none:
// * Vresult is none.
if (from.is_keyword() && from.as_keyword().keyword() == Keyword::None
&& to.is_keyword() && to.as_keyword().keyword() == Keyword::None) {
return KeywordStyleValue::create(Keyword::None);
}
// * Treating none as a list of zero length, if Va or Vb differ in length:
auto style_value_to_transformations = [](StyleValue const& style_value)
-> Vector<NonnullRefPtr<TransformationStyleValue const>> {
if (style_value.is_transformation())
return { style_value.as_transformation() };
// NB: This encompasses both the allowed value "none" and any invalid values.
if (!style_value.is_value_list())
return {};
Vector<NonnullRefPtr<TransformationStyleValue const>> result;
result.ensure_capacity(style_value.as_value_list().size());
for (auto const& value : style_value.as_value_list().values()) {
VERIFY(value->is_transformation());
result.unchecked_append(value->as_transformation());
}
return result;
};
auto from_transformations = style_value_to_transformations(from);
auto to_transformations = style_value_to_transformations(to);
if (from_transformations.size() != to_transformations.size()) {
// * extend the shorter list to the length of the longer list, setting the function at each additional
// position to the identity transform function matching the function at the corresponding position in the
// longer list. Both transform function lists are then interpolated following the next rule.
auto& shorter_list = from_transformations.size() < to_transformations.size() ? from_transformations : to_transformations;
auto const& longer_list = from_transformations.size() < to_transformations.size() ? to_transformations : from_transformations;
for (size_t i = shorter_list.size(); i < longer_list.size(); ++i) {
auto const& transformation = longer_list[i];
shorter_list.append(TransformationStyleValue::identity_transformation(transformation->transform_function()));
}
}
// https://drafts.csswg.org/css-transforms-1/#transform-primitives
auto is_2d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate,
TransformFunction::Scale,
TransformFunction::Translate);
};
auto is_2d_transform = [&is_2d_primitive](TransformFunction function) {
return is_2d_primitive(function)
|| first_is_one_of(function,
TransformFunction::ScaleX,
TransformFunction::ScaleY,
TransformFunction::TranslateX,
TransformFunction::TranslateY);
};
// https://drafts.csswg.org/css-transforms-2/#transform-primitives
auto is_3d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate3d,
TransformFunction::Scale3d,
TransformFunction::Translate3d);
};
auto is_3d_transform = [&is_2d_transform, &is_3d_primitive](TransformFunction function) {
return is_2d_transform(function)
|| is_3d_primitive(function)
|| first_is_one_of(function,
TransformFunction::RotateX,
TransformFunction::RotateY,
TransformFunction::RotateZ,
TransformFunction::ScaleZ,
TransformFunction::TranslateZ);
};
auto convert_2d_transform_to_primitive = [](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Scale:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
break;
case TransformFunction::ScaleX:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleY:
generic_function = TransformFunction::Scale;
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Rotate:
generic_function = TransformFunction::Rotate;
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateX:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateY:
generic_function = TransformFunction::Translate;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
VERIFY_NOT_REACHED();
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
auto convert_3d_transform_to_primitive = [&](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
// NB: Convert to 2D primitive if possible so we don't have to deal with scale/translate X/Y separately.
if (is_2d_transform(transform->transform_function()))
transform = convert_2d_transform_to_primitive(transform);
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Rotate:
case TransformFunction::RotateZ:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateX:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateY:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Scale:
generic_function = TransformFunction::Scale3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleZ:
generic_function = TransformFunction::Scale3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateZ:
generic_function = TransformFunction::Translate3d;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
VERIFY_NOT_REACHED();
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
// * Let Vresult be an empty list. Beginning at the start of Va and Vb, compare the corresponding functions at each
// position:
StyleValueVector result;
result.ensure_capacity(from_transformations.size());
size_t index = 0;
for (; index < from_transformations.size(); ++index) {
auto from_transformation = from_transformations[index];
auto to_transformation = to_transformations[index];
auto from_function = from_transformation->transform_function();
auto to_function = to_transformation->transform_function();
// * While the functions have either the same name, or are derivatives of the same primitive transform
// function, interpolate the corresponding pair of functions as described in § 10 Interpolation of
// primitives and derived transform functions and append the result to Vresult.
// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
// Two different types of transform functions that share the same primitive, or transform functions of the same
// type with different number of arguments can be interpolated. Both transform functions need a former
// conversion to the common primitive first and get interpolated numerically afterwards. The computed value will
// be the primitive with the resulting interpolated arguments.
// The transform functions <matrix()>, matrix3d() and perspective() get converted into 4x4 matrices first and
// interpolated as defined in section Interpolation of Matrices afterwards.
if (first_is_one_of(TransformFunction::Matrix, from_function, to_function)
|| first_is_one_of(TransformFunction::Matrix3d, from_function, to_function)
|| first_is_one_of(TransformFunction::Perspective, from_function, to_function)) {
break;
}
// If both transform functions share a primitive in the two-dimensional space, both transform functions get
// converted to the two-dimensional primitive. If one or both transform functions are three-dimensional
// transform functions, the common three-dimensional primitive is used.
if (is_2d_transform(from_function) && is_2d_transform(to_function)) {
from_transformation = convert_2d_transform_to_primitive(from_transformation);
to_transformation = convert_2d_transform_to_primitive(to_transformation);
} else if (is_3d_transform(from_function) || is_3d_transform(to_function)) {
// NB: 3D primitives do not support value expansion like their 2D counterparts do (e.g. scale(1.5) ->
// scale(1.5, 1.5), so we check if they are already a primitive first.
if (!is_3d_primitive(from_function))
from_transformation = convert_3d_transform_to_primitive(from_transformation);
if (!is_3d_primitive(to_function))
to_transformation = convert_3d_transform_to_primitive(to_transformation);
}
from_function = from_transformation->transform_function();
to_function = to_transformation->transform_function();
// NB: We converted both functions to their primitives. But if they're different primitives or if they have a
// different number of values, we can't interpolate numerically between them. Break here so the next loop
// can take care of the remaining functions.
auto const& from_values = from_transformation->values();
auto const& to_values = to_transformation->values();
if (from_function != to_function || from_values.size() != to_values.size())
break;
// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
if (from_function == TransformFunction::Rotate3d) {
// FIXME: For interpolations with the primitive rotate3d(), the direction vectors of the transform functions get
// normalized first. If the normalized vectors are not equal and both rotation angles are non-zero the
// transform functions get converted into 4x4 matrices first and interpolated as defined in section
// Interpolation of Matrices afterwards. Otherwise the rotation angle gets interpolated numerically and the
// rotation vector of the non-zero angle is used or (0, 0, 1) if both angles are zero.
auto interpolated_rotation = interpolate_rotate(element, calculation_context, from_transformation,
to_transformation, delta, AllowDiscrete::No);
if (!interpolated_rotation)
break;
result.unchecked_append(*interpolated_rotation);
} else {
StyleValueVector interpolated;
interpolated.ensure_capacity(from_values.size());
for (size_t i = 0; i < from_values.size(); ++i) {
auto interpolated_value = interpolate_value(element, calculation_context, from_values[i], to_values[i],
delta, AllowDiscrete::No);
if (!interpolated_value)
break;
interpolated.unchecked_append(*interpolated_value);
}
if (interpolated.size() != from_values.size())
break;
result.unchecked_append(TransformationStyleValue::create(PropertyID::Transform, from_function, move(interpolated)));
}
}
// NB: Return if we're done.
if (index == from_transformations.size())
return StyleValueList::create(move(result), StyleValueList::Separator::Space);
static constexpr auto make_transformation = [](TransformationStyleValue const& transformation) -> Optional<Transformation> {
Vector<TransformValue> values;
values.ensure_capacity(transformation.values().size());
for (auto const& value : transformation.values()) {
switch (value->type()) {
case StyleValue::Type::Angle:
values.unchecked_append(AngleOrCalculated { value->as_angle().angle() });
break;
case StyleValue::Type::Calculated: {
auto& calculated = value->as_calculated();
if (calculated.resolves_to_angle()) {
values.unchecked_append(AngleOrCalculated { calculated });
} else if (calculated.resolves_to_length()) {
values.unchecked_append(LengthPercentage { calculated });
} else if (calculated.resolves_to_number() || calculated.resolves_to_percentage()) {
values.unchecked_append(NumberPercentage { calculated });
} else {
dbgln("Calculation `{}` inside {} transform-function is not a recognized type", calculated.to_string(SerializationMode::Normal), to_string(transformation.transform_function()));
return {};
}
break;
}
case StyleValue::Type::Length:
values.unchecked_append(LengthPercentage { value->as_length().length() });
break;
case StyleValue::Type::Percentage:
values.unchecked_append(LengthPercentage { value->as_percentage().percentage() });
break;
case StyleValue::Type::Number:
values.unchecked_append(NumberPercentage { Number(Number::Type::Number, value->as_number().number()) });
break;
default:
return {};
}
}
return Transformation { transformation.transform_function(), move(values) };
};
// * If the pair do not have a common name or primitive transform function, post-multiply the remaining
// transform functions in each of Va and Vb respectively to produce two 4x4 matrices. Interpolate these two
// matrices as described in § 11 Interpolation of Matrices, append the result to Vresult, and cease
// iterating over Va and Vb.
Optional<Painting::PaintableBox const&> paintable_box;
if (auto* paintable = as_if<Painting::PaintableBox>(element.paintable()))
paintable_box = *paintable;
auto post_multiply_remaining_transformations = [&paintable_box](size_t start_index, Vector<NonnullRefPtr<TransformationStyleValue const>> const& transformations) {
FloatMatrix4x4 result = FloatMatrix4x4::identity();
for (auto index = start_index; index < transformations.size(); ++index) {
auto transformation = make_transformation(transformations[index]);
if (!transformation.has_value()) {
dbgln("Unable to interpret a transformation; bailing out of interpolation.");
break;
}
auto transformation_matrix = transformation->to_matrix(paintable_box);
if (transformation_matrix.is_error()) {
dbgln("Unable to interpret a transformation's matrix; bailing out of interpolation.");
break;
}
result = result * transformation_matrix.value();
}
return result;
};
auto from_matrix = post_multiply_remaining_transformations(index, from_transformations);
auto to_matrix = post_multiply_remaining_transformations(index, to_transformations);
auto maybe_interpolated_matrix = interpolate_matrices(from_matrix, to_matrix, delta);
if (maybe_interpolated_matrix.has_value()) {
auto interpolated_matrix = maybe_interpolated_matrix.release_value();
StyleValueVector values;
values.ensure_capacity(16);
for (int i = 0; i < 16; i++)
values.unchecked_append(NumberStyleValue::create(interpolated_matrix[i % 4, i / 4]));
result.append(TransformationStyleValue::create(PropertyID::Transform, TransformFunction::Matrix3d, move(values)));
} else {
dbgln("Unable to interpolate matrices.");
}
return StyleValueList::create(move(result), StyleValueList::Separator::Space);
}
Color interpolate_color(Color from, Color to, float delta, ColorSyntax syntax)

View file

@ -29,7 +29,7 @@ Optional<LengthPercentageOrAuto> interpolate_length_percentage_or_auto(Calculati
RefPtr<StyleValue const> interpolate_value(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_repeatable_list(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_box_shadow(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_transform(DOM::Element&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_transform(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
Color interpolate_color(Color from, Color to, float delta, ColorSyntax syntax);

View file

@ -4,11 +4,11 @@
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2024, Steffen T. Larssen <dudedbz@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TransformationStyleValue.h"
#include <AK/StringBuilder.h>
#include <LibWeb/CSS/CSSMatrixComponent.h>
#include <LibWeb/CSS/CSSPerspective.h>
@ -23,14 +23,79 @@
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/Transformation.h>
#include <LibWeb/Geometry/DOMMatrix.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<TransformationStyleValue const> TransformationStyleValue::identity_transformation(
TransformFunction transform_function)
{
// https://drafts.csswg.org/css-transforms-1/#identity-transform-function
// A transform function that is equivalent to a identity 4x4 matrix (see Mathematical Description of Transform
// Functions). Examples for identity transform functions are translate(0), translateX(0), translateY(0), scale(1),
// scaleX(1), scaleY(1), rotate(0), skew(0, 0), skewX(0), skewY(0) and matrix(1, 0, 0, 1, 0, 0).
// https://drafts.csswg.org/css-transforms-2/#identity-transform-function
// In addition to the identity transform function in CSS Transforms, examples for identity transform functions
// include translate3d(0, 0, 0), translateZ(0), scaleZ(1), rotate3d(1, 1, 1, 0), rotateX(0), rotateY(0), rotateZ(0)
// and matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1). A special case is perspective: perspective(none).
// The value of m34 becomes infinitesimal small and the transform function is therefore assumed to be equal to the
// identity matrix.
auto identity_parameters = [&] -> StyleValueVector {
auto const number_zero = NumberStyleValue::create(0.);
auto const number_one = NumberStyleValue::create(1.);
switch (transform_function) {
case TransformFunction::Matrix:
return { number_one, number_zero, number_zero, number_one, number_zero, number_zero };
case TransformFunction::Matrix3d:
return { number_one, number_zero, number_zero, number_zero,
number_zero, number_one, number_zero, number_zero,
number_zero, number_zero, number_one, number_zero,
number_zero, number_zero, number_zero, number_one };
case TransformFunction::Perspective:
return { KeywordStyleValue::create(Keyword::None) };
case TransformFunction::Rotate:
case TransformFunction::RotateX:
case TransformFunction::RotateY:
case TransformFunction::RotateZ:
return { AngleStyleValue::create(Angle::make_degrees(0.)) };
case TransformFunction::Rotate3d:
return { number_one, number_one, number_one, AngleStyleValue::create(Angle::make_degrees(0.)) };
case TransformFunction::Skew:
case TransformFunction::SkewX:
case TransformFunction::SkewY:
case TransformFunction::Translate:
case TransformFunction::TranslateX:
case TransformFunction::TranslateY:
case TransformFunction::TranslateZ:
return { LengthStyleValue::create(Length::make_px(0.)) };
case TransformFunction::Translate3d:
return {
LengthStyleValue::create(Length::make_px(0.)),
LengthStyleValue::create(Length::make_px(0.)),
LengthStyleValue::create(Length::make_px(0.)),
};
case TransformFunction::Scale:
case TransformFunction::ScaleX:
case TransformFunction::ScaleY:
case TransformFunction::ScaleZ:
return { number_one };
case TransformFunction::Scale3d:
return { number_one, number_one, number_one };
}
VERIFY_NOT_REACHED();
};
return create(PropertyID::Transform, transform_function, identity_parameters());
}
Transformation TransformationStyleValue::to_transformation() const
{
auto function_metadata = transform_function_metadata(m_properties.transform_function);

View file

@ -22,6 +22,8 @@ public:
}
virtual ~TransformationStyleValue() override = default;
static ValueComparingNonnullRefPtr<TransformationStyleValue const> identity_transformation(TransformFunction);
TransformFunction transform_function() const { return m_properties.transform_function; }
StyleValueVector const& values() const { return m_properties.values; }

View file

@ -0,0 +1,88 @@
Harness status: OK
Found 82 tests
64 Pass
18 Fail
Pass Interpolation between translateX(0px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(0px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between translateY(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between translateY(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(50px) and translateY(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(50px) and translateY(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateZ(50px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateZ(50px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateZ(-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateZ(-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(0%) and translate(50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(0%) and translate(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50%) and translate(100%, 50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50%) and translate(100%, 50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(0%, 50%) and translate(50%, 100%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(0%, 50%) and translate(50%, 100%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate3d(0,0,-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate3d(0,0,-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50px, 0px) and translate(100px, 0px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50px, 0px) and translate(100px, 0px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50px, -50px) and translate(100px, 50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50px, -50px) and translate(100px, 50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between rotate(30deg) and rotate(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between rotate(30deg) and rotate(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between rotateZ(30deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between rotateZ(30deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotate(0deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotate(0deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotateX(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotateX(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotate(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotate(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between scale(1) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between scale(1) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 3) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 3) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleX(1) and scaleX(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleX(1) and scaleX(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleY(1) and scaleY(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleY(1) and scaleY(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleZ(1) and scaleZ(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleZ(1) and scaleZ(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between scaleX(2) and scaleY(2) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between scaleX(2) and scaleY(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleX(2) and scaleY(3) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleX(2) and scaleY(3) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleZ(1) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleZ(1) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 2) and scale(3, 4) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 2) and scale(3, 4) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale3d(1, 2, 3) and scale3d(4, 5, 6) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale3d(1, 2, 3) and scale3d(4, 5, 6) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale3d(1, 2, 3) and scale(4, 5) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale3d(1, 2, 3) and scale(4, 5) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 2) and scale3d(3, 4, 5) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 2) and scale3d(3, 4, 5) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(60deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(60deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(180deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(180deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skew(0deg, 0deg) and skew(60deg, 60deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skew(0deg, 0deg) and skew(60deg, 60deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skew(45deg, 0deg) and skew(0deg, 45deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skew(45deg, 0deg) and skew(0deg, 45deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(10px) and perspective(2.5px) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(10px) and perspective(2.5px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(10px) and perspective(none) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(10px) and perspective(none) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(none) and perspective(none) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(none) and perspective(none) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix(2, 0, 0, 2, 10, 30) and matrix(4, 0, 0, 6, 14, 10) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix(2, 0, 0, 2, 10, 30) and matrix(4, 0, 0, 6, 14, 10) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1) and matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1) and matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1) and matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1) and matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1) gives the correct computed value halfway according to computedStyleMap with zoom active.

View file

@ -0,0 +1,90 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>transform interpolation</title>
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om/#transformvalue-objects">
<meta name="assert" content="transform gives the correct computed values when interpolated">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../web-animations/testcommon.js"></script>
<body>
<script>
function interpolation_test(from, to, expected_50) {
test(t => {
let div = createDiv(t);
let anim = div.animate({transform: [from, to]}, 2000);
anim.pause();
anim.currentTime = 1000;
let halfway = div.computedStyleMap().get('transform').toString();
assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
}, "Interpolation between " + from + " and " + to + " gives the correct " +
"computed value halfway according to computedStyleMap.");
test(t => {
let div = createDiv(t);
div.style.zoom = 1.25;
let anim = div.animate({transform: [from, to]}, 2000);
anim.pause();
anim.currentTime = 1000;
let halfway = div.computedStyleMap().get('transform').toString();
assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
}, "Interpolation between " + from + " and " + to + " gives the correct " +
"computed value halfway according to computedStyleMap with zoom active.");
}
interpolation_test('translateX(0px)', 'translateX(50px)', 'translate(25px, 0px)');
interpolation_test('translateX(0%)', 'translateX(50%)', 'translate(25%, 0px)');
interpolation_test('translateY(0%)', 'translateX(50%)', 'translate(25%, 0px)');
interpolation_test('translateX(50px)', 'translateY(50px)', 'translate(25px, 25px)');
interpolation_test('translateX(50px)', 'translateZ(50px)', 'translate3d(25px, 0px, 25px)');
interpolation_test('translateZ(50px)', 'translateX(50px)', 'translate3d(25px, 0px, 25px)');
interpolation_test('translateZ(-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
interpolation_test('translate(0%)', 'translate(50%)', 'translate(25%, 0px)');
interpolation_test('translate(50%)', 'translate(100%, 50%)', 'translate(75%, 25%)');
interpolation_test('translate(0%, 50%)', 'translate(50%, 100%)', 'translate(25%, 75%)');
interpolation_test('translate3d(0,0,-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
interpolation_test('translate(50px, 0px)', 'translate(100px, 0px)', 'translate(75px, 0px)');
interpolation_test('translate(50px, -50px)', 'translate(100px, 50px)', 'translate(75px, 0px)');
interpolation_test('rotate(30deg)', 'rotate(90deg)', 'rotate(60deg)');
interpolation_test('rotateZ(30deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 60deg)');
interpolation_test('rotate(0deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 45deg)');
interpolation_test('rotateX(0deg)','rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
interpolation_test('rotate(0deg)', 'rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
interpolation_test('scale(1)', 'scale(2)', 'scale(1.5, 1.5)');
interpolation_test('scale(1, 3)', 'scale(2)', 'scale(1.5, 2.5)');
interpolation_test('scaleX(1)', 'scaleX(2)', 'scale(1.5, 1)');
interpolation_test('scaleY(1)', 'scaleY(2)', 'scale(1, 1.5)');
interpolation_test('scaleZ(1)', 'scaleZ(2)', 'scale3d(1, 1, 1.5)');
interpolation_test('scaleX(2)', 'scaleY(2)', 'scale(1.5, 1.5)');
interpolation_test('scaleX(2)', 'scaleY(3)', 'scale(1.5, 2)');
interpolation_test('scaleZ(1)', 'scale(2)', 'scale3d(1.5, 1.5, 1)');
interpolation_test('scale(1, 2)', 'scale(3, 4)', 'scale(2, 3)');
interpolation_test('scale3d(1, 2, 3)', 'scale3d(4, 5, 6)', 'scale3d(2.5, 3.5, 4.5)');
interpolation_test('scale3d(1, 2, 3)', 'scale(4, 5)', 'scale3d(2.5, 3.5, 2)');
interpolation_test('scale(1, 2)', 'scale3d(3, 4, 5)', 'scale3d(2, 3, 3)');
interpolation_test('skewX(0deg)', 'skewX(60deg)', 'skewX(30deg)');
interpolation_test('skewX(0deg)', 'skewX(90deg)', 'skewX(45deg)');
interpolation_test('skewX(0deg)', 'skewX(180deg)', 'skewX(90deg)');
interpolation_test('skew(0deg, 0deg)', 'skew(60deg, 60deg)', 'skew(30deg, 30deg)');
interpolation_test('skew(45deg, 0deg)', 'skew(0deg, 45deg)', 'skew(22.5deg, 22.5deg)');
interpolation_test('perspective(10px)', 'perspective(2.5px)', 'perspective(4px)');
interpolation_test('perspective(10px)', 'perspective(none)', 'perspective(20px)');
interpolation_test('perspective(none)', 'perspective(none)', 'perspective(none)');
// A matrix() with just scale and translation.
interpolation_test('matrix(2, 0, 0, 2, 10, 30)', 'matrix(4, 0, 0, 6, 14, 10)', 'matrix(3, 0, 0, 4, 12, 20)');
// A matrix3d() with just scale and translation.
interpolation_test('matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1)', 'matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1)', 'matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, -3, 6, 3, 1)');
// A matrix3d() with just perspective.
interpolation_test('matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 4, 0, 1, 0, 5, 0, 0, 1, 11, 0, 0, 0, 1)');
// NOTE: New tests added here should also be added in
// transform-interpolation-inline-value.html.
</script>