From 70d99db9924b068d2a20dd64fec2bd71f7d7b346 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 19 Sep 2024 12:58:38 +0100 Subject: [PATCH] LibWeb/CSS: Extract interpolation code into its own files Apart from shrinking StyleComputer a little, we need the ability to get the current value of a transition from outside of it. --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../Libraries/LibWeb/CSS/Interpolation.cpp | 601 ++++++++++++++++++ Userland/Libraries/LibWeb/CSS/Interpolation.h | 22 + .../Libraries/LibWeb/CSS/StyleComputer.cpp | 572 +---------------- 4 files changed, 625 insertions(+), 571 deletions(-) create mode 100644 Userland/Libraries/LibWeb/CSS/Interpolation.cpp create mode 100644 Userland/Libraries/LibWeb/CSS/Interpolation.h diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 94ac688b035..05e8807b47b 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -65,6 +65,7 @@ set(SOURCES CSS/Frequency.cpp CSS/GridTrackPlacement.cpp CSS/GridTrackSize.cpp + CSS/Interpolation.cpp CSS/Length.cpp CSS/LengthBox.cpp CSS/MediaList.cpp diff --git a/Userland/Libraries/LibWeb/CSS/Interpolation.cpp b/Userland/Libraries/LibWeb/CSS/Interpolation.cpp new file mode 100644 index 00000000000..44832b37629 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/Interpolation.cpp @@ -0,0 +1,601 @@ +/* + * Copyright (c) 2018-2023, Andreas Kling + * Copyright (c) 2021, the SerenityOS developers. + * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2024, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Interpolation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +template +static T interpolate_raw(T from, T to, float delta) +{ + if constexpr (AK::Detail::IsSame) { + return from + (to - from) * static_cast(delta); + } else { + return static_cast>(from + (to - from) * delta); + } +} + +ValueComparingRefPtr interpolate_property(DOM::Element& element, PropertyID property_id, CSSStyleValue const& from, CSSStyleValue const& to, float delta) +{ + auto animation_type = animation_type_from_longhand_property(property_id); + switch (animation_type) { + case AnimationType::ByComputedValue: + return interpolate_value(element, from, to, delta); + case AnimationType::None: + return to; + case AnimationType::Custom: { + if (property_id == PropertyID::Transform) { + if (auto interpolated_transform = interpolate_transform(element, from, to, delta)) + return *interpolated_transform; + + // https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms + // In some cases, an animation might cause a transformation matrix to be singular or non-invertible. + // For example, an animation in which scale moves from 1 to -1. At the time when the matrix is in + // such a state, the transformed element is not rendered. + return {}; + } + if (property_id == PropertyID::BoxShadow) + return interpolate_box_shadow(element, from, to, delta); + + // FIXME: Handle all custom animatable properties + [[fallthrough]]; + } + // FIXME: Handle repeatable-list animatable properties + case AnimationType::RepeatableList: + case AnimationType::Discrete: + default: + return delta >= 0.5f ? to : from; + } +} + +// A null return value means the interpolated matrix was not invertible or otherwise invalid +RefPtr interpolate_transform(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue 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) -> AK::Optional { + AK::Vector values; + + for (auto const& value : transformation.values()) { + switch (value->type()) { + case CSSStyleValue::Type::Angle: + values.append(AngleOrCalculated { value->as_angle().angle() }); + break; + case CSSStyleValue::Type::Math: + values.append(LengthPercentage { value->as_math() }); + break; + case CSSStyleValue::Type::Length: + values.append(LengthPercentage { value->as_length().length() }); + break; + case CSSStyleValue::Type::Percentage: + values.append(LengthPercentage { value->as_percentage().percentage() }); + break; + case CSSStyleValue::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 { + auto transformation = make_transformation(value); + if (!transformation.has_value()) + return {}; + Optional paintable_box; + if (auto layout_node = element.layout_node()) { + if (auto paintable = layout_node->paintable(); paintable && is(paintable)) + paintable_box = *static_cast(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, CSSStyleValue 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; + FloatVector3 skew; + FloatVector4 rotation; + FloatVector4 perspective; + }; + // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix + static constexpr auto decompose = [](FloatMatrix4x4 matrix) -> Optional { + // 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 {}; + + 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 {}; + + 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 = 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); + 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); + + 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); +} + +Color interpolate_color(Color from, Color to, float delta) +{ + // https://drafts.csswg.org/css-color/#interpolation-space + // If the host syntax does not define what color space interpolation should take place in, it defaults to Oklab. + auto from_oklab = from.to_oklab(); + auto to_oklab = to.to_oklab(); + + auto color = Color::from_oklab( + 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.alpha(), to.alpha(), delta)); + return color; +} + +NonnullRefPtr interpolate_box_shadow(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta) +{ + // https://drafts.csswg.org/css-backgrounds/#box-shadow + // Animation type: by computed value, treating none as a zero-item list and appending blank shadows + // (transparent 0 0 0 0) with a corresponding inset keyword as needed to match the longer list if + // the shorter list is otherwise compatible with the longer one + + static constexpr auto process_list = [](CSSStyleValue const& value) { + StyleValueVector shadows; + if (value.is_value_list()) { + for (auto const& element : value.as_value_list().values()) { + if (element->is_shadow()) + shadows.append(element); + } + } else if (value.is_shadow()) { + shadows.append(value); + } else if (!value.is_keyword() || value.as_keyword().keyword() != Keyword::None) { + VERIFY_NOT_REACHED(); + } + return shadows; + }; + + static constexpr auto extend_list_if_necessary = [](StyleValueVector& values, StyleValueVector const& other) { + values.ensure_capacity(other.size()); + for (size_t i = values.size(); i < other.size(); i++) { + values.unchecked_append(ShadowStyleValue::create( + CSSColorValue::create_from_color(Color::Transparent), + LengthStyleValue::create(Length::make_px(0)), + LengthStyleValue::create(Length::make_px(0)), + LengthStyleValue::create(Length::make_px(0)), + LengthStyleValue::create(Length::make_px(0)), + other[i]->as_shadow().placement())); + } + }; + + StyleValueVector from_shadows = process_list(from); + StyleValueVector to_shadows = process_list(to); + + extend_list_if_necessary(from_shadows, to_shadows); + extend_list_if_necessary(to_shadows, from_shadows); + + VERIFY(from_shadows.size() == to_shadows.size()); + StyleValueVector result_shadows; + result_shadows.ensure_capacity(from_shadows.size()); + + for (size_t i = 0; i < from_shadows.size(); i++) { + auto const& from_shadow = from_shadows[i]->as_shadow(); + auto const& to_shadow = to_shadows[i]->as_shadow(); + auto result_shadow = ShadowStyleValue::create( + CSSColorValue::create_from_color(interpolate_color(from_shadow.color()->to_color({}), to_shadow.color()->to_color({}), delta)), + interpolate_value(element, from_shadow.offset_x(), to_shadow.offset_x(), delta), + interpolate_value(element, from_shadow.offset_y(), to_shadow.offset_y(), delta), + interpolate_value(element, from_shadow.blur_radius(), to_shadow.blur_radius(), delta), + interpolate_value(element, from_shadow.spread_distance(), to_shadow.spread_distance(), delta), + delta >= 0.5f ? to_shadow.placement() : from_shadow.placement()); + result_shadows.unchecked_append(result_shadow); + } + + return StyleValueList::create(move(result_shadows), StyleValueList::Separator::Comma); +} + +NonnullRefPtr interpolate_value(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta) +{ + if (from.type() != to.type()) { + // Handle mixed percentage and dimension types + // https://www.w3.org/TR/css-values-4/#mixed-percentages + + struct NumericBaseTypeAndDefault { + CSSNumericType::BaseType base_type; + ValueComparingNonnullRefPtr default_value; + }; + static constexpr auto numeric_base_type_and_default = [](CSSStyleValue const& value) -> Optional { + switch (value.type()) { + case CSSStyleValue::Type::Angle: { + static auto default_angle_value = AngleStyleValue::create(Angle::make_degrees(0)); + return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Angle, default_angle_value }; + } + case CSSStyleValue::Type::Frequency: { + static auto default_frequency_value = FrequencyStyleValue::create(Frequency::make_hertz(0)); + return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Frequency, default_frequency_value }; + } + case CSSStyleValue::Type::Length: { + static auto default_length_value = LengthStyleValue::create(Length::make_px(0)); + return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Length, default_length_value }; + } + case CSSStyleValue::Type::Percentage: { + static auto default_percentage_value = PercentageStyleValue::create(Percentage { 0.0 }); + return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Percent, default_percentage_value }; + } + case CSSStyleValue::Type::Time: { + static auto default_time_value = TimeStyleValue::create(Time::make_seconds(0)); + return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Time, default_time_value }; + } + default: + return {}; + } + }; + + static constexpr auto to_calculation_node = [](CSSStyleValue const& value) -> NonnullOwnPtr { + switch (value.type()) { + case CSSStyleValue::Type::Angle: + return NumericCalculationNode::create(value.as_angle().angle()); + case CSSStyleValue::Type::Frequency: + return NumericCalculationNode::create(value.as_frequency().frequency()); + case CSSStyleValue::Type::Length: + return NumericCalculationNode::create(value.as_length().length()); + case CSSStyleValue::Type::Percentage: + return NumericCalculationNode::create(value.as_percentage().percentage()); + case CSSStyleValue::Type::Time: + return NumericCalculationNode::create(value.as_time().time()); + default: + VERIFY_NOT_REACHED(); + } + }; + + auto from_base_type_and_default = numeric_base_type_and_default(from); + auto to_base_type_and_default = numeric_base_type_and_default(to); + + if (from_base_type_and_default.has_value() && to_base_type_and_default.has_value() && (from_base_type_and_default->base_type == CSSNumericType::BaseType::Percent || to_base_type_and_default->base_type == CSSNumericType::BaseType::Percent)) { + // This is an interpolation from a numeric unit to a percentage, or vice versa. The trick here is to + // interpolate two separate values. For example, consider an interpolation from 30px to 80%. It's quite + // hard to understand how this interpolation works, but if instead we rewrite the values as "30px + 0%" and + // "0px + 80%", then it is very simple to understand; we just interpolate each component separately. + + auto interpolated_from = interpolate_value(element, from, from_base_type_and_default->default_value, delta); + auto interpolated_to = interpolate_value(element, to_base_type_and_default->default_value, to, delta); + + Vector> values; + values.ensure_capacity(2); + values.unchecked_append(to_calculation_node(interpolated_from)); + values.unchecked_append(to_calculation_node(interpolated_to)); + auto calc_node = SumCalculationNode::create(move(values)); + return CSSMathValue::create(move(calc_node), CSSNumericType { to_base_type_and_default->base_type, 1 }); + } + + return delta >= 0.5f ? to : from; + } + + switch (from.type()) { + case CSSStyleValue::Type::Angle: + return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta))); + case CSSStyleValue::Type::Color: { + Optional layout_node; + if (auto node = element.layout_node()) + layout_node = *node; + return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta)); + } + case CSSStyleValue::Type::Integer: + return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); + case CSSStyleValue::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(), delta), from_length.type())); + } + case CSSStyleValue::Type::Number: + return NumberStyleValue::create(interpolate_raw(from.as_number().number(), to.as_number().number(), delta)); + case CSSStyleValue::Type::Percentage: + return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value(), delta))); + case CSSStyleValue::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( + interpolate_value(element, from_position.edge_x(), to_position.edge_x(), delta)->as_edge(), + interpolate_value(element, from_position.edge_y(), to_position.edge_y(), delta)->as_edge()); + } + case CSSStyleValue::Type::Ratio: { + auto from_ratio = from.as_ratio().ratio(); + auto to_ratio = to.as_ratio().ratio(); + + // The interpolation of a is defined by converting each to a number by dividing the first value + // by the second (so a ratio of 3 / 2 would become 1.5), taking the logarithm of that result (so the 1.5 would + // become approximately 0.176), then interpolating those values. The result during the interpolation is + // converted back to a by inverting the logarithm, then interpreting the result as a with the + // result as the first value and 1 as the second value. + auto from_number = log(from_ratio.value()); + auto to_number = log(to_ratio.value()); + auto interp_number = interpolate_raw(from_number, to_number, delta); + return RatioStyleValue::create(Ratio(pow(M_E, interp_number))); + } + case CSSStyleValue::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(), 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 CSSStyleValue::Type::Transformation: + VERIFY_NOT_REACHED(); + case CSSStyleValue::Type::ValueList: { + auto& from_list = from.as_value_list(); + auto& to_list = to.as_value_list(); + if (from_list.size() != to_list.size()) + return from; + + StyleValueVector interpolated_values; + interpolated_values.ensure_capacity(from_list.size()); + for (size_t i = 0; i < from_list.size(); ++i) + interpolated_values.append(interpolate_value(element, from_list.values()[i], to_list.values()[i], delta)); + + return StyleValueList::create(move(interpolated_values), from_list.separator()); + } + default: + return from; + } +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/Interpolation.h b/Userland/Libraries/LibWeb/CSS/Interpolation.h new file mode 100644 index 00000000000..cf17a33c897 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/Interpolation.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +ValueComparingRefPtr interpolate_property(DOM::Element&, PropertyID, CSSStyleValue const& from, CSSStyleValue const& to, float delta); + +NonnullRefPtr interpolate_value(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); +NonnullRefPtr interpolate_box_shadow(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); +RefPtr interpolate_transform(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); + +Color interpolate_color(Color from, Color to, float delta); + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 795c859ec46..759ad80a9cf 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -1024,577 +1025,6 @@ static void cascade_custom_properties(DOM::Element& element, Optional interpolate_value(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue 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); - } -} - -// A null return value means the interpolated matrix was not invertible or otherwise invalid -static RefPtr interpolate_transform(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue 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 { - Vector values; - - for (auto const& value : transformation.values()) { - switch (value->type()) { - case CSSStyleValue::Type::Angle: - values.append(AngleOrCalculated { value->as_angle().angle() }); - break; - case CSSStyleValue::Type::Math: - values.append(LengthPercentage { value->as_math() }); - break; - case CSSStyleValue::Type::Length: - values.append(LengthPercentage { value->as_length().length() }); - break; - case CSSStyleValue::Type::Percentage: - values.append(LengthPercentage { value->as_percentage().percentage() }); - break; - case CSSStyleValue::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 { - auto transformation = make_transformation(value.as_transformation()); - if (!transformation.has_value()) - return {}; - Optional paintable_box; - if (auto layout_node = element.layout_node()) { - if (auto paintable = layout_node->paintable(); paintable && is(paintable)) - paintable_box = *static_cast(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, CSSStyleValue 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; - FloatVector3 skew; - FloatVector4 rotation; - FloatVector4 perspective; - }; - // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix - static constexpr auto decompose = [](FloatMatrix4x4 matrix) -> Optional { - // 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 {}; - - 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 {}; - - 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 = 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); - 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); - - 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 Color interpolate_color(Color from, Color to, float delta) -{ - // https://drafts.csswg.org/css-color/#interpolation-space - // If the host syntax does not define what color space interpolation should take place in, it defaults to Oklab. - auto from_oklab = from.to_oklab(); - auto to_oklab = to.to_oklab(); - - auto color = Color::from_oklab( - 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.alpha(), to.alpha(), delta)); - return color; -} - -static NonnullRefPtr interpolate_box_shadow(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta) -{ - // https://drafts.csswg.org/css-backgrounds/#box-shadow - // Animation type: by computed value, treating none as a zero-item list and appending blank shadows - // (transparent 0 0 0 0) with a corresponding inset keyword as needed to match the longer list if - // the shorter list is otherwise compatible with the longer one - - static constexpr auto process_list = [](CSSStyleValue const& value) { - StyleValueVector shadows; - if (value.is_value_list()) { - for (auto const& element : value.as_value_list().values()) { - if (element->is_shadow()) - shadows.append(element); - } - } else if (value.is_shadow()) { - shadows.append(value); - } else if (!value.is_keyword() || value.as_keyword().keyword() != Keyword::None) { - VERIFY_NOT_REACHED(); - } - return shadows; - }; - - static constexpr auto extend_list_if_necessary = [](StyleValueVector& values, StyleValueVector const& other) { - values.ensure_capacity(other.size()); - for (size_t i = values.size(); i < other.size(); i++) { - values.unchecked_append(ShadowStyleValue::create( - CSSColorValue::create_from_color(Color::Transparent), - LengthStyleValue::create(Length::make_px(0)), - LengthStyleValue::create(Length::make_px(0)), - LengthStyleValue::create(Length::make_px(0)), - LengthStyleValue::create(Length::make_px(0)), - other[i]->as_shadow().placement())); - } - }; - - StyleValueVector from_shadows = process_list(from); - StyleValueVector to_shadows = process_list(to); - - extend_list_if_necessary(from_shadows, to_shadows); - extend_list_if_necessary(to_shadows, from_shadows); - - VERIFY(from_shadows.size() == to_shadows.size()); - StyleValueVector result_shadows; - result_shadows.ensure_capacity(from_shadows.size()); - - for (size_t i = 0; i < from_shadows.size(); i++) { - auto const& from_shadow = from_shadows[i]->as_shadow(); - auto const& to_shadow = to_shadows[i]->as_shadow(); - auto result_shadow = ShadowStyleValue::create( - CSSColorValue::create_from_color(interpolate_color(from_shadow.color()->to_color({}), to_shadow.color()->to_color({}), delta)), - interpolate_value(element, from_shadow.offset_x(), to_shadow.offset_x(), delta), - interpolate_value(element, from_shadow.offset_y(), to_shadow.offset_y(), delta), - interpolate_value(element, from_shadow.blur_radius(), to_shadow.blur_radius(), delta), - interpolate_value(element, from_shadow.spread_distance(), to_shadow.spread_distance(), delta), - delta >= 0.5f ? to_shadow.placement() : from_shadow.placement()); - result_shadows.unchecked_append(result_shadow); - } - - return StyleValueList::create(move(result_shadows), StyleValueList::Separator::Comma); -} - -static NonnullRefPtr interpolate_value(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta) -{ - if (from.type() != to.type()) { - // Handle mixed percentage and dimension types - // https://www.w3.org/TR/css-values-4/#mixed-percentages - - struct NumericBaseTypeAndDefault { - CSSNumericType::BaseType base_type; - ValueComparingNonnullRefPtr default_value; - }; - static constexpr auto numeric_base_type_and_default = [](CSSStyleValue const& value) -> Optional { - switch (value.type()) { - case CSSStyleValue::Type::Angle: { - static auto default_angle_value = AngleStyleValue::create(Angle::make_degrees(0)); - return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Angle, default_angle_value }; - } - case CSSStyleValue::Type::Frequency: { - static auto default_frequency_value = FrequencyStyleValue::create(Frequency::make_hertz(0)); - return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Frequency, default_frequency_value }; - } - case CSSStyleValue::Type::Length: { - static auto default_length_value = LengthStyleValue::create(Length::make_px(0)); - return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Length, default_length_value }; - } - case CSSStyleValue::Type::Percentage: { - static auto default_percentage_value = PercentageStyleValue::create(Percentage { 0.0 }); - return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Percent, default_percentage_value }; - } - case CSSStyleValue::Type::Time: { - static auto default_time_value = TimeStyleValue::create(Time::make_seconds(0)); - return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Time, default_time_value }; - } - default: - return {}; - } - }; - - static constexpr auto to_calculation_node = [](CSSStyleValue const& value) -> NonnullOwnPtr { - switch (value.type()) { - case CSSStyleValue::Type::Angle: - return NumericCalculationNode::create(value.as_angle().angle()); - case CSSStyleValue::Type::Frequency: - return NumericCalculationNode::create(value.as_frequency().frequency()); - case CSSStyleValue::Type::Length: - return NumericCalculationNode::create(value.as_length().length()); - case CSSStyleValue::Type::Percentage: - return NumericCalculationNode::create(value.as_percentage().percentage()); - case CSSStyleValue::Type::Time: - return NumericCalculationNode::create(value.as_time().time()); - default: - VERIFY_NOT_REACHED(); - } - }; - - auto from_base_type_and_default = numeric_base_type_and_default(from); - auto to_base_type_and_default = numeric_base_type_and_default(to); - - if (from_base_type_and_default.has_value() && to_base_type_and_default.has_value() && (from_base_type_and_default->base_type == CSSNumericType::BaseType::Percent || to_base_type_and_default->base_type == CSSNumericType::BaseType::Percent)) { - // This is an interpolation from a numeric unit to a percentage, or vice versa. The trick here is to - // interpolate two separate values. For example, consider an interpolation from 30px to 80%. It's quite - // hard to understand how this interpolation works, but if instead we rewrite the values as "30px + 0%" and - // "0px + 80%", then it is very simple to understand; we just interpolate each component separately. - - auto interpolated_from = interpolate_value(element, from, from_base_type_and_default->default_value, delta); - auto interpolated_to = interpolate_value(element, to_base_type_and_default->default_value, to, delta); - - Vector> values; - values.ensure_capacity(2); - values.unchecked_append(to_calculation_node(interpolated_from)); - values.unchecked_append(to_calculation_node(interpolated_to)); - auto calc_node = SumCalculationNode::create(move(values)); - return CSSMathValue::create(move(calc_node), CSSNumericType { to_base_type_and_default->base_type, 1 }); - } - - return delta >= 0.5f ? to : from; - } - - switch (from.type()) { - case CSSStyleValue::Type::Angle: - return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta))); - case CSSStyleValue::Type::Color: { - Optional layout_node; - if (auto node = element.layout_node()) - layout_node = *node; - return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta)); - } - case CSSStyleValue::Type::Integer: - return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); - case CSSStyleValue::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(), delta), from_length.type())); - } - case CSSStyleValue::Type::Number: - return NumberStyleValue::create(interpolate_raw(from.as_number().number(), to.as_number().number(), delta)); - case CSSStyleValue::Type::Percentage: - return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value(), delta))); - case CSSStyleValue::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( - interpolate_value(element, from_position.edge_x(), to_position.edge_x(), delta)->as_edge(), - interpolate_value(element, from_position.edge_y(), to_position.edge_y(), delta)->as_edge()); - } - case CSSStyleValue::Type::Ratio: { - auto from_ratio = from.as_ratio().ratio(); - auto to_ratio = to.as_ratio().ratio(); - - // The interpolation of a is defined by converting each to a number by dividing the first value - // by the second (so a ratio of 3 / 2 would become 1.5), taking the logarithm of that result (so the 1.5 would - // become approximately 0.176), then interpolating those values. The result during the interpolation is - // converted back to a by inverting the logarithm, then interpreting the result as a with the - // result as the first value and 1 as the second value. - auto from_number = log(from_ratio.value()); - auto to_number = log(to_ratio.value()); - auto interp_number = interpolate_raw(from_number, to_number, delta); - return RatioStyleValue::create(Ratio(pow(M_E, interp_number))); - } - case CSSStyleValue::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(), 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 CSSStyleValue::Type::Transformation: - VERIFY_NOT_REACHED(); - case CSSStyleValue::Type::ValueList: { - auto& from_list = from.as_value_list(); - auto& to_list = to.as_value_list(); - if (from_list.size() != to_list.size()) - return from; - - StyleValueVector interpolated_values; - interpolated_values.ensure_capacity(from_list.size()); - for (size_t i = 0; i < from_list.size(); ++i) - interpolated_values.append(interpolate_value(element, from_list.values()[i], to_list.values()[i], delta)); - - return StyleValueList::create(move(interpolated_values), from_list.separator()); - } - default: - return from; - } -} - -static ValueComparingRefPtr interpolate_property(DOM::Element& element, PropertyID property_id, CSSStyleValue const& from, CSSStyleValue const& to, float delta) -{ - auto animation_type = animation_type_from_longhand_property(property_id); - switch (animation_type) { - case AnimationType::ByComputedValue: - return interpolate_value(element, from, to, delta); - case AnimationType::None: - return to; - case AnimationType::Custom: { - if (property_id == PropertyID::Transform) { - if (auto interpolated_transform = interpolate_transform(element, from, to, delta)) - return *interpolated_transform; - - // https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms - // In some cases, an animation might cause a transformation matrix to be singular or non-invertible. - // For example, an animation in which scale moves from 1 to -1. At the time when the matrix is in - // such a state, the transformed element is not rendered. - return {}; - } - if (property_id == PropertyID::BoxShadow) - return interpolate_box_shadow(element, from, to, delta); - - // FIXME: Handle all custom animatable properties - [[fallthrough]]; - } - // FIXME: Handle repeatable-list animatable properties - case AnimationType::RepeatableList: - case AnimationType::Discrete: - default: - return delta >= 0.5f ? to : from; - } -} - void StyleComputer::collect_animation_into(DOM::Element& element, Optional pseudo_element, JS::NonnullGCPtr effect, StyleProperties& style_properties, AnimationRefresh refresh) const { auto animation = effect->associated_animation();