From 6d0672eec082c47b26dbb94225e33d6e62cb48c3 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Wed, 28 Feb 2024 18:51:07 -0700 Subject: [PATCH] LibWeb: Implement complex transform interpolation With this commit, we can interpolate between transforms whose functions don't match. For example: translate(100px) -> scale(2) translateX(50px) --- .../Libraries/LibWeb/CSS/StyleComputer.cpp | 348 ++++++++++++++++-- 1 file changed, 323 insertions(+), 25 deletions(-) diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index c69011a08f..9b7ad068de 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -69,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -758,18 +759,315 @@ static ErrorOr cascade_custom_properties(DOM::Element& element, Optional> interpolate_value(StyleValue const& from, StyleValue const& to, float delta) +template +static T interpolate_raw(T from, T to, float delta) +{ + if constexpr (IsSame) { + return from + (to - from) * static_cast(delta); + } else { + return static_cast>(from + (to - from) * delta); + } +} + +static ErrorOr> interpolate_transform(DOM::Element& element, StyleValue const& from, StyleValue 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) -> ErrorOr { + Vector 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: + values.append(AngleOrCalculated { value->as_calculated() }); + 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 Error::from_string_literal("Transform contains unsupported style value"); + } + } + + return Transformation { transformation.transform_function(), move(values) }; + }; + + static constexpr auto transformation_style_value_to_matrix = [](DOM::Element& element, TransformationStyleValue const& value) -> ErrorOr { + auto transformation = TRY(make_transformation(value.as_transformation())); + Optional paintable_box; + if (auto layout_node = element.layout_node()) { + if (auto paintable = layout_node->paintable(); paintable && is(paintable)) + paintable_box = *static_cast(paintable); + } + return transformation.to_matrix(paintable_box); + }; + + static constexpr auto style_value_to_matrix = [](DOM::Element& element, StyleValue const& value) -> ErrorOr { + if (value.to_identifier() == ValueID::None) + return FloatMatrix4x4::identity(); + + if (value.is_transformation()) + return transformation_style_value_to_matrix(element, value.as_transformation()); + + VERIFY(value.is_value_list()); + auto matrix = FloatMatrix4x4::identity(); + for (auto const& value_element : value.as_value_list().values()) { + if (value_element->is_transformation()) + matrix = matrix * TRY(transformation_style_value_to_matrix(element, value_element->as_transformation())); + } + + return matrix; + }; + + struct DecomposedValues { + FloatVector3 translation; + FloatVector3 scale; + FloatVector3 skew; + FloatVector4 rotation; + FloatVector4 perspective; + }; + // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix + static constexpr auto decompose = [](FloatMatrix4x4 matrix) -> ErrorOr { + // https://drafts.csswg.org/css-transforms-1/#supporting-functions + static constexpr auto combine = [](auto a, auto b, float ascl, float bscl) { + return FloatVector3 { + ascl * a[0] + bscl * b[0], + ascl * a[1] + bscl * b[1], + ascl * a[2] + bscl * b[2], + }; + }; + + // Normalize the matrix. + if (matrix(3, 3) == 0.f) + return Error::from_string_literal("Cannot interpolate non-invertible matrix"); + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + matrix(i, j) /= matrix(3, 3); + + // perspectiveMatrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + auto perspective_matrix = matrix; + for (int i = 0; i < 3; i++) + perspective_matrix(3, i) = 0.f; + perspective_matrix(3, 3) = 1.f; + + if (!perspective_matrix.is_invertible()) + return Error::from_string_literal("Cannot interpolate non-invertible matrix"); + + DecomposedValues values; + + // First, isolate perspective. + if (matrix(3, 0) != 0.f || matrix(3, 1) != 0.f || matrix(3, 2) != 0.f) { + // rightHandSide is the right hand side of the equation. + // Note: It is the bottom side in a row-major matrix + FloatVector4 bottom_side = { + matrix(3, 0), + matrix(3, 1), + matrix(3, 2), + matrix(3, 3), + }; + + // Solve the equation by inverting perspectiveMatrix and multiplying + // rightHandSide by the inverse. + auto inverse_perspective_matrix = perspective_matrix.inverse(); + auto transposed_inverse_perspective_matrix = inverse_perspective_matrix.transpose(); + values.perspective = transposed_inverse_perspective_matrix * bottom_side; + } else { + // No perspective. + values.perspective = { 0.0, 0.0, 0.0, 1.0 }; + } + + // Next take care of translation + for (int i = 0; i < 3; i++) + values.translation[i] = matrix(i, 3); + + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + FloatVector3 row[3]; + for (int i = 0; i < 3; i++) + row[i] = { matrix(0, i), matrix(1, i), matrix(2, i) }; + + // Compute X scale factor and normalize first row. + values.scale[0] = row[0].length(); + row[0].normalize(); + + // Compute XY shear factor and make 2nd row orthogonal to 1st. + values.skew[0] = row[0].dot(row[1]); + row[1] = combine(row[1], row[0], 1.f, -values.skew[0]); + + // Now, compute Y scale and normalize 2nd row. + values.scale[1] = row[1].length(); + row[1].normalize(); + values.skew[0] /= values.scale[1]; + + // Compute XZ and YZ shears, orthogonalize 3rd row + values.skew[1] = row[0].dot(row[2]); + row[2] = combine(row[2], row[0], 1.f, -values.skew[1]); + values.skew[2] = row[1].dot(row[2]); + row[2] = combine(row[2], row[1], 1.f, -values.skew[2]); + + // Next, get Z scale and normalize 3rd row. + values.scale[2] = row[2].length(); + row[2].normalize(); + values.skew[1] /= values.scale[2]; + values.skew[2] /= values.scale[2]; + + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + auto pdum3 = row[1].cross(row[2]); + if (row[0].dot(pdum3) < 0.f) { + for (int i = 0; i < 3; i++) { + values.scale[i] *= -1.f; + row[i][0] *= -1.f; + row[i][1] *= -1.f; + row[i][2] *= -1.f; + } + } + + // Now, get the rotations out + values.rotation[0] = 0.5f * sqrt(max(1.f + row[0][0] - row[1][1] - row[2][2], 0.f)); + values.rotation[1] = 0.5f * sqrt(max(1.f - row[0][0] + row[1][1] - row[2][2], 0.f)); + values.rotation[2] = 0.5f * sqrt(max(1.f - row[0][0] - row[1][1] + row[2][2], 0.f)); + values.rotation[3] = 0.5f * sqrt(max(1.f + row[0][0] + row[1][1] + row[2][2], 0.f)); + + if (row[2][1] > row[1][2]) + values.rotation[0] = -values.rotation[0]; + if (row[0][2] > row[2][0]) + values.rotation[1] = -values.rotation[1]; + if (row[1][0] > row[0][1]) + values.rotation[2] = -values.rotation[2]; + + // FIXME: This accounts for the fact that the browser coordinate system is left-handed instead of right-handed. + // The reason for this is that the positive Y-axis direction points down instead of up. To fix this, we + // invert the Y axis. However, it feels like the spec pseudo-code above should have taken something like + // this into account, so we're probably doing something else wrong. + values.rotation[2] *= -1; + + return values; + }; + + // https://drafts.csswg.org/css-transforms-2/#recomposing-to-a-3d-matrix + static constexpr auto recompose = [](DecomposedValues const& values) -> FloatMatrix4x4 { + auto matrix = FloatMatrix4x4::identity(); + + // apply perspective + for (int i = 0; i < 4; i++) + matrix(3, i) = values.perspective[i]; + + // apply translation + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 3; j++) + matrix(i, 3) += values.translation[j] * matrix(i, j); + } + + // apply rotation + auto x = values.rotation[0]; + auto y = values.rotation[1]; + auto z = values.rotation[2]; + auto w = values.rotation[3]; + + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is a identity 4x4 matrix initially + auto rotation_matrix = FloatMatrix4x4::identity(); + rotation_matrix(0, 0) = 1.f - 2.f * (y * y + z * z); + rotation_matrix(1, 0) = 2.f * (x * y - z * w); + rotation_matrix(2, 0) = 2.f * (x * z + y * w); + rotation_matrix(0, 1) = 2.f * (x * y + z * w); + rotation_matrix(1, 1) = 1.f - 2.f * (x * x + z * z); + rotation_matrix(2, 1) = 2.f * (y * z - x * w); + rotation_matrix(0, 2) = 2.f * (x * z - y * w); + rotation_matrix(1, 2) = 2.f * (y * z + x * w); + rotation_matrix(2, 2) = 1.f - 2.f * (x * x + y * y); + + matrix = matrix * rotation_matrix; + + // apply skew + // temp is a identity 4x4 matrix initially + auto temp = FloatMatrix4x4::identity(); + if (values.skew[2] != 0.f) { + temp(1, 2) = values.skew[2]; + matrix = matrix * temp; + } + + if (values.skew[1] != 0.f) { + temp(1, 2) = 0.f; + temp(0, 2) = values.skew[1]; + matrix = matrix * temp; + } + + if (values.skew[0] != 0.f) { + temp(0, 2) = 0.f; + temp(0, 1) = values.skew[0]; + matrix = matrix * temp; + } + + // apply scale + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 4; j++) + matrix(j, i) *= values.scale[i]; + } + + return matrix; + }; + + // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values + static constexpr auto interpolate = [](DecomposedValues& from, DecomposedValues& to, float delta) -> DecomposedValues { + auto product = clamp(from.rotation.dot(to.rotation), -1.0f, 1.0f); + FloatVector4 interpolated_rotation; + if (fabsf(product) == 1.0f) { + interpolated_rotation = from.rotation; + } else { + auto theta = acos(product); + auto w = sin(delta * theta) / sqrtf(1.0f - product * product); + + for (int i = 0; i < 4; i++) { + from.rotation[i] *= cos(delta * theta) - product * w; + to.rotation[i] *= w; + interpolated_rotation[i] = from.rotation[i] + to.rotation[i]; + } + } + + return { + interpolate_raw(from.translation, to.translation, delta), + interpolate_raw(from.scale, to.scale, delta), + interpolate_raw(from.skew, to.skew, delta), + interpolated_rotation, + interpolate_raw(from.perspective, to.perspective, delta), + }; + }; + + auto from_matrix = TRY(style_value_to_matrix(element, from)); + auto to_matrix = TRY(style_value_to_matrix(element, to)); + auto from_decomposed = TRY(decompose(from_matrix)); + auto to_decomposed = TRY(decompose(to_matrix)); + auto interpolated_decomposed = interpolate(from_decomposed, to_decomposed, delta); + auto interpolated = recompose(interpolated_decomposed); + + StyleValueVector values; + values.ensure_capacity(16); + for (int i = 0; i < 16; i++) + values.append(NumberStyleValue::create(static_cast(interpolated(i % 4, i / 4)))); + return StyleValueList::create({ TransformationStyleValue::create(TransformFunction::Matrix3d, move(values)) }, StyleValueList::Separator::Comma); +} + +static ErrorOr> interpolate_value(DOM::Element& element, StyleValue const& from, StyleValue const& to, float delta) { if (from.type() != to.type()) return delta >= 0.5f ? to : from; - auto interpolate_raw = [delta = static_cast(delta)](auto from, auto to) { - return static_cast>(static_cast(from) + static_cast(to - from) * delta); - }; - switch (from.type()) { case StyleValue::Type::Angle: - return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees()))); + return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta))); case StyleValue::Type::Color: { auto from_color = from.as_color().color(); auto to_color = to.as_color().color(); @@ -777,41 +1075,41 @@ static ErrorOr> interpolate_value(StyleValue con auto to_oklab = to_color.to_oklab(); auto color = Color::from_oklab( - interpolate_raw(from_oklab.L, to_oklab.L), - interpolate_raw(from_oklab.a, to_oklab.a), - interpolate_raw(from_oklab.b, to_oklab.b)); - color.set_alpha(interpolate_raw(from_color.alpha(), to_color.alpha())); + interpolate_raw(from_oklab.L, to_oklab.L, delta), + interpolate_raw(from_oklab.a, to_oklab.a, delta), + interpolate_raw(from_oklab.b, to_oklab.b, delta)); + color.set_alpha(interpolate_raw(from_color.alpha(), to_color.alpha(), delta)); return ColorStyleValue::create(color); } case StyleValue::Type::Integer: - return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer())); + return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); case StyleValue::Type::Length: { auto& from_length = from.as_length().length(); auto& to_length = to.as_length().length(); - return LengthStyleValue::create(Length(interpolate_raw(from_length.raw_value(), to_length.raw_value()), from_length.type())); + return LengthStyleValue::create(Length(interpolate_raw(from_length.raw_value(), to_length.raw_value(), delta), from_length.type())); } case StyleValue::Type::Number: - return NumberStyleValue::create(interpolate_raw(from.as_number().number(), to.as_number().number())); + return NumberStyleValue::create(interpolate_raw(from.as_number().number(), to.as_number().number(), delta)); case StyleValue::Type::Percentage: - return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value()))); + return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value(), delta))); case StyleValue::Type::Position: { // https://www.w3.org/TR/css-values-4/#combine-positions // FIXME: Interpolation of is defined as the independent interpolation of each component (x, y) normalized as an offset from the top left corner as a . auto& from_position = from.as_position(); auto& to_position = to.as_position(); return PositionStyleValue::create( - TRY(interpolate_value(from_position.edge_x(), to_position.edge_x(), delta))->as_edge(), - TRY(interpolate_value(from_position.edge_y(), to_position.edge_y(), delta))->as_edge()); + TRY(interpolate_value(element, from_position.edge_x(), to_position.edge_x(), delta))->as_edge(), + TRY(interpolate_value(element, from_position.edge_y(), to_position.edge_y(), delta))->as_edge()); } case StyleValue::Type::Rect: { auto from_rect = from.as_rect().rect(); auto to_rect = to.as_rect().rect(); return RectStyleValue::create({ - Length(interpolate_raw(from_rect.top_edge.raw_value(), to_rect.top_edge.raw_value()), from_rect.top_edge.type()), - Length(interpolate_raw(from_rect.right_edge.raw_value(), to_rect.right_edge.raw_value()), from_rect.right_edge.type()), - Length(interpolate_raw(from_rect.bottom_edge.raw_value(), to_rect.bottom_edge.raw_value()), from_rect.bottom_edge.type()), - Length(interpolate_raw(from_rect.left_edge.raw_value(), to_rect.left_edge.raw_value()), from_rect.left_edge.type()), + Length(interpolate_raw(from_rect.top_edge.raw_value(), to_rect.top_edge.raw_value(), delta), from_rect.top_edge.type()), + Length(interpolate_raw(from_rect.right_edge.raw_value(), to_rect.right_edge.raw_value(), delta), from_rect.right_edge.type()), + Length(interpolate_raw(from_rect.bottom_edge.raw_value(), to_rect.bottom_edge.raw_value(), delta), from_rect.bottom_edge.type()), + Length(interpolate_raw(from_rect.left_edge.raw_value(), to_rect.left_edge.raw_value(), delta), from_rect.left_edge.type()), }); } case StyleValue::Type::Transformation: @@ -825,7 +1123,7 @@ static ErrorOr> interpolate_value(StyleValue con StyleValueVector interpolated_values; interpolated_values.ensure_capacity(from_list.size()); for (size_t i = 0; i < from_list.size(); ++i) - interpolated_values.append(TRY(interpolate_value(from_list.values()[i], to_list.values()[i], delta))); + interpolated_values.append(TRY(interpolate_value(element, from_list.values()[i], to_list.values()[i], delta))); return StyleValueList::create(move(interpolated_values), from_list.separator()); } @@ -834,12 +1132,12 @@ static ErrorOr> interpolate_value(StyleValue con } } -static ErrorOr> interpolate_property(PropertyID property_id, StyleValue const& from, StyleValue const& to, float delta) +static ErrorOr> interpolate_property(DOM::Element& element, PropertyID property_id, StyleValue const& from, StyleValue const& to, float delta) { auto animation_type = animation_type_from_longhand_property(property_id); switch (animation_type) { case AnimationType::ByComputedValue: - return interpolate_value(from, to, delta); + return interpolate_value(element, from, to, delta); case AnimationType::None: return to; case AnimationType::Custom: { @@ -861,7 +1159,7 @@ static ErrorOr> interpolate_proper } } } - return from; + return interpolate_transform(element, from, to, delta); } // FIXME: Handle all custom animatable properties @@ -955,7 +1253,7 @@ ErrorOr StyleComputer::collect_animation_into(JS::NonnullGCPtrtarget(), it.key, *start, *end, progress_in_keyframe)); dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Interpolated value for property {} at {}: {} -> {} = {}", string_from_property_id(it.key), progress_in_keyframe, start->to_string(), end->to_string(), next_value->to_string()); style_properties.set_property(it.key, next_value); }