mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-08 06:09:58 +00:00
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:
parent
911ecf1450
commit
e4dc2663ba
Notes:
github-actions[bot]
2025-11-18 13:37:43 +00:00
Author: https://github.com/gmta
Commit: e4dc2663ba
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6826
Reviewed-by: https://github.com/AtkinsSJ
Reviewed-by: https://github.com/nico
6 changed files with 610 additions and 89 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue