/* * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2025-2026, Tim Ledbetter * Copyright (c) 2025, Jelle Raaijmakers * * 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 #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, Optional accepted_type_range = {}) { if constexpr (AK::Detail::IsSame) { if (accepted_type_range.has_value()) return clamp(from + (to - from) * static_cast(delta), accepted_type_range->min, accepted_type_range->max); return from + (to - from) * static_cast(delta); } else if constexpr (AK::Detail::IsIntegral) { auto from_float = static_cast(from); auto to_float = static_cast(to); auto min = accepted_type_range.has_value() ? accepted_type_range->min : NumericLimits::min(); auto max = accepted_type_range.has_value() ? accepted_type_range->max : NumericLimits::max(); auto unclamped_result = roundf(from_float + (to_float - from_float) * delta); return static_cast>(clamp(unclamped_result, min, max)); } VERIFY(!accepted_type_range.has_value()); return static_cast>(from + (to - from) * delta); } static NonnullRefPtr with_keyword_values_resolved(DOM::Element& element, PropertyID property_id, StyleValue const& value) { if (value.is_guaranteed_invalid()) { // At the moment, we're only dealing with "real" properties, so this behaves the same as `unset`. // https://drafts.csswg.org/css-values-5/#invalid-at-computed-value-time return property_initial_value(property_id); } if (!value.is_keyword()) return value; switch (value.as_keyword().keyword()) { case Keyword::Initial: case Keyword::Unset: return property_initial_value(property_id); case Keyword::Inherit: return StyleComputer::get_non_animated_inherit_value(property_id, { element }); default: break; } return value; } static RefPtr interpolate_discrete(StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { if (from.equals(to)) return from; if (allow_discrete == AllowDiscrete::No) return {}; return delta >= 0.5f ? to : from; } static RefPtr interpolate_scale(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& a_from, StyleValue const& a_to, float delta, AllowDiscrete allow_discrete) { if (a_from.to_keyword() == Keyword::None && a_to.to_keyword() == Keyword::None) return a_from; static auto one = TransformationStyleValue::create(PropertyID::Scale, TransformFunction::Scale, { NumberStyleValue::create(1), NumberStyleValue::create(1) }); auto const& from = a_from.to_keyword() == Keyword::None ? *one : a_from; auto const& to = a_to.to_keyword() == Keyword::None ? *one : a_to; auto const& from_transform = from.as_transformation(); auto const& to_transform = to.as_transformation(); auto interpolated_x = interpolate_value(element, calculation_context, from_transform.values()[0], to_transform.values()[0], delta, allow_discrete); if (!interpolated_x) return {}; auto interpolated_y = interpolate_value(element, calculation_context, from_transform.values()[1], to_transform.values()[1], delta, allow_discrete); if (!interpolated_y) return {}; RefPtr interpolated_z; if (from_transform.values().size() == 3 || to_transform.values().size() == 3) { static auto one_value = NumberStyleValue::create(1); auto from = from_transform.values().size() == 3 ? from_transform.values()[2] : one_value; auto to = to_transform.values().size() == 3 ? to_transform.values()[2] : one_value; interpolated_z = interpolate_value(element, calculation_context, from, to, delta, allow_discrete); if (!interpolated_z) return {}; } StyleValueVector new_values = { *interpolated_x, *interpolated_y }; if (interpolated_z) new_values.append(*interpolated_z); return TransformationStyleValue::create( PropertyID::Scale, new_values.size() == 3 ? TransformFunction::Scale3d : TransformFunction::Scale, move(new_values)); } // https://drafts.fxtf.org/filter-effects/#interpolation-of-filter-functions static Optional interpolate_filter_function(DOM::Element& element, CalculationContext const& calculation_context, FilterValue const& from, FilterValue const& to, float delta, AllowDiscrete allow_discrete) { VERIFY(!from.has()); VERIFY(!to.has()); if (from.index() != to.index()) { return {}; } auto result = from.visit( [&](FilterOperation::Blur const& from_value) -> Optional { auto const& to_value = to.get(); CalculationContext blur_calculation_context = calculation_context; blur_calculation_context.accepted_type_ranges.set(ValueType::Length, { 0, NumericLimits::max() }); if (auto interpolated_style_value = interpolate_value(element, blur_calculation_context, from_value.radius, to_value.radius, delta, allow_discrete)) { return FilterOperation::Blur { .radius = interpolated_style_value.release_nonnull() }; } return {}; }, [&](FilterOperation::HueRotate const& from_value) -> Optional { auto const& to_value = to.get(); if (auto interpolated_style_value = interpolate_value(element, calculation_context, from_value.angle, to_value.angle, delta, allow_discrete)) { return FilterOperation::HueRotate { .angle = interpolated_style_value.release_nonnull(), }; } return {}; }, [&](FilterOperation::Color const& from_value) -> Optional { auto const& to_value = to.get(); auto operation = delta >= 0.5f ? to_value.operation : from_value.operation; CalculationContext filter_function_calculation_context = calculation_context; switch (operation) { case Gfx::ColorFilterType::Grayscale: case Gfx::ColorFilterType::Invert: case Gfx::ColorFilterType::Opacity: case Gfx::ColorFilterType::Sepia: filter_function_calculation_context.accepted_type_ranges.set(ValueType::Number, { 0, 1 }); break; case Gfx::ColorFilterType::Brightness: case Gfx::ColorFilterType::Contrast: case Gfx::ColorFilterType::Saturate: filter_function_calculation_context.accepted_type_ranges.set(ValueType::Number, { 0, NumericLimits::max() }); break; } if (auto interpolated_style_value = interpolate_value(element, filter_function_calculation_context, from_value.amount, to_value.amount, delta, allow_discrete)) { return FilterOperation::Color { .operation = operation, .amount = *interpolated_style_value }; } return {}; }, [&](FilterOperation::DropShadow const& from_value) -> Optional { auto const& to_value = to.get(); auto drop_shadow_to_shadow_style_value = [](FilterOperation::DropShadow const& drop_shadow) { return ShadowStyleValue::create( ShadowStyleValue::ShadowType::Normal, drop_shadow.color, drop_shadow.offset_x, drop_shadow.offset_y, drop_shadow.radius, LengthStyleValue::create(Length::make_px(0)), ShadowPlacement::Outer); }; StyleValueVector from_shadows { drop_shadow_to_shadow_style_value(from_value) }; StyleValueVector to_shadows { drop_shadow_to_shadow_style_value(to_value) }; auto from_list = StyleValueList::create(move(from_shadows), StyleValueList::Separator::Comma); auto to_list = StyleValueList::create(move(to_shadows), StyleValueList::Separator::Comma); auto result = interpolate_box_shadow(element, calculation_context, *from_list, *to_list, delta, allow_discrete); if (!result) return {}; auto const& result_shadow = result->as_value_list().value_at(0, false)->as_shadow(); RefPtr result_radius; auto radius_has_value = delta >= 0.5f ? to_value.radius : from_value.radius; if (radius_has_value) result_radius = result_shadow.blur_radius(); return FilterOperation::DropShadow { .offset_x = result_shadow.offset_x(), .offset_y = result_shadow.offset_y(), .radius = result_radius, // FIXME: We shouldn't apply the default color here .color = result_shadow.color() }; }, [](URL const&) -> Optional { // URL filters cannot be interpolated return {}; }); return result; } // https://drafts.fxtf.org/filter-effects/#interpolation-of-filters static RefPtr interpolate_filter_value_list(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& a_from, StyleValue const& a_to, float delta, AllowDiscrete allow_discrete) { auto is_filter_value_list_without_url = [](StyleValue const& value) { if (!value.is_filter_value_list()) return false; auto const& filter_value_list = value.as_filter_value_list(); return !filter_value_list.contains_url(); }; auto initial_value_for = [&](FilterValue value) { return value.visit([&](FilterOperation::Blur const&) -> FilterValue { return FilterOperation::Blur { LengthStyleValue::create(Length::make_px(0)) }; }, [&](FilterOperation::DropShadow const&) -> FilterValue { return FilterOperation::DropShadow { .offset_x = LengthStyleValue::create(Length::make_px(0)), .offset_y = LengthStyleValue::create(Length::make_px(0)), .radius = LengthStyleValue::create(Length::make_px(0)), .color = ColorStyleValue::create_from_color(Color::Transparent, ColorSyntax::Legacy) }; }, [&](FilterOperation::HueRotate const&) -> FilterValue { return FilterOperation::HueRotate { AngleStyleValue::create(Angle::make_degrees(0)) }; }, [&](FilterOperation::Color const& color) -> FilterValue { auto default_value_for_interpolation = [&]() { switch (color.operation) { case Gfx::ColorFilterType::Grayscale: case Gfx::ColorFilterType::Invert: case Gfx::ColorFilterType::Sepia: return 0.0; case Gfx::ColorFilterType::Brightness: case Gfx::ColorFilterType::Contrast: case Gfx::ColorFilterType::Opacity: case Gfx::ColorFilterType::Saturate: return 1.0; } VERIFY_NOT_REACHED(); }(); return FilterOperation::Color { .operation = color.operation, .amount = NumberStyleValue::create(default_value_for_interpolation) }; }, [&](auto&) -> FilterValue { VERIFY_NOT_REACHED(); }); }; auto interpolate_filter_values = [&](StyleValue const& from, StyleValue const& to) -> RefPtr { auto const& from_filter_values = from.as_filter_value_list().filter_value_list(); auto const& to_filter_values = to.as_filter_value_list().filter_value_list(); Vector interpolated_filter_values; for (size_t i = 0; i < from.as_filter_value_list().size(); ++i) { auto const& from_value = from_filter_values[i]; auto const& to_value = to_filter_values[i]; auto interpolated_value = interpolate_filter_function(element, calculation_context, from_value, to_value, delta, allow_discrete); if (!interpolated_value.has_value()) return {}; interpolated_filter_values.append(interpolated_value.release_value()); } return FilterValueListStyleValue::create(move(interpolated_filter_values)); }; if (is_filter_value_list_without_url(a_from) && is_filter_value_list_without_url(a_to)) { auto const& from_list = a_from.as_filter_value_list(); auto const& to_list = a_to.as_filter_value_list(); // If both filters have a of same length without and for each for which there is a corresponding item in each list if (from_list.size() == to_list.size()) { // Interpolate each pair following the rules in section Interpolation of Filter Functions. return interpolate_filter_values(a_from, a_to); } // If both filters have a of different length without and for each for which there is a corresponding item in each list // 1. Append the missing equivalent s from the longer list to the end of the shorter list. The new added s must be initialized to their initial values for interpolation. auto append_missing_values_to = [&](FilterValueListStyleValue const& short_list, FilterValueListStyleValue const& longer_list) -> ValueComparingNonnullRefPtr { Vector new_filter_list = short_list.filter_value_list(); for (size_t i = new_filter_list.size(); i < longer_list.size(); ++i) { auto const& filter_value = longer_list.filter_value_list()[i]; new_filter_list.append(initial_value_for(filter_value)); } return FilterValueListStyleValue::create(move(new_filter_list)); }; ValueComparingNonnullRefPtr from = from_list.size() < to_list.size() ? append_missing_values_to(from_list, to_list) : a_from; ValueComparingNonnullRefPtr to = to_list.size() < from_list.size() ? append_missing_values_to(to_list, from_list) : a_to; // 2. Interpolate each pair following the rules in section Interpolation of Filter Functions. return interpolate_filter_values(from, to); } // If one filter is none and the other is a without if ((is_filter_value_list_without_url(a_from) && a_to.to_keyword() == Keyword::None) || (is_filter_value_list_without_url(a_to) && a_from.to_keyword() == Keyword::None)) { // 1. Replace none with the corresponding of the other filter. The new s must be initialized to their initial values for interpolation. auto replace_none_with_initial_filter_list_values = [&](FilterValueListStyleValue const& filter_value_list) { Vector initial_values; for (auto const& filter_value : filter_value_list.filter_value_list()) { initial_values.append(initial_value_for(filter_value)); } return FilterValueListStyleValue::create(move(initial_values)); }; ValueComparingNonnullRefPtr from = a_from.is_keyword() ? replace_none_with_initial_filter_list_values(a_to.as_filter_value_list()) : a_from; ValueComparingNonnullRefPtr to = a_to.is_keyword() ? replace_none_with_initial_filter_list_values(a_from.as_filter_value_list()) : a_to; // 2. Interpolate each pair following the rules in section Interpolation of Filter Functions. return interpolate_filter_values(from, to); } // Otherwise: // Use discrete interpolation return {}; } static RefPtr interpolate_translate(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& a_from, StyleValue const& a_to, float delta, AllowDiscrete allow_discrete) { if (a_from.to_keyword() == Keyword::None && a_to.to_keyword() == Keyword::None) return a_from; static auto zero_px = LengthStyleValue::create(Length::make_px(0)); static auto zero = TransformationStyleValue::create(PropertyID::Translate, TransformFunction::Translate, { zero_px, zero_px }); auto const& from = a_from.to_keyword() == Keyword::None ? *zero : a_from; auto const& to = a_to.to_keyword() == Keyword::None ? *zero : a_to; auto const& from_transform = from.as_transformation(); auto const& to_transform = to.as_transformation(); auto interpolated_x = interpolate_value(element, calculation_context, from_transform.values()[0], to_transform.values()[0], delta, allow_discrete); if (!interpolated_x) return {}; auto interpolated_y = interpolate_value(element, calculation_context, from_transform.values()[1], to_transform.values()[1], delta, allow_discrete); if (!interpolated_y) return {}; RefPtr interpolated_z; if (from_transform.values().size() == 3 || to_transform.values().size() == 3) { auto from_z = from_transform.values().size() == 3 ? from_transform.values()[2] : zero_px; auto to_z = to_transform.values().size() == 3 ? to_transform.values()[2] : zero_px; interpolated_z = interpolate_value(element, calculation_context, from_z, to_z, delta, allow_discrete); if (!interpolated_z) return {}; } StyleValueVector new_values = { *interpolated_x, *interpolated_y }; if (interpolated_z) new_values.append(*interpolated_z); return TransformationStyleValue::create( PropertyID::Translate, new_values.size() == 3 ? TransformFunction::Translate3d : TransformFunction::Translate, move(new_values)); } // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values static FloatVector4 slerp(FloatVector4 const& from, FloatVector4 const& to, float delta) { auto product = from.dot(to); product = clamp(product, -1.0f, 1.0f); if (fabsf(product) >= 1.0f) return from; auto theta = acosf(product); auto w = sinf(delta * theta) / sqrtf(1 - (product * product)); auto from_multiplier = cosf(delta * theta) - (product * w); if (abs(w) < AK::NumericLimits::epsilon()) return from * from_multiplier; if (abs(from_multiplier) < AK::NumericLimits::epsilon()) return to * w; return from * from_multiplier + to * w; } static RefPtr interpolate_rotate(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& a_from, StyleValue const& a_to, float delta, AllowDiscrete allow_discrete) { if (a_from.to_keyword() == Keyword::None && a_to.to_keyword() == Keyword::None) return a_from; static auto zero_degrees_value = AngleStyleValue::create(Angle::make_degrees(0)); static auto zero = TransformationStyleValue::create(PropertyID::Rotate, TransformFunction::Rotate, { zero_degrees_value }); auto const& from = a_from.to_keyword() == Keyword::None ? *zero : a_from; auto const& to = a_to.to_keyword() == Keyword::None ? *zero : a_to; auto const& from_transform = from.as_transformation(); auto const& to_transform = to.as_transformation(); auto from_transform_type = from_transform.transform_function(); auto to_transform_type = to_transform.transform_function(); if (from_transform_type == to_transform_type && from_transform.values().size() == 1) { auto interpolated_angle = interpolate_value(element, calculation_context, from_transform.values()[0], to_transform.values()[0], delta, allow_discrete); if (!interpolated_angle) return {}; return TransformationStyleValue::create(PropertyID::Rotate, from_transform_type, { *interpolated_angle.release_nonnull() }); } FloatVector3 from_axis { 0, 0, 1 }; auto from_angle_value = from_transform.values()[0]; if (from_transform.values().size() == 4) { from_axis.set_x(from_transform.values()[0]->as_number().number()); from_axis.set_y(from_transform.values()[1]->as_number().number()); from_axis.set_z(from_transform.values()[2]->as_number().number()); from_angle_value = from_transform.values()[3]; } float from_angle = Angle::from_style_value(from_angle_value, {}).to_radians(); FloatVector3 to_axis { 0, 0, 1 }; auto to_angle_value = to_transform.values()[0]; if (to_transform.values().size() == 4) { to_axis.set_x(to_transform.values()[0]->as_number().number()); to_axis.set_y(to_transform.values()[1]->as_number().number()); to_axis.set_z(to_transform.values()[2]->as_number().number()); to_angle_value = to_transform.values()[3]; } float to_angle = Angle::from_style_value(to_angle_value, {}).to_radians(); auto from_axis_angle = [](FloatVector3 const& axis, float angle) -> FloatVector4 { auto normalized = axis.normalized(); auto half_angle = angle / 2.0f; auto sin_half_angle = sinf(half_angle); FloatVector4 result { normalized.x() * sin_half_angle, normalized.y() * sin_half_angle, normalized.z() * sin_half_angle, cosf(half_angle) }; return result; }; struct AxisAngle { FloatVector3 axis; float angle; }; auto quaternion_to_axis_angle = [](FloatVector4 const& quaternion) { FloatVector3 axis { quaternion[0], quaternion[1], quaternion[2] }; auto epsilon = 1e-5f; auto sin_half_angle = sqrtf(max(0.0f, 1.0f - quaternion[3] * quaternion[3])); auto angle = 2.0f * acosf(clamp(quaternion[3], -1.0f, 1.0f)); if (sin_half_angle < epsilon) return AxisAngle { axis, angle }; axis = axis * (1.0f / sin_half_angle); return AxisAngle { axis, angle }; }; // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions // If the normalized vectors are equal, or if one of the angles is zero, interpolate the angle // numerically and use the rotation vector of the non-zero angle (or (0, 0, 1) if both are zero). auto epsilon = 1e-5f; auto from_axis_normalized = from_axis.length() > epsilon ? from_axis.normalized() : FloatVector3 { 0, 0, 1 }; auto to_axis_normalized = to_axis.length() > epsilon ? to_axis.normalized() : FloatVector3 { 0, 0, 1 }; bool axes_are_equal = (from_axis_normalized - to_axis_normalized).length() < epsilon; if (axes_are_equal || from_angle == 0.f || to_angle == 0.f) { auto result_angle = from_angle + (to_angle - from_angle) * delta; FloatVector3 result_axis = { 0, 0, 1 }; if (to_angle != 0.f) result_axis = to_axis_normalized; else if (from_angle != 0.f) result_axis = from_axis_normalized; auto interpolated_x_axis = NumberStyleValue::create(result_axis.x()); auto interpolated_y_axis = NumberStyleValue::create(result_axis.y()); auto interpolated_z_axis = NumberStyleValue::create(result_axis.z()); auto interpolated_angle = AngleStyleValue::create(Angle::make_degrees(AK::to_degrees(result_angle))); return TransformationStyleValue::create( PropertyID::Rotate, TransformFunction::Rotate3d, { interpolated_x_axis, interpolated_y_axis, interpolated_z_axis, interpolated_angle }); } // If the normalized vectors are not equal and both rotation angles are non-zero, convert to // 4x4 matrices and interpolate as defined in Interpolation of Matrices. auto from_quaternion = from_axis_angle(from_axis, from_angle); auto to_quaternion = from_axis_angle(to_axis, to_angle); auto interpolated_quaternion = slerp(from_quaternion, to_quaternion, delta); auto interpolated_axis_angle = quaternion_to_axis_angle(interpolated_quaternion); auto interpolated_x_axis = NumberStyleValue::create(interpolated_axis_angle.axis.x()); auto interpolated_y_axis = NumberStyleValue::create(interpolated_axis_angle.axis.y()); auto interpolated_z_axis = NumberStyleValue::create(interpolated_axis_angle.axis.z()); auto interpolated_angle = AngleStyleValue::create(Angle::make_degrees(AK::to_degrees(interpolated_axis_angle.angle))); return TransformationStyleValue::create( PropertyID::Rotate, TransformFunction::Rotate3d, { interpolated_x_axis, interpolated_y_axis, interpolated_z_axis, interpolated_angle }); } struct ExpandedGridTracksAndLines { Vector tracks; Vector> line_names; }; static ExpandedGridTracksAndLines expand_grid_tracks_and_lines(GridTrackSizeList const& list) { ExpandedGridTracksAndLines result; Optional current_track; Optional current_line_names; auto append_result = [&] { result.tracks.append(*current_track); result.line_names.append(move(current_line_names)); current_track.clear(); current_line_names.clear(); }; for (auto const& component : list.list()) { if (auto const* grid_line_names = component.get_pointer()) { VERIFY(!current_line_names.has_value()); current_line_names = *grid_line_names; } else if (auto const* grid_track = component.get_pointer()) { if (current_track.has_value()) append_result(); current_track = *grid_track; } if (current_track.has_value() && current_line_names.has_value()) append_result(); } if (current_track.has_value()) append_result(); return result; } static void append_grid_track_with_line_names(GridTrackSizeList& list, ExplicitGridTrack track, Optional line_names) { list.append(move(track)); if (line_names.has_value()) list.append(line_names.release_value()); } static Optional interpolate_grid_track_size_list(CalculationContext const& calculation_context, GridTrackSizeList const& from, GridTrackSizeList const& to, float delta) { auto interpolate_css_size = [&](Size const& from_size, Size const& to_size) -> Size { if (from_size.is_length_percentage() && to_size.is_length_percentage()) { auto interpolated_length = interpolate_length_percentage(calculation_context, from_size.length_percentage(), to_size.length_percentage(), delta); return Size::make_length_percentage(*interpolated_length); } if (from_size.type() != to_size.type()) return delta < 0.5f ? from_size : to_size; switch (from_size.type()) { case Size::Type::FitContent: { if (!from_size.fit_content_available_space().has_value() || !to_size.fit_content_available_space().has_value()) break; auto interpolated_available_space = interpolate_length_percentage(calculation_context, *from_size.fit_content_available_space(), *to_size.fit_content_available_space(), delta); if (!interpolated_available_space.has_value()) break; return Size::make_fit_content(interpolated_available_space.release_value()); } default: break; } return delta < 0.5f ? from_size : to_size; }; auto interpolate_grid_size = [&](GridSize const& from_grid_size, GridSize const& to_grid_size) -> GridSize { if (from_grid_size.is_flexible_length() || to_grid_size.is_flexible_length()) { if (from_grid_size.is_flexible_length() && to_grid_size.is_flexible_length()) { auto interpolated_flex = interpolate_raw(from_grid_size.flex_factor(), to_grid_size.flex_factor(), delta); return GridSize { Flex::make_fr(interpolated_flex) }; } } else { auto interpolated_size = interpolate_css_size(from_grid_size.css_size(), to_grid_size.css_size()); return GridSize { move(interpolated_size) }; } return delta < 0.5f ? from_grid_size : to_grid_size; }; auto expanded_from = expand_grid_tracks_and_lines(from); auto expanded_to = expand_grid_tracks_and_lines(to); if (expanded_from.tracks.size() != expanded_to.tracks.size()) return {}; GridTrackSizeList result; for (size_t i = 0; i < expanded_from.tracks.size(); ++i) { auto& from_track = expanded_from.tracks[i]; auto& to_track = expanded_to.tracks[i]; auto interpolated_line_names = delta < 0.5f ? move(expanded_from.line_names[i]) : move(expanded_to.line_names[i]); if (from_track.is_repeat() || to_track.is_repeat()) { // https://drafts.csswg.org/css-grid/#repeat-interpolation if (!from_track.is_repeat() || !to_track.is_repeat()) return {}; auto from_repeat = from_track.repeat(); auto to_repeat = to_track.repeat(); if (!from_repeat.is_fixed() || !to_repeat.is_fixed()) return {}; if (from_repeat.repeat_count() != to_repeat.repeat_count() || from_repeat.grid_track_size_list().track_list().size() != to_repeat.grid_track_size_list().track_list().size()) return {}; auto interpolated_repeat_grid_tracks = interpolate_grid_track_size_list(calculation_context, from_repeat.grid_track_size_list(), to_repeat.grid_track_size_list(), delta); if (!interpolated_repeat_grid_tracks.has_value()) return {}; ExplicitGridTrack interpolated_grid_track { GridRepeat { from_repeat.type(), move(*interpolated_repeat_grid_tracks), IntegerStyleValue::create(from_repeat.repeat_count()) } }; append_grid_track_with_line_names(result, move(interpolated_grid_track), move(interpolated_line_names)); } else if (from_track.is_minmax() && to_track.is_minmax()) { auto from_minmax = from_track.minmax(); auto to_minmax = to_track.minmax(); auto interpolated_min = interpolate_grid_size(from_minmax.min_grid_size(), to_minmax.min_grid_size()); auto interpolated_max = interpolate_grid_size(from_minmax.max_grid_size(), to_minmax.max_grid_size()); ExplicitGridTrack interpolated_grid_track { GridMinMax { interpolated_min, interpolated_max } }; append_grid_track_with_line_names(result, move(interpolated_grid_track), move(interpolated_line_names)); } else if (from_track.is_default() && to_track.is_default()) { auto const& from_grid_size = from_track.grid_size(); auto const& to_grid_size = to_track.grid_size(); auto interpolated_grid_size = interpolate_grid_size(from_grid_size, to_grid_size); ExplicitGridTrack interpolated_grid_track { move(interpolated_grid_size) }; append_grid_track_with_line_names(result, move(interpolated_grid_track), move(interpolated_line_names)); } else { auto interpolated_grid_track = delta < 0.5f ? move(from_track) : move(to_track); append_grid_track_with_line_names(result, move(interpolated_grid_track), move(interpolated_line_names)); } } return result; } ValueComparingRefPtr interpolate_property(DOM::Element& element, PropertyID property_id, StyleValue const& a_from, StyleValue const& a_to, float delta, AllowDiscrete allow_discrete) { auto from = with_keyword_values_resolved(element, property_id, a_from); auto to = with_keyword_values_resolved(element, property_id, a_to); auto calculation_context = CalculationContext::for_property(PropertyNameAndID::from_id(property_id)); auto animation_type = animation_type_from_longhand_property(property_id); switch (animation_type) { case AnimationType::ByComputedValue: return interpolate_value(element, calculation_context, from, to, delta, allow_discrete); case AnimationType::None: return to; case AnimationType::RepeatableList: 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, calculation_context, from, to, delta, allow_discrete)) 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 || property_id == PropertyID::TextShadow) { if (auto interpolated_box_shadow = interpolate_box_shadow(element, calculation_context, from, to, delta, allow_discrete)) return *interpolated_box_shadow; return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::FontStyle) { auto static oblique_0deg_value = FontStyleStyleValue::create(FontStyleKeyword::Oblique, AngleStyleValue::create(Angle::make_degrees(0))); auto from_value = from->as_font_style().font_style() == FontStyleKeyword::Normal ? oblique_0deg_value : from; auto to_value = to->as_font_style().font_style() == FontStyleKeyword::Normal ? oblique_0deg_value : to; return interpolate_value(element, calculation_context, from_value, to_value, delta, allow_discrete); } if (property_id == PropertyID::FontVariationSettings) { // https://drafts.csswg.org/css-fonts/#font-variation-settings-def // Two declarations of font-feature-settings can be animated between if they are "like". "Like" declarations // are ones where the same set of properties appear (in any order). Because successive duplicate properties // are applied instead of prior duplicate properties, two declarations can be "like" even if they have // differing number of properties. If two declarations are "like" then animation occurs pairwise between // corresponding values in the declarations. Otherwise, animation is not possible. if (!from->is_value_list() || !to->is_value_list()) return interpolate_discrete(from, to, delta, allow_discrete); // The values in these lists have already been deduplicated and sorted at this point, so we can use // interpolate_value() to interpolate them pairwise. return interpolate_value(element, calculation_context, from, to, delta, allow_discrete); } // https://drafts.csswg.org/web-animations-1/#animating-visibility if (property_id == PropertyID::Visibility) { // For the visibility property, visible is interpolated as a discrete step where values of p between 0 and 1 map to visible and other values of p map to the closer endpoint. // If neither value is visible, then discrete animation is used. if (from->equals(to)) return from; auto from_is_visible = from->to_keyword() == Keyword::Visible; auto to_is_visible = to->to_keyword() == Keyword::Visible; if (from_is_visible || to_is_visible) { if (delta <= 0) return from; if (delta >= 1) return to; return KeywordStyleValue::create(Keyword::Visible); } return interpolate_discrete(from, to, delta, allow_discrete); } // https://drafts.csswg.org/css-contain/#content-visibility-animation if (property_id == PropertyID::ContentVisibility) { // In general, the content-visibility property’s animation type is discrete. // However, similar to interpolation of visibility, during interpolation between hidden and any other content-visibility value, // p values between 0 and 1 map to the non-hidden value. if (from->equals(to)) return from; auto from_is_hidden = from->to_keyword() == Keyword::Hidden; auto to_is_hidden = to->to_keyword() == Keyword::Hidden; if (from_is_hidden || to_is_hidden) { auto non_hidden_value = from_is_hidden ? to : from; if (delta <= 0) return from; if (delta >= 1) return to; return non_hidden_value; } return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::Scale) { if (auto result = interpolate_scale(element, calculation_context, from, to, delta, allow_discrete)) return result; return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::Translate) { if (auto result = interpolate_translate(element, calculation_context, from, to, delta, allow_discrete)) return result; return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::Rotate) { if (auto result = interpolate_rotate(element, calculation_context, from, to, delta, allow_discrete)) return result; return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::Filter || property_id == PropertyID::BackdropFilter) { if (auto result = interpolate_filter_value_list(element, calculation_context, from, to, delta, allow_discrete)) return result; return interpolate_discrete(from, to, delta, allow_discrete); } if (property_id == PropertyID::GridTemplateRows || property_id == PropertyID::GridTemplateColumns) { // https://drafts.csswg.org/css-grid/#track-sizing // If the list lengths match, by computed value type per item in the computed track list. auto from_list = from->as_grid_track_size_list().grid_track_size_list(); auto to_list = to->as_grid_track_size_list().grid_track_size_list(); auto interpolated_grid_tack_size_list = interpolate_grid_track_size_list(calculation_context, from_list, to_list, delta); if (!interpolated_grid_tack_size_list.has_value()) return interpolate_discrete(from, to, delta, allow_discrete); return GridTrackSizeListStyleValue::create(interpolated_grid_tack_size_list.release_value()); } // FIXME: Handle all custom animatable properties [[fallthrough]]; } case AnimationType::Discrete: default: return interpolate_discrete(from, to, delta, allow_discrete); } } // https://drafts.csswg.org/css-transitions/#transitionable bool property_values_are_transitionable(PropertyID property_id, StyleValue const& old_value, StyleValue const& new_value, DOM::Element& element, TransitionBehavior transition_behavior) { // When comparing the before-change style and after-change style for a given property, // the property values are transitionable if they have an animation type that is neither not animatable nor discrete. auto animation_type = animation_type_from_longhand_property(property_id); if (animation_type == AnimationType::None || (transition_behavior != TransitionBehavior::AllowDiscrete && animation_type == AnimationType::Discrete)) return false; // Even when a property is transitionable, the two values may not be. The spec uses the example of inset/non-inset shadows. if (transition_behavior != TransitionBehavior::AllowDiscrete && !interpolate_property(element, property_id, old_value, new_value, 0.5f, AllowDiscrete::No)) return false; return true; } static Optional interpolate_matrices(FloatMatrix4x4 const& from, FloatMatrix4x4 const& to, float delta) { 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 interpolated_rotation = slerp(from.rotation, to.rotation, delta); 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_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); return recompose(interpolated_decomposed); } static StyleValueVector matrix_to_style_value_vector(FloatMatrix4x4 const& matrix) { StyleValueVector values; values.ensure_capacity(16); for (int i = 0; i < 16; i++) values.unchecked_append(NumberStyleValue::create(matrix[i % 4, i / 4])); return values; } // https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms RefPtr interpolate_transform(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { // * 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> { 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> 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 transform) -> NonnullRefPtr { 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 transform) -> NonnullRefPtr { // 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: generic_function = TransformFunction::Matrix3d; // NB: Called during animation interpolation. auto paintable_box = [&] -> Optional { if (auto* box = element.unsafe_paintable_box()) return *box; return {}; }(); parameters = matrix_to_style_value_vector(MUST(transform->to_matrix(paintable_box))); } 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 , 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); // * 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. // NB: Called during animation interpolation. Optional paintable_box; if (auto* paintable = as_if(element.unsafe_paintable())) paintable_box = *paintable; auto post_multiply_remaining_transformations = [&paintable_box](size_t start_index, Vector> const& transformations) -> Optional { FloatMatrix4x4 result = FloatMatrix4x4::identity(); for (auto index = start_index; index < transformations.size(); ++index) { auto transformation_matrix = transformations[index]->to_matrix(paintable_box); if (transformation_matrix.is_error()) return {}; 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); // https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms // If one of the matrices for interpolation is non-invertible, the used animation function must // fall-back to a discrete animation according to the rules of the respective animation specification. if (!from_matrix.has_value() || !to_matrix.has_value()) return interpolate_discrete(from, to, delta, allow_discrete); auto maybe_interpolated_matrix = interpolate_matrices(from_matrix.value(), to_matrix.value(), delta); if (!maybe_interpolated_matrix.has_value()) return interpolate_discrete(from, to, delta, allow_discrete); result.append(TransformationStyleValue::create(PropertyID::Transform, TransformFunction::Matrix3d, matrix_to_style_value_vector(maybe_interpolated_matrix.release_value()))); return StyleValueList::create(move(result), StyleValueList::Separator::Space); } Color interpolate_color(Color from, Color to, float delta, ColorSyntax syntax) { // https://drafts.csswg.org/css-color/#interpolation // FIXME: Handle all interpolation methods. // FIXME: Handle "analogous", "missing", and "powerless" components, somehow. // FIXME: Remove duplicated code with Color::mixed_with(Color other, float weight) // 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. // However, user agents must handle interpolation between legacy sRGB color formats (hex colors, named colors, // rgb(), hsl() or hwb() and the equivalent alpha-including forms) in gamma-encoded sRGB space. This provides // Web compatibility; legacy sRGB content interpolates in the sRGB space by default. auto from_alpha = from.alpha() / 255.0f; auto to_alpha = to.alpha() / 255.0f; auto interpolated_alpha = interpolate_raw(from_alpha, to_alpha, delta); auto clamped_alpha = clamp(interpolated_alpha, 0.0f, 1.0f); if (clamped_alpha == 0) return Color::Transparent; // 5. changing the color components to premultiplied form // 6. linearly interpolating each component of the computed value of the color separately // 7. undoing premultiplication if (syntax == ColorSyntax::Modern) { auto from_oklab = from.to_premultiplied_oklab(); auto to_oklab = to.to_premultiplied_oklab(); return Color::from_oklab( interpolate_raw(from_oklab.L, to_oklab.L, delta) / clamped_alpha, interpolate_raw(from_oklab.a, to_oklab.a, delta) / clamped_alpha, interpolate_raw(from_oklab.b, to_oklab.b, delta) / clamped_alpha, clamped_alpha); } auto premultiply_interpolate_unpremultiply = [&](u8 from_channel, u8 to_channel) -> u8 { auto pre_from = from_channel * from_alpha; auto pre_to = to_channel * to_alpha; auto interpolated = interpolate_raw(pre_from, pre_to, delta); return clamp(lroundf(interpolated / clamped_alpha), 0L, 255L); }; return Color { premultiply_interpolate_unpremultiply(from.red(), to.red()), premultiply_interpolate_unpremultiply(from.green(), to.green()), premultiply_interpolate_unpremultiply(from.blue(), to.blue()), static_cast(lroundf(clamped_alpha * 255.0f)), }; } RefPtr interpolate_box_shadow(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { // 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 = [](StyleValue const& value) -> StyleValueVector { if (value.to_keyword() == Keyword::None) return {}; return value.as_value_list().values(); }; 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( other.get(0).value()->as_shadow().shadow_type(), ColorStyleValue::create_from_color(Color::Transparent, ColorSyntax::Legacy), 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()); // NB: Called during style interpolation. ColorResolutionContext color_resolution_context {}; if (auto node = element.unsafe_layout_node()) { color_resolution_context = ColorResolutionContext::for_layout_node_with_style(*element.unsafe_layout_node()); } 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 interpolated_offset_x = interpolate_value(element, calculation_context, from_shadow.offset_x(), to_shadow.offset_x(), delta, allow_discrete); auto interpolated_offset_y = interpolate_value(element, calculation_context, from_shadow.offset_y(), to_shadow.offset_y(), delta, allow_discrete); auto interpolated_blur_radius = interpolate_value(element, calculation_context, from_shadow.blur_radius(), to_shadow.blur_radius(), delta, allow_discrete); auto interpolated_spread_distance = interpolate_value(element, calculation_context, from_shadow.spread_distance(), to_shadow.spread_distance(), delta, allow_discrete); if (!interpolated_offset_x || !interpolated_offset_y || !interpolated_blur_radius || !interpolated_spread_distance) return {}; auto color_syntax = ColorSyntax::Legacy; if ((!from_shadow.color()->is_keyword() && from_shadow.color()->as_color().color_syntax() == ColorSyntax::Modern) || (!to_shadow.color()->is_keyword() && to_shadow.color()->as_color().color_syntax() == ColorSyntax::Modern)) { color_syntax = ColorSyntax::Modern; } // FIXME: If we aren't able to resolve the colors here, we should postpone interpolation until we can (perhaps // by creating something similar to a ColorMixStyleValue). auto from_color = from_shadow.color()->to_color(color_resolution_context); auto to_color = to_shadow.color()->to_color(color_resolution_context); Color interpolated_color = Color::Black; if (from_color.has_value() && to_color.has_value()) interpolated_color = interpolate_color(from_color.value(), to_color.value(), delta, color_syntax); auto result_shadow = ShadowStyleValue::create( from_shadow.shadow_type(), ColorStyleValue::create_from_color(interpolated_color, ColorSyntax::Modern), *interpolated_offset_x, *interpolated_offset_y, *interpolated_blur_radius, *interpolated_spread_distance, 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 Optional get_value_type_of_numeric_style_value(StyleValue const& value, CalculationContext const& calculation_context) { switch (value.type()) { case StyleValue::Type::Angle: return ValueType::Angle; case StyleValue::Type::Frequency: return ValueType::Frequency; case StyleValue::Type::Integer: return ValueType::Integer; case StyleValue::Type::Length: return ValueType::Length; case StyleValue::Type::Number: return ValueType::Number; case StyleValue::Type::Percentage: return calculation_context.percentages_resolve_as.value_or(ValueType::Percentage); case StyleValue::Type::Resolution: return ValueType::Resolution; case StyleValue::Type::Time: return ValueType::Time; case StyleValue::Type::Calculated: { auto const& calculated = value.as_calculated(); if (calculated.resolves_to_angle_percentage()) return ValueType::Angle; if (calculated.resolves_to_frequency_percentage()) return ValueType::Frequency; if (calculated.resolves_to_length_percentage()) return ValueType::Length; if (calculated.resolves_to_resolution()) return ValueType::Resolution; if (calculated.resolves_to_number()) return calculation_context.resolve_numbers_as_integers ? ValueType::Integer : ValueType::Number; if (calculated.resolves_to_percentage()) return calculation_context.percentages_resolve_as.value_or(ValueType::Percentage); if (calculated.resolves_to_time_percentage()) return ValueType::Time; return {}; } default: return {}; } } static RefPtr interpolate_mixed_value(CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta) { auto from_value_type = get_value_type_of_numeric_style_value(from, calculation_context); auto to_value_type = get_value_type_of_numeric_style_value(to, calculation_context); if (from_value_type.has_value() && from_value_type == to_value_type) { // https://drafts.csswg.org/css-values-4/#combine-mixed // The computed value of a percentage-dimension mix is defined as // FIXME: a computed dimension if the percentage component is zero or is defined specifically to compute to a dimension value // a computed percentage if the dimension component is zero // a computed calc() expression otherwise if (auto const* from_dimension_value = as_if(from); from_dimension_value && to.type() == StyleValue::Type::Percentage) { auto dimension_component = from_dimension_value->raw_value() * (1.f - delta); auto percentage_component = to.as_percentage().raw_value() * delta; if (dimension_component == 0.f) return PercentageStyleValue::create(Percentage { percentage_component }); } else if (auto const* to_dimension_value = as_if(to); to_dimension_value && from.type() == StyleValue::Type::Percentage) { auto dimension_component = to_dimension_value->raw_value() * delta; auto percentage_component = from.as_percentage().raw_value() * (1.f - delta); if (dimension_component == 0) return PercentageStyleValue::create(Percentage { percentage_component }); } auto from_node = CalculationNode::from_style_value(from, calculation_context); auto to_node = CalculationNode::from_style_value(to, calculation_context); // https://drafts.csswg.org/css-values-4/#combine-math // Interpolation of math functions, with each other or with numeric values and other numeric-valued functions, is defined as Vresult = calc((1 - p) * VA + p * VB). auto from_contribution = ProductCalculationNode::create({ from_node, NumericCalculationNode::create(Number { Number::Type::Number, 1.f - delta }, calculation_context), }); auto to_contribution = ProductCalculationNode::create({ to_node, NumericCalculationNode::create(Number { Number::Type::Number, delta }, calculation_context), }); return CalculatedStyleValue::create( simplify_a_calculation_tree(SumCalculationNode::create({ from_contribution, to_contribution }), calculation_context, {}), *from_node->numeric_type()->added_to(*to_node->numeric_type()), calculation_context); } return {}; } template static NonnullRefPtr length_percentage_or_auto_to_style_value(T const& value) { if constexpr (requires { value.is_auto(); }) { if (value.is_auto()) return KeywordStyleValue::create(Keyword::Auto); } if (value.is_length()) return LengthStyleValue::create(value.length()); if (value.is_percentage()) return PercentageStyleValue::create(value.percentage()); if (value.is_calculated()) return value.calculated(); VERIFY_NOT_REACHED(); } Optional interpolate_length_percentage(CalculationContext const& calculation_context, LengthPercentage const& from, LengthPercentage const& to, float delta) { if (from.is_length() && to.is_length()) return Length::make_px(interpolate_raw(from.length().raw_value(), to.length().raw_value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Length))); if (from.is_percentage() && to.is_percentage()) return Percentage(interpolate_raw(from.percentage().value(), to.percentage().value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Percentage))); auto from_style_value = length_percentage_or_auto_to_style_value(from); auto to_style_value = length_percentage_or_auto_to_style_value(to); auto interpolated_style_value = interpolate_mixed_value(calculation_context, from_style_value, to_style_value, delta); if (!interpolated_style_value) return {}; return LengthPercentage::from_style_value(*interpolated_style_value); } Optional interpolate_length_percentage_or_auto(CalculationContext const& calculation_context, LengthPercentageOrAuto const& from, LengthPercentageOrAuto const& to, float delta) { if (from.is_auto() && to.is_auto()) return LengthPercentageOrAuto::make_auto(); if (from.is_length() && to.is_length()) return Length::make_px(interpolate_raw(from.length().raw_value(), to.length().raw_value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Length))); if (from.is_percentage() && to.is_percentage()) return Percentage(interpolate_raw(from.percentage().value(), to.percentage().value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Percentage))); auto from_style_value = length_percentage_or_auto_to_style_value(from); auto to_style_value = length_percentage_or_auto_to_style_value(to); auto interpolated_style_value = interpolate_mixed_value(calculation_context, from_style_value, to_style_value, delta); if (!interpolated_style_value) return {}; return LengthPercentageOrAuto::from_style_value(*interpolated_style_value); } static RefPtr interpolate_value_impl(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { if (from.type() != to.type() || from.is_calculated() || to.is_calculated()) { // Handle mixed percentage and dimension types, as well as CalculatedStyleValues // https://www.w3.org/TR/css-values-4/#mixed-percentages return interpolate_mixed_value(calculation_context, from, to, delta); } switch (from.type()) { case StyleValue::Type::Angle: { auto interpolated_value = interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta, calculation_context.accepted_type_ranges.get(ValueType::Angle)); return AngleStyleValue::create(Angle::make_degrees(interpolated_value)); } case StyleValue::Type::BackgroundSize: { auto interpolated_x = interpolate_value(element, calculation_context, from.as_background_size().size_x(), to.as_background_size().size_x(), delta, allow_discrete); auto interpolated_y = interpolate_value(element, calculation_context, from.as_background_size().size_y(), to.as_background_size().size_y(), delta, allow_discrete); if (!interpolated_x || !interpolated_y) return {}; return BackgroundSizeStyleValue::create(*interpolated_x, *interpolated_y); } case StyleValue::Type::BorderImageSlice: { auto& from_border_image_slice = from.as_border_image_slice(); auto& to_border_image_slice = to.as_border_image_slice(); if (from_border_image_slice.fill() != to_border_image_slice.fill()) return {}; auto interpolated_top = interpolate_value(element, calculation_context, from_border_image_slice.top(), to_border_image_slice.top(), delta, allow_discrete); auto interpolated_right = interpolate_value(element, calculation_context, from_border_image_slice.right(), to_border_image_slice.right(), delta, allow_discrete); auto interpolated_bottom = interpolate_value(element, calculation_context, from_border_image_slice.bottom(), to_border_image_slice.bottom(), delta, allow_discrete); auto interpolated_left = interpolate_value(element, calculation_context, from_border_image_slice.left(), to_border_image_slice.left(), delta, allow_discrete); if (!interpolated_top || !interpolated_right || !interpolated_bottom || !interpolated_left) return {}; return BorderImageSliceStyleValue::create( interpolated_top.release_nonnull(), interpolated_right.release_nonnull(), interpolated_bottom.release_nonnull(), interpolated_left.release_nonnull(), from_border_image_slice.fill()); } case StyleValue::Type::BasicShape: { // https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation auto& from_shape = from.as_basic_shape().basic_shape(); auto& to_shape = to.as_basic_shape().basic_shape(); if (from_shape.index() != to_shape.index()) return {}; CalculationContext basic_shape_calculation_context { .percentages_resolve_as = ValueType::Length }; auto const interpolate_optional_position = [&](RefPtr from_position, RefPtr to_position) -> Optional> { if (!from_position && !to_position) return nullptr; auto const& from_position_with_default = from_position ? from_position.release_nonnull() : PositionStyleValue::create_computed_center(); auto const& to_position_with_default = to_position ? to_position.release_nonnull() : PositionStyleValue::create_computed_center(); auto interpolated_position = interpolate_value(element, basic_shape_calculation_context, from_position_with_default, to_position_with_default, delta, allow_discrete); // NB: Use OptionalNone to indicate failure to interpolate since nullptr is a valid result for interpolating // between two null positions. if (!interpolated_position) return OptionalNone {}; return interpolated_position; }; auto interpolated_shape = from_shape.visit( [&](Inset const& from_inset) -> Optional { // If both shapes are of type inset(), interpolate between each value in the shape functions. auto& to_inset = to_shape.get(); auto interpolated_top = interpolate_value(element, basic_shape_calculation_context, from_inset.top, to_inset.top, delta, allow_discrete); auto interpolated_right = interpolate_value(element, basic_shape_calculation_context, from_inset.right, to_inset.right, delta, allow_discrete); auto interpolated_bottom = interpolate_value(element, basic_shape_calculation_context, from_inset.bottom, to_inset.bottom, delta, allow_discrete); auto interpolated_left = interpolate_value(element, basic_shape_calculation_context, from_inset.left, to_inset.left, delta, allow_discrete); auto interpolated_border_radius = interpolate_value(element, basic_shape_calculation_context, from_inset.border_radius, to_inset.border_radius, delta, allow_discrete); if (!interpolated_top || !interpolated_right || !interpolated_bottom || !interpolated_left || !interpolated_border_radius) return {}; return Inset { interpolated_top.release_nonnull(), interpolated_right.release_nonnull(), interpolated_bottom.release_nonnull(), interpolated_left.release_nonnull(), interpolated_border_radius.release_nonnull() }; }, [&](Circle const& from_circle) -> Optional { // If both shapes are the same type, that type is ellipse() or circle(), and the radiuses are specified // as (rather than keywords), interpolate between each value in the shape functions. auto const& to_circle = to_shape.get(); auto interpolated_radius = interpolate_value_impl(element, basic_shape_calculation_context, from_circle.radius, to_circle.radius, delta, AllowDiscrete::No); auto interpolated_position = interpolate_optional_position(from_circle.position, to_circle.position); if (!interpolated_radius || !interpolated_position.has_value()) return {}; return Circle { interpolated_radius.release_nonnull(), interpolated_position.value() }; }, [&](Ellipse const& from_ellipse) -> Optional { auto const& to_ellipse = to_shape.get(); auto interpolated_radius = interpolate_value_impl(element, basic_shape_calculation_context, from_ellipse.radius, to_ellipse.radius, delta, AllowDiscrete::No); auto interpolated_position = interpolate_optional_position(from_ellipse.position, to_ellipse.position); if (!interpolated_radius || !interpolated_position.has_value()) return {}; return Ellipse { interpolated_radius.release_nonnull(), interpolated_position.value() }; }, [&](Polygon const& from_polygon) -> Optional { // If both shapes are of type polygon(), both polygons have the same number of vertices, and use the // same <'fill-rule'>, interpolate between each value in the shape functions. auto const& to_polygon = to_shape.get(); if (from_polygon.fill_rule != to_polygon.fill_rule) return {}; if (from_polygon.points.size() != to_polygon.points.size()) return {}; Vector interpolated_points; interpolated_points.ensure_capacity(from_polygon.points.size()); for (size_t i = 0; i < from_polygon.points.size(); i++) { auto const& from_point = from_polygon.points[i]; auto const& to_point = to_polygon.points[i]; auto interpolated_point_x = interpolate_value(element, basic_shape_calculation_context, from_point.x, to_point.x, delta, allow_discrete); auto interpolated_point_y = interpolate_value(element, basic_shape_calculation_context, from_point.y, to_point.y, delta, allow_discrete); if (!interpolated_point_x || !interpolated_point_y) return {}; interpolated_points.unchecked_append(Polygon::Point { *interpolated_point_x, *interpolated_point_y }); } return Polygon { from_polygon.fill_rule, move(interpolated_points) }; }, [](auto&) -> Optional { return {}; }); if (!interpolated_shape.has_value()) return {}; return BasicShapeStyleValue::create(*interpolated_shape); } case StyleValue::Type::BorderRadius: { auto const& from_horizontal_radius = from.as_border_radius().horizontal_radius(); auto const& to_horizontal_radius = to.as_border_radius().horizontal_radius(); auto const& from_vertical_radius = from.as_border_radius().vertical_radius(); auto const& to_vertical_radius = to.as_border_radius().vertical_radius(); auto interpolated_horizontal_radius = interpolate_value_impl(element, calculation_context, from_horizontal_radius, to_horizontal_radius, delta, allow_discrete); auto interpolated_vertical_radius = interpolate_value_impl(element, calculation_context, from_vertical_radius, to_vertical_radius, delta, allow_discrete); if (!interpolated_horizontal_radius || !interpolated_vertical_radius) return {}; return BorderRadiusStyleValue::create(interpolated_horizontal_radius.release_nonnull(), interpolated_vertical_radius.release_nonnull()); } case StyleValue::Type::BorderRadiusRect: { CalculationContext border_radius_rect_computation_context = { .percentages_resolve_as = ValueType::Length, .accepted_type_ranges = { { ValueType::Length, { 0, AK::NumericLimits::max() } }, { ValueType::Percentage, { 0, AK::NumericLimits::max() } } }, }; auto const& from_top_left = from.as_border_radius_rect().top_left(); auto const& to_top_left = to.as_border_radius_rect().top_left(); auto const& from_top_right = from.as_border_radius_rect().top_right(); auto const& to_top_right = to.as_border_radius_rect().top_right(); auto const& from_bottom_right = from.as_border_radius_rect().bottom_right(); auto const& to_bottom_right = to.as_border_radius_rect().bottom_right(); auto const& from_bottom_left = from.as_border_radius_rect().bottom_left(); auto const& to_bottom_left = to.as_border_radius_rect().bottom_left(); auto interpolated_top_left = interpolate_value_impl(element, border_radius_rect_computation_context, from_top_left, to_top_left, delta, allow_discrete); auto interpolated_top_right = interpolate_value_impl(element, border_radius_rect_computation_context, from_top_right, to_top_right, delta, allow_discrete); auto interpolated_bottom_right = interpolate_value_impl(element, border_radius_rect_computation_context, from_bottom_right, to_bottom_right, delta, allow_discrete); auto interpolated_bottom_left = interpolate_value_impl(element, border_radius_rect_computation_context, from_bottom_left, to_bottom_left, delta, allow_discrete); if (!interpolated_top_left || !interpolated_top_right || !interpolated_bottom_right || !interpolated_bottom_left) return {}; return BorderRadiusRectStyleValue::create(interpolated_top_left.release_nonnull(), interpolated_top_right.release_nonnull(), interpolated_bottom_right.release_nonnull(), interpolated_bottom_left.release_nonnull()); } case StyleValue::Type::Color: { // NB: Called during style interpolation. ColorResolutionContext color_resolution_context {}; if (auto node = element.unsafe_layout_node()) { color_resolution_context = ColorResolutionContext::for_layout_node_with_style(*element.unsafe_layout_node()); } auto color_syntax = ColorSyntax::Legacy; if ((!from.is_keyword() && from.as_color().color_syntax() == ColorSyntax::Modern) || (!to.is_keyword() && to.as_color().color_syntax() == ColorSyntax::Modern)) { color_syntax = ColorSyntax::Modern; } // FIXME: If we aren't able to resolve the colors here, we should postpone interpolation until we can (perhaps // by creating something similar to a ColorMixStyleValue). auto from_color = from.to_color(color_resolution_context); auto to_color = to.to_color(color_resolution_context); Color interpolated_color = Color::Black; if (from_color.has_value() && to_color.has_value()) interpolated_color = interpolate_color(from_color.value(), to_color.value(), delta, color_syntax); return ColorStyleValue::create_from_color(interpolated_color, ColorSyntax::Modern); } case StyleValue::Type::Edge: { auto const& from_offset = from.as_edge().offset(); auto const& to_offset = to.as_edge().offset(); if (auto interpolated_value = interpolate_value_impl(element, calculation_context, from_offset, to_offset, delta, allow_discrete)) return EdgeStyleValue::create({}, interpolated_value); return {}; } case StyleValue::Type::FontStyle: { auto const& from_font_style = from.as_font_style(); auto const& to_font_style = to.as_font_style(); auto interpolated_font_style = interpolate_value(element, calculation_context, KeywordStyleValue::create(to_keyword(from_font_style.font_style())), KeywordStyleValue::create(to_keyword(to_font_style.font_style())), delta, allow_discrete); if (!interpolated_font_style) return {}; if (from_font_style.angle() && to_font_style.angle()) { auto interpolated_angle = interpolate_value(element, { .accepted_type_ranges = { { ValueType::Angle, { -90, 90 } } } }, *from_font_style.angle(), *to_font_style.angle(), delta, allow_discrete); if (!interpolated_angle) return {}; return FontStyleStyleValue::create(*keyword_to_font_style_keyword(interpolated_font_style->to_keyword()), interpolated_angle); } return FontStyleStyleValue::create(*keyword_to_font_style_keyword(interpolated_font_style->to_keyword())); } case StyleValue::Type::Integer: { // https://drafts.csswg.org/css-values/#combine-integers // Interpolation of is defined as Vresult = round((1 - p) × VA + p × VB); // that is, interpolation happens in the real number space as for s, and the result is converted to an by rounding to the nearest integer. auto interpolated_value = interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta, calculation_context.accepted_type_ranges.get(ValueType::Integer)); return IntegerStyleValue::create(interpolated_value); } case StyleValue::Type::Length: { auto const& from_length = from.as_length().length(); auto const& to_length = to.as_length().length(); auto interpolated_value = interpolate_raw(from_length.raw_value(), to_length.raw_value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Length)); return LengthStyleValue::create(Length(interpolated_value, from_length.unit())); } case StyleValue::Type::Number: { auto interpolated_value = interpolate_raw(from.as_number().number(), to.as_number().number(), delta, calculation_context.accepted_type_ranges.get(ValueType::Number)); return NumberStyleValue::create(interpolated_value); } case StyleValue::Type::OpenTypeTagged: { auto& from_open_type_tagged = from.as_open_type_tagged(); auto& to_open_type_tagged = to.as_open_type_tagged(); if (from_open_type_tagged.tag() != to_open_type_tagged.tag()) return {}; auto interpolated_value = interpolate_value(element, calculation_context, from_open_type_tagged.value(), to_open_type_tagged.value(), delta, allow_discrete); if (!interpolated_value) return {}; return OpenTypeTaggedStyleValue::create(OpenTypeTaggedStyleValue::Mode::FontVariationSettings, from_open_type_tagged.tag(), interpolated_value.release_nonnull()); } case StyleValue::Type::Percentage: { auto interpolated_value = interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value(), delta, calculation_context.accepted_type_ranges.get(ValueType::Percentage)); return PercentageStyleValue::create(Percentage(interpolated_value)); } 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 const& from_position = from.as_position(); auto const& to_position = to.as_position(); auto interpolated_edge_x = interpolate_value(element, calculation_context, from_position.edge_x(), to_position.edge_x(), delta, allow_discrete); auto interpolated_edge_y = interpolate_value(element, calculation_context, from_position.edge_y(), to_position.edge_y(), delta, allow_discrete); if (!interpolated_edge_x || !interpolated_edge_y) return {}; return PositionStyleValue::create(interpolated_edge_x->as_edge(), interpolated_edge_y->as_edge()); } case StyleValue::Type::RadialSize: { auto const& from_components = from.as_radial_size().components(); auto const& to_components = to.as_radial_size().components(); auto const is_radial_extent = [](auto const& component) { return component.template has(); }; // https://drafts.csswg.org/css-images-4/#interpolating-gradients // https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation // FIXME: Radial extents should disallow interpolation for basic-shape values but should be converted into their // equivalent length-percentage values for radial gradients if (any_of(from_components, is_radial_extent) || any_of(to_components, is_radial_extent)) return {}; CalculationContext radial_size_calculation_context { .percentages_resolve_as = ValueType::Length, .accepted_type_ranges = { { ValueType::Length, { 0, AK::NumericLimits::max() } }, } }; if (from_components.size() == 1 && to_components.size() == 1) { auto const& from_component = from_components[0].get>(); auto const& to_component = to_components[0].get>(); auto interpolated_value = interpolate_value(element, radial_size_calculation_context, from_component, to_component, delta, allow_discrete); if (!interpolated_value) return {}; return RadialSizeStyleValue::create({ interpolated_value.release_nonnull() }); } auto const& from_horizontal_component = from_components[0].get>(); auto const& from_vertical_component = from_components.size() > 1 ? from_components[1].get>() : from_horizontal_component; auto const& to_horizontal_component = to_components[0].get>(); auto const& to_vertical_component = to_components.size() > 1 ? to_components[1].get>() : to_horizontal_component; auto interpolated_horizontal = interpolate_value(element, radial_size_calculation_context, from_horizontal_component, to_horizontal_component, delta, allow_discrete); auto interpolated_vertical = interpolate_value(element, radial_size_calculation_context, from_vertical_component, to_vertical_component, delta, allow_discrete); if (!interpolated_horizontal || !interpolated_vertical) return {}; return RadialSizeStyleValue::create({ interpolated_horizontal.release_nonnull(), interpolated_vertical.release_nonnull() }); } case StyleValue::Type::Ratio: { auto from_ratio = from.as_ratio().resolved(); auto to_ratio = to.as_ratio().resolved(); // https://drafts.csswg.org/css-values/#combine-ratio // If either is degenerate, the values cannot be interpolated. if (from_ratio.is_degenerate() || to_ratio.is_degenerate()) return {}; // 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 interpolated_value = interpolate_raw(from_number, to_number, delta, calculation_context.accepted_type_ranges.get(ValueType::Ratio)); return RatioStyleValue::create(NumberStyleValue::create(pow(M_E, interpolated_value)), NumberStyleValue::create(1)); } case StyleValue::Type::Rect: { auto const& from_rect = from.as_rect(); auto const& to_rect = to.as_rect(); auto interpolated_top = interpolate_value_impl(element, calculation_context, from_rect.top(), to_rect.top(), delta, allow_discrete); auto interpolated_right = interpolate_value_impl(element, calculation_context, from_rect.right(), to_rect.right(), delta, allow_discrete); auto interpolated_bottom = interpolate_value_impl(element, calculation_context, from_rect.bottom(), to_rect.bottom(), delta, allow_discrete); auto interpolated_left = interpolate_value_impl(element, calculation_context, from_rect.left(), to_rect.left(), delta, allow_discrete); if (!interpolated_top || !interpolated_right || !interpolated_bottom || !interpolated_left) return {}; return RectStyleValue::create(interpolated_top.release_nonnull(), interpolated_right.release_nonnull(), interpolated_bottom.release_nonnull(), interpolated_left.release_nonnull()); } case StyleValue::Type::TextIndent: { auto& from_text_indent = from.as_text_indent(); auto& to_text_indent = to.as_text_indent(); if (from_text_indent.each_line() != to_text_indent.each_line() || from_text_indent.hanging() != to_text_indent.hanging()) return {}; auto interpolated_length_percentage = interpolate_value(element, calculation_context, from_text_indent.length_percentage(), to_text_indent.length_percentage(), delta, allow_discrete); if (!interpolated_length_percentage) return {}; return TextIndentStyleValue::create(interpolated_length_percentage.release_nonnull(), from_text_indent.hanging() ? TextIndentStyleValue::Hanging::Yes : TextIndentStyleValue::Hanging::No, from_text_indent.each_line() ? TextIndentStyleValue::EachLine::Yes : TextIndentStyleValue::EachLine::No); } case StyleValue::Type::Superellipse: { // https://drafts.csswg.org/css-borders-4/#corner-shape-interpolation // https://drafts.csswg.org/css-borders-4/#normalized-superellipse-half-corner auto normalized_super_ellipse_half_corner = [](double s) -> double { // To compute the normalized superellipse half corner given a superellipse parameter s, return the first matching statement, switching on s: // -∞ Return 0. if (s == -AK::Infinity) return 0; // ∞ Return 1. if (s == AK::Infinity) return 1; // Otherwise // 1. Let k be 0.5^abs(s). auto k = pow(0.5, abs(s)); // 2. Let convexHalfCorner be 0.5^k. auto convex_half_corner = pow(0.5, k); // 3. If s is less than 0, return 1 - convexHalfCorner. if (s < 0) return 1 - convex_half_corner; // 4. Return convexHalfCorner. return convex_half_corner; }; auto interpolation_value_to_super_ellipse_parameter = [](double interpolation_value) -> double { // To convert a interpolationValue back to a superellipse parameter, switch on interpolationValue: // 0 Return -∞. if (interpolation_value == 0) return -AK::Infinity; // 0.5 Return 0. if (interpolation_value == 0.5) return 0; // 1 Return ∞. if (interpolation_value == 1) return AK::Infinity; // Otherwise // 1. Let convexHalfCorner be interpolationValue. auto convex_half_corner = interpolation_value; // 2. If interpolationValue is less than 0.5, set convexHalfCorner to 1 - interpolationValue. if (interpolation_value < 0.5) convex_half_corner = 1 - interpolation_value; // 3. Let k be ln(0.5) / ln(convexHalfCorner). auto k = log(0.5) / log(convex_half_corner); // 4. Let s be log2(k). auto s = log2(k); // AD-HOC: The logs above can introduce slight inaccuracies, this can interfere with the behaviour of // serializing superellipse style values as their equivalent keywords as that relies on exact // equality. To mitigate this we simply round to a whole number if we are sufficiently near if (abs(round(s) - s) < AK::NumericLimits::epsilon()) s = round(s); // 5. If interpolationValue is less than 0.5, return -s. if (interpolation_value < 0.5) return -s; // 6. Return s. return s; }; auto from_normalized_value = normalized_super_ellipse_half_corner(from.as_superellipse().parameter()); auto to_normalized_value = normalized_super_ellipse_half_corner(to.as_superellipse().parameter()); auto interpolated_value = interpolate_raw(from_normalized_value, to_normalized_value, delta, AcceptedTypeRange { .min = 0, .max = 1 }); return SuperellipseStyleValue::create(NumberStyleValue::create(interpolation_value_to_super_ellipse_parameter(interpolated_value))); } case StyleValue::Type::Transformation: VERIFY_NOT_REACHED(); case StyleValue::Type::ValueList: { auto const& from_list = from.as_value_list(); auto const& to_list = to.as_value_list(); if (from_list.size() != to_list.size()) return {}; // FIXME: If the number of components or the types of corresponding components do not match, // or if any component value uses discrete animation and the two corresponding values do not match, // then the property values combine as discrete. StyleValueVector interpolated_values; interpolated_values.ensure_capacity(from_list.size()); for (size_t i = 0; i < from_list.size(); ++i) { auto interpolated = interpolate_value(element, calculation_context, from_list.values()[i], to_list.values()[i], delta, AllowDiscrete::No); if (!interpolated) return {}; interpolated_values.append(*interpolated); } return StyleValueList::create(move(interpolated_values), from_list.separator()); } default: return {}; } } RefPtr interpolate_repeatable_list(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { // https://www.w3.org/TR/web-animations/#repeatable-list // Same as by computed value except that if the two lists have differing numbers of items, they are first repeated to the least common multiple number of items. // Each item is then combined by computed value. // If a pair of values cannot be combined or if any component value uses discrete animation, then the property values combine as discrete. auto make_repeatable_list = [&](auto const& from_list, auto const& to_list, Function)> append_callback) -> bool { // If the number of components or the types of corresponding components do not match, // or if any component value uses discrete animation and the two corresponding values do not match, // then the property values combine as discrete auto list_size = AK::lcm(from_list.size(), to_list.size()); for (size_t i = 0; i < list_size; ++i) { auto value = interpolate_value(element, calculation_context, from_list.value_at(i, true), to_list.value_at(i, true), delta, AllowDiscrete::No); if (!value) return false; append_callback(*value); } return true; }; auto make_single_value_list = [&](auto const& value, size_t size, auto separator) { StyleValueVector values; values.ensure_capacity(size); for (size_t i = 0; i < size; ++i) values.append(value); return StyleValueList::create(move(values), separator); }; NonnullRefPtr from_list = from; NonnullRefPtr to_list = to; if (!from.is_value_list() && to.is_value_list()) from_list = make_single_value_list(from, to.as_value_list().size(), to.as_value_list().separator()); else if (!to.is_value_list() && from.is_value_list()) to_list = make_single_value_list(to, from.as_value_list().size(), from.as_value_list().separator()); else if (!from.is_value_list() && !to.is_value_list()) return interpolate_value(element, calculation_context, from, to, delta, allow_discrete); StyleValueVector interpolated_values; if (!make_repeatable_list(from_list->as_value_list(), to_list->as_value_list(), [&](auto const& value) { interpolated_values.append(value); })) return interpolate_discrete(from, to, delta, allow_discrete); return StyleValueList::create(move(interpolated_values), from_list->as_value_list().separator()); } // https://drafts.csswg.org/filter-effects/#accumulation Vector accumulate_filter_function(FilterValueListStyleValue const& underlying_list, FilterValueListStyleValue const& animated_list) { // Accumulation of s follows the same matching and extending rules as interpolation, falling // back to replace behavior if the lists do not match. However instead of interpolating the matching // pairs, their arguments are arithmetically added together - except in the case of // s whose initial value for interpolation is 1, which combine using one-based addition: // Vresult = Va + Vb - 1 auto initial_value_for = [](FilterValue const& value) { return value.visit( [&](FilterOperation::Blur const&) -> FilterValue { return FilterOperation::Blur { LengthStyleValue::create(Length::make_px(0)) }; }, [&](FilterOperation::DropShadow const&) -> FilterValue { return FilterOperation::DropShadow { .offset_x = LengthStyleValue::create(Length::make_px(0)), .offset_y = LengthStyleValue::create(Length::make_px(0)), .radius = LengthStyleValue::create(Length::make_px(0)), .color = nullptr }; }, [&](FilterOperation::HueRotate const&) -> FilterValue { return FilterOperation::HueRotate { AngleStyleValue::create(Angle::make_degrees(0)) }; }, [&](FilterOperation::Color const& color) -> FilterValue { auto default_value_for_accumulation = [&]() { switch (color.operation) { case Gfx::ColorFilterType::Grayscale: case Gfx::ColorFilterType::Invert: case Gfx::ColorFilterType::Sepia: return 0.0; case Gfx::ColorFilterType::Brightness: case Gfx::ColorFilterType::Contrast: case Gfx::ColorFilterType::Opacity: case Gfx::ColorFilterType::Saturate: return 1.0; } VERIFY_NOT_REACHED(); }(); return FilterOperation::Color { .operation = color.operation, .amount = NumberStyleValue::create(default_value_for_accumulation) }; }, [&](auto&) -> FilterValue { VERIFY_NOT_REACHED(); }); }; auto accumulate_filter = [](FilterValue const& underlying, FilterValue const& animated) -> Optional { return underlying.visit( [&](FilterOperation::Blur const& underlying_blur) -> Optional { if (!animated.has()) return {}; auto const& animated_blur = animated.get(); return FilterOperation::Blur { .radius = LengthStyleValue::create(Length::make_px(underlying_blur.resolved_radius() + animated_blur.resolved_radius())) }; }, [&](FilterOperation::HueRotate const& underlying_rotate) -> Optional { if (!animated.has()) return {}; auto const& animated_rotate = animated.get(); return FilterOperation::HueRotate { .angle = AngleStyleValue::create(Angle::make_degrees(underlying_rotate.angle_degrees() + animated_rotate.angle_degrees())) }; }, [&](FilterOperation::Color const& underlying_color) -> Optional { if (!animated.has()) return {}; auto const& animated_color = animated.get(); if (underlying_color.operation != animated_color.operation) return {}; auto underlying_amount = underlying_color.resolved_amount(); auto animated_amount = animated_color.resolved_amount(); double accumulated; switch (underlying_color.operation) { case Gfx::ColorFilterType::Brightness: case Gfx::ColorFilterType::Contrast: case Gfx::ColorFilterType::Opacity: case Gfx::ColorFilterType::Saturate: accumulated = underlying_amount + animated_amount - 1.0; break; case Gfx::ColorFilterType::Grayscale: case Gfx::ColorFilterType::Invert: case Gfx::ColorFilterType::Sepia: accumulated = underlying_amount + animated_amount; break; default: VERIFY_NOT_REACHED(); } return FilterOperation::Color { .operation = underlying_color.operation, .amount = NumberStyleValue::create(accumulated) }; }, [&](FilterOperation::DropShadow const& underlying_shadow) -> Optional { if (!animated.has()) return {}; auto const& animated_shadow = animated.get(); auto add_lengths = [](NonnullRefPtr const& a, NonnullRefPtr const& b) -> NonnullRefPtr { auto a_value = Length::from_style_value(a, {}).absolute_length_to_px_without_rounding(); auto b_value = Length::from_style_value(b, {}).absolute_length_to_px_without_rounding(); return LengthStyleValue::create(Length::make_px(a_value + b_value)); }; auto offset_x = add_lengths(underlying_shadow.offset_x, animated_shadow.offset_x); auto offset_y = add_lengths(underlying_shadow.offset_y, animated_shadow.offset_y); RefPtr accumulated_radius; if (underlying_shadow.radius || animated_shadow.radius) { auto underlying_radius = underlying_shadow.radius ? Length::from_style_value(*underlying_shadow.radius, {}).absolute_length_to_px_without_rounding() : 0; auto animated_radius = animated_shadow.radius ? Length::from_style_value(*animated_shadow.radius, {}).absolute_length_to_px_without_rounding() : 0; accumulated_radius = LengthStyleValue::create(Length::make_px(underlying_radius + animated_radius)); } RefPtr accumulated_color = animated_shadow.color; if (underlying_shadow.color && animated_shadow.color) { ColorResolutionContext color_resolution_context {}; auto underlying_color = underlying_shadow.color->to_color(color_resolution_context); auto animated_color = animated_shadow.color->to_color(color_resolution_context); if (underlying_color.has_value() && animated_color.has_value()) { auto accumulated = Color( min(255, underlying_color->red() + animated_color->red()), min(255, underlying_color->green() + animated_color->green()), min(255, underlying_color->blue() + animated_color->blue()), min(255, underlying_color->alpha() + animated_color->alpha())); accumulated_color = ColorStyleValue::create_from_color(accumulated, ColorSyntax::Legacy); } } return FilterOperation::DropShadow { .offset_x = offset_x, .offset_y = offset_y, .radius = accumulated_radius, .color = accumulated_color }; }, [&](URL const&) -> Optional { return {}; }); }; // Extend shorter list with initial values size_t max_size = max(underlying_list.size(), animated_list.size()); Vector extended_underlying; Vector extended_animated; for (size_t i = 0; i < max_size; ++i) { if (i < underlying_list.size()) { extended_underlying.append(underlying_list.filter_value_list()[i]); } else { extended_underlying.append(initial_value_for(animated_list.filter_value_list()[i])); } if (i < animated_list.size()) { extended_animated.append(animated_list.filter_value_list()[i]); } else { extended_animated.append(initial_value_for(underlying_list.filter_value_list()[i])); } } Vector result; result.ensure_capacity(max_size); for (size_t i = 0; i < max_size; ++i) { auto accumulated = accumulate_filter(extended_underlying[i], extended_animated[i]); if (!accumulated.has_value()) return {}; result.unchecked_append(accumulated.release_value()); } return result; } RefPtr interpolate_value(DOM::Element& element, CalculationContext const& calculation_context, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete allow_discrete) { if (auto result = interpolate_value_impl(element, calculation_context, from, to, delta, allow_discrete)) return result; return interpolate_discrete(from, to, delta, allow_discrete); } template static T composite_raw_values(T underlying_raw_value, T animated_raw_value) { return underlying_raw_value + animated_raw_value; } static Optional add_length_percentages(CalculationContext const& calculation_context, LengthPercentage const& a, LengthPercentage const& b) { if (a.is_length() && b.is_length() && a.length().unit() == b.length().unit()) { return LengthPercentage { Length { a.length().raw_value() + b.length().raw_value(), a.length().unit() } }; } if (a.is_percentage() && b.is_percentage()) { return LengthPercentage { Percentage { a.percentage().value() + b.percentage().value() } }; } auto a_style_value = length_percentage_or_auto_to_style_value(a); auto b_style_value = length_percentage_or_auto_to_style_value(b); auto a_node = CalculationNode::from_style_value(a_style_value, calculation_context); auto b_node = CalculationNode::from_style_value(b_style_value, calculation_context); auto sum = SumCalculationNode::create({ a_node, b_node }); auto simplified = simplify_a_calculation_tree(sum, calculation_context, {}); auto result_type = a_node->numeric_type()->added_to(*b_node->numeric_type()); if (!result_type.has_value()) return {}; auto calculated = CalculatedStyleValue::create(simplified, *result_type, calculation_context); return LengthPercentage::from_style_value(calculated); } static Optional composite_grid_track_size_list(CalculationContext const& calculation_context, GridTrackSizeList const& underlying, GridTrackSizeList const& animated) { auto composite_css_size = [&calculation_context](Size const& underlying_size, Size const& animated_size) -> Optional { if (underlying_size.is_length_percentage() && animated_size.is_length_percentage()) { auto composited = add_length_percentages(calculation_context, underlying_size.length_percentage(), animated_size.length_percentage()); if (!composited.has_value()) return {}; return Size::make_length_percentage(*composited); } if (underlying_size.is_fit_content() && animated_size.is_fit_content()) { if (!underlying_size.fit_content_available_space().has_value() || !animated_size.fit_content_available_space().has_value()) return {}; auto composited = add_length_percentages(calculation_context, *underlying_size.fit_content_available_space(), *animated_size.fit_content_available_space()); if (!composited.has_value()) return {}; return Size::make_fit_content(*composited); } return {}; }; auto composite_grid_size = [&](GridSize const& underlying_grid_size, GridSize const& animated_grid_size) -> Optional { if (underlying_grid_size.is_flexible_length() && animated_grid_size.is_flexible_length()) { auto composited_flex = underlying_grid_size.flex_factor() + animated_grid_size.flex_factor(); return GridSize { Flex::make_fr(composited_flex) }; } if (!underlying_grid_size.is_flexible_length() && !animated_grid_size.is_flexible_length()) { auto composited_size = composite_css_size(underlying_grid_size.css_size(), animated_grid_size.css_size()); if (composited_size.has_value()) return GridSize { move(*composited_size) }; } return {}; }; auto expanded_underlying = expand_grid_tracks_and_lines(underlying); auto expanded_animated = expand_grid_tracks_and_lines(animated); if (expanded_underlying.tracks.size() != expanded_animated.tracks.size()) return {}; GridTrackSizeList result; for (size_t i = 0; i < expanded_underlying.tracks.size(); ++i) { auto& underlying_track = expanded_underlying.tracks[i]; auto& animated_track = expanded_animated.tracks[i]; auto composited_line_names = move(expanded_animated.line_names[i]); if (underlying_track.is_repeat() || animated_track.is_repeat()) { if (!underlying_track.is_repeat() || !animated_track.is_repeat()) return {}; auto underlying_repeat = underlying_track.repeat(); auto animated_repeat = animated_track.repeat(); if (!underlying_repeat.is_fixed() || !animated_repeat.is_fixed()) return {}; if (underlying_repeat.repeat_count() != animated_repeat.repeat_count() || underlying_repeat.grid_track_size_list().track_list().size() != animated_repeat.grid_track_size_list().track_list().size()) return {}; auto composited_repeat_grid_tracks = composite_grid_track_size_list(calculation_context, underlying_repeat.grid_track_size_list(), animated_repeat.grid_track_size_list()); if (!composited_repeat_grid_tracks.has_value()) return {}; ExplicitGridTrack composited_grid_track { GridRepeat { underlying_repeat.type(), move(*composited_repeat_grid_tracks), IntegerStyleValue::create(underlying_repeat.repeat_count()) } }; append_grid_track_with_line_names(result, move(composited_grid_track), move(composited_line_names)); continue; } if (underlying_track.is_minmax() && animated_track.is_minmax()) { auto underlying_minmax = underlying_track.minmax(); auto animated_minmax = animated_track.minmax(); auto composited_min = composite_grid_size(underlying_minmax.min_grid_size(), animated_minmax.min_grid_size()); auto composited_max = composite_grid_size(underlying_minmax.max_grid_size(), animated_minmax.max_grid_size()); ExplicitGridTrack composited_grid_track { GridMinMax { composited_min.value_or(animated_minmax.min_grid_size()), composited_max.value_or(animated_minmax.max_grid_size()) } }; append_grid_track_with_line_names(result, move(composited_grid_track), move(composited_line_names)); continue; } if (underlying_track.is_default() && animated_track.is_default()) { auto const& underlying_grid_size = underlying_track.grid_size(); auto const& animated_grid_size = animated_track.grid_size(); auto composited_grid_size_result = composite_grid_size(underlying_grid_size, animated_grid_size); if (composited_grid_size_result.has_value()) { ExplicitGridTrack composited_grid_track { move(*composited_grid_size_result) }; append_grid_track_with_line_names(result, move(composited_grid_track), move(composited_line_names)); continue; } } append_grid_track_with_line_names(result, animated_track, move(composited_line_names)); } return result; } static RefPtr composite_mixed_value(StyleValue const& underlying_value, StyleValue const& animated_value, CalculationContext const& calculation_context) { // https://drafts.csswg.org/css-values-4/#combine-mixed // Addition of is defined the same as interpolation except by adding each component rather than interpolating it. auto underlying_value_type = get_value_type_of_numeric_style_value(underlying_value, calculation_context); auto animated_value_type = get_value_type_of_numeric_style_value(animated_value, calculation_context); if (underlying_value_type.has_value() && underlying_value_type == animated_value_type) { // The computed value of a percentage-dimension mix is defined as // FIXME: a computed dimension if the percentage component is zero or is defined specifically to compute to a dimension value // a computed percentage if the dimension component is zero // a computed calc() expression otherwise if (auto const* from_dimension_value = as_if(underlying_value); from_dimension_value && animated_value.type() == StyleValue::Type::Percentage) { auto dimension_component = from_dimension_value->raw_value(); auto percentage_component = animated_value.as_percentage().raw_value(); if (dimension_component == 0.f) return PercentageStyleValue::create(Percentage { percentage_component }); } else if (auto const* to_dimension_value = as_if(animated_value); to_dimension_value && underlying_value.type() == StyleValue::Type::Percentage) { auto dimension_component = to_dimension_value->raw_value(); auto percentage_component = underlying_value.as_percentage().raw_value(); if (dimension_component == 0) return PercentageStyleValue::create(Percentage { percentage_component }); } auto underlying_node = CalculationNode::from_style_value(underlying_value, calculation_context); auto animated_node = CalculationNode::from_style_value(animated_value, calculation_context); return CalculatedStyleValue::create( simplify_a_calculation_tree(SumCalculationNode::create({ underlying_node, animated_node }), calculation_context, {}), *underlying_node->numeric_type()->added_to(*animated_node->numeric_type()), calculation_context); } return {}; } RefPtr composite_value(PropertyID property_id, StyleValue const& underlying_value, StyleValue const& animated_value, Bindings::CompositeOperation composite_operation) { auto calculation_context = CalculationContext::for_property(PropertyNameAndID::from_id(property_id)); auto composite_dimension_value = [](StyleValue const& underlying_value, StyleValue const& animated_value) -> Optional { auto const& underlying_dimension = as(underlying_value); auto const& animated_dimension = as(animated_value); return composite_raw_values(underlying_dimension.raw_value(), animated_dimension.raw_value()); }; if (composite_operation == Bindings::CompositeOperation::Replace) return {}; if (underlying_value.type() != animated_value.type() || underlying_value.is_calculated() || animated_value.is_calculated()) return composite_mixed_value(underlying_value, animated_value, calculation_context); switch (underlying_value.type()) { case StyleValue::Type::Angle: { auto result = composite_dimension_value(underlying_value, animated_value); if (!result.has_value()) return {}; VERIFY(underlying_value.as_angle().angle().unit() == animated_value.as_angle().angle().unit()); return AngleStyleValue::create({ *result, underlying_value.as_angle().angle().unit() }); } case StyleValue::Type::BasicShape: { auto const& underlying_basic_shape = underlying_value.as_basic_shape(); auto const& animated_basic_shape = animated_value.as_basic_shape(); if (underlying_basic_shape.basic_shape().index() != animated_basic_shape.basic_shape().index()) return {}; return underlying_basic_shape.basic_shape().visit( [&](Inset const& underlying_inset) -> RefPtr { auto const& animated_inset = animated_basic_shape.basic_shape().get(); auto composited_top = composite_value(property_id, underlying_inset.top, animated_inset.top, composite_operation); auto composited_right = composite_value(property_id, underlying_inset.right, animated_inset.right, composite_operation); auto composited_bottom = composite_value(property_id, underlying_inset.bottom, animated_inset.bottom, composite_operation); auto composited_left = composite_value(property_id, underlying_inset.left, animated_inset.left, composite_operation); auto composited_border_radius = composite_value(property_id, underlying_inset.border_radius, animated_inset.border_radius, composite_operation); if (!composited_top || !composited_right || !composited_bottom || !composited_left || !composited_border_radius) return {}; return BasicShapeStyleValue::create(Inset { composited_top.release_nonnull(), composited_right.release_nonnull(), composited_bottom.release_nonnull(), composited_left.release_nonnull(), composited_border_radius.release_nonnull() }); }, [&](Circle const& underlying_circle) -> RefPtr { auto const& animated_circle = animated_basic_shape.basic_shape().get(); auto composited_radius = composite_value(property_id, underlying_circle.radius, animated_circle.radius, composite_operation); if (!composited_radius) return {}; RefPtr composited_position; if (underlying_circle.position || animated_circle.position) { auto const& underlying_position_with_default = underlying_circle.position ? ValueComparingNonnullRefPtr { *underlying_circle.position } : PositionStyleValue::create_computed_center(); auto const& animated_position_with_default = animated_circle.position ? ValueComparingNonnullRefPtr { *animated_circle.position } : PositionStyleValue::create_computed_center(); composited_position = composite_value(property_id, underlying_position_with_default, animated_position_with_default, composite_operation); if (!composited_position) return {}; } return BasicShapeStyleValue::create(Circle { composited_radius.release_nonnull(), composited_position }); }, [&](Ellipse const& underlying_ellipse) -> RefPtr { auto const& animated_ellipse = animated_basic_shape.basic_shape().get(); auto composited_radius = composite_value(property_id, underlying_ellipse.radius, animated_ellipse.radius, composite_operation); if (!composited_radius) return {}; RefPtr composited_position; if (underlying_ellipse.position || animated_ellipse.position) { auto const& underlying_position_with_default = underlying_ellipse.position ? ValueComparingNonnullRefPtr { *underlying_ellipse.position } : PositionStyleValue::create_computed_center(); auto const& animated_position_with_default = animated_ellipse.position ? ValueComparingNonnullRefPtr { *animated_ellipse.position } : PositionStyleValue::create_computed_center(); composited_position = composite_value(property_id, underlying_position_with_default, animated_position_with_default, composite_operation); if (!composited_position) return {}; } return BasicShapeStyleValue::create(Ellipse { composited_radius.release_nonnull(), composited_position }); }, [&](Polygon const& underlying_polygon) -> RefPtr { auto const& animated_polygon = animated_basic_shape.basic_shape().get(); if (underlying_polygon.fill_rule != animated_polygon.fill_rule) return {}; if (underlying_polygon.points.size() != animated_polygon.points.size()) return {}; Vector composited_points; composited_points.ensure_capacity(underlying_polygon.points.size()); for (size_t i = 0; i < underlying_polygon.points.size(); i++) { auto const& underlying_point = underlying_polygon.points[i]; auto const& animated_point = animated_polygon.points[i]; auto composited_point_x = composite_value(property_id, underlying_point.x, animated_point.x, composite_operation); auto composited_point_y = composite_value(property_id, underlying_point.y, animated_point.y, composite_operation); if (!composited_point_x || !composited_point_y) return {}; composited_points.unchecked_append(Polygon::Point { *composited_point_x, *composited_point_y }); } return BasicShapeStyleValue::create(Polygon { underlying_polygon.fill_rule, move(composited_points) }); }, [&](Xywh const&) -> RefPtr { // xywh() should have been absolutized into inset() before now VERIFY_NOT_REACHED(); }, [&](Rect const&) -> RefPtr { // rect() should have been absolutized into inset() before now VERIFY_NOT_REACHED(); }, [&](Path const&) -> RefPtr { // FIXME: Implement composition for path() return {}; }); } case StyleValue::Type::BorderImageSlice: { auto& underlying_border_image_slice_value = underlying_value.as_border_image_slice(); auto& animated_border_image_slice_value = animated_value.as_border_image_slice(); if (underlying_border_image_slice_value.fill() != animated_border_image_slice_value.fill()) return {}; auto composited_top = composite_value(property_id, underlying_border_image_slice_value.top(), animated_border_image_slice_value.top(), composite_operation); auto composited_right = composite_value(property_id, underlying_border_image_slice_value.right(), animated_border_image_slice_value.right(), composite_operation); auto composited_bottom = composite_value(property_id, underlying_border_image_slice_value.bottom(), animated_border_image_slice_value.bottom(), composite_operation); auto composited_left = composite_value(property_id, underlying_border_image_slice_value.left(), animated_border_image_slice_value.left(), composite_operation); if (!composited_top || !composited_right || !composited_bottom || !composited_left) return {}; return BorderImageSliceStyleValue::create(composited_top.release_nonnull(), composited_right.release_nonnull(), composited_bottom.release_nonnull(), composited_left.release_nonnull(), underlying_border_image_slice_value.fill()); } case StyleValue::Type::BorderRadius: { auto composited_horizontal_radius = composite_value(property_id, underlying_value.as_border_radius().horizontal_radius(), animated_value.as_border_radius().horizontal_radius(), composite_operation); auto composited_vertical_radius = composite_value(property_id, underlying_value.as_border_radius().vertical_radius(), animated_value.as_border_radius().vertical_radius(), composite_operation); if (!composited_horizontal_radius || !composited_vertical_radius) return {}; return BorderRadiusStyleValue::create(composited_horizontal_radius.release_nonnull(), composited_vertical_radius.release_nonnull()); } case StyleValue::Type::BorderRadiusRect: { auto const& underlying_top_left = underlying_value.as_border_radius_rect().top_left(); auto const& animated_top_left = animated_value.as_border_radius_rect().top_left(); auto const& underlying_top_right = underlying_value.as_border_radius_rect().top_right(); auto const& animated_top_right = animated_value.as_border_radius_rect().top_right(); auto const& underlying_bottom_right = underlying_value.as_border_radius_rect().bottom_right(); auto const& animated_bottom_right = animated_value.as_border_radius_rect().bottom_right(); auto const& underlying_bottom_left = underlying_value.as_border_radius_rect().bottom_left(); auto const& animated_bottom_left = animated_value.as_border_radius_rect().bottom_left(); auto composited_top_left = composite_value(property_id, underlying_top_left, animated_top_left, composite_operation); auto composited_top_right = composite_value(property_id, underlying_top_right, animated_top_right, composite_operation); auto composited_bottom_right = composite_value(property_id, underlying_bottom_right, animated_bottom_right, composite_operation); auto composited_bottom_left = composite_value(property_id, underlying_bottom_left, animated_bottom_left, composite_operation); if (!composited_top_left || !composited_top_right || !composited_bottom_right || !composited_bottom_left) return {}; return BorderRadiusRectStyleValue::create(composited_top_left.release_nonnull(), composited_top_right.release_nonnull(), composited_bottom_right.release_nonnull(), composited_bottom_left.release_nonnull()); } case StyleValue::Type::Edge: { auto const& underlying_offset = underlying_value.as_edge().offset(); auto const& animated_offset = animated_value.as_edge().offset(); if (auto composited_value = composite_value(property_id, underlying_offset, animated_offset, composite_operation)) return EdgeStyleValue::create({}, composited_value); return {}; } case StyleValue::Type::GridTrackSizeList: { auto underlying_list = underlying_value.as_grid_track_size_list().grid_track_size_list(); auto animated_list = animated_value.as_grid_track_size_list().grid_track_size_list(); auto composited_list = composite_grid_track_size_list(calculation_context, underlying_list, animated_list); if (!composited_list.has_value()) return {}; return GridTrackSizeListStyleValue::create(composited_list.release_value()); } case StyleValue::Type::Integer: { auto result = composite_raw_values(underlying_value.as_integer().integer(), animated_value.as_integer().integer()); return IntegerStyleValue::create(result); } case StyleValue::Type::Length: { auto result = composite_dimension_value(underlying_value, animated_value); if (!result.has_value()) return {}; VERIFY(underlying_value.as_length().length().unit() == animated_value.as_length().length().unit()); return LengthStyleValue::create(Length { *result, underlying_value.as_length().length().unit() }); } case StyleValue::Type::Number: { auto result = composite_raw_values(underlying_value.as_number().number(), animated_value.as_number().number()); return NumberStyleValue::create(result); } case StyleValue::Type::OpenTypeTagged: { auto& underlying_open_type_tagged = underlying_value.as_open_type_tagged(); auto& animated_open_type_tagged = animated_value.as_open_type_tagged(); if (underlying_open_type_tagged.tag() != animated_open_type_tagged.tag()) return {}; auto composited_value = composite_value(property_id, underlying_open_type_tagged.value(), animated_open_type_tagged.value(), composite_operation); if (!composited_value) return {}; return OpenTypeTaggedStyleValue::create(OpenTypeTaggedStyleValue::Mode::FontVariationSettings, underlying_open_type_tagged.tag(), composited_value.release_nonnull()); } case StyleValue::Type::Percentage: { auto result = composite_raw_values(underlying_value.as_percentage().percentage().value(), animated_value.as_percentage().percentage().value()); return PercentageStyleValue::create(Percentage { result }); } case StyleValue::Type::Position: { auto& underlying_position = underlying_value.as_position(); auto& animated_position = animated_value.as_position(); auto composited_edge_x = composite_value(property_id, underlying_position.edge_x(), animated_position.edge_x(), composite_operation); auto composited_edge_y = composite_value(property_id, underlying_position.edge_y(), animated_position.edge_y(), composite_operation); if (!composited_edge_x || !composited_edge_y) return {}; return PositionStyleValue::create(composited_edge_x->as_edge(), composited_edge_y->as_edge()); } case StyleValue::Type::RadialSize: { auto const& underlying_components = underlying_value.as_radial_size().components(); auto const& animated_components = animated_value.as_radial_size().components(); auto const is_radial_extent = [](auto const& component) { return component.template has(); }; // https://drafts.csswg.org/css-images-4/#interpolating-gradients // https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation // FIXME: Radial extents should disallow composition for basic-shape values but should be converted into their // equivalent length-percentage values for radial gradients if (any_of(underlying_components, is_radial_extent) || any_of(animated_components, is_radial_extent)) return {}; if (underlying_components.size() == 1 && animated_components.size() == 1) { auto const& underlying_component = underlying_components[0].get>(); auto const& animated_component = animated_components[0].get>(); auto interpolated_value = composite_value(property_id, underlying_component, animated_component, composite_operation); if (!interpolated_value) return {}; return RadialSizeStyleValue::create({ interpolated_value.release_nonnull() }); } auto const& underlying_horizontal_component = underlying_components[0].get>(); auto const& underlying_vertical_component = underlying_components.size() > 1 ? underlying_components[1].get>() : underlying_horizontal_component; auto const& animated_horizontal_component = animated_components[0].get>(); auto const& animated_vertical_component = animated_components.size() > 1 ? animated_components[1].get>() : animated_horizontal_component; auto composited_horizontal = composite_value(property_id, underlying_horizontal_component, animated_horizontal_component, composite_operation); auto composited_vertical = composite_value(property_id, underlying_vertical_component, animated_vertical_component, composite_operation); if (!composited_horizontal || !composited_vertical) return {}; return RadialSizeStyleValue::create({ composited_horizontal.release_nonnull(), composited_vertical.release_nonnull() }); } case StyleValue::Type::Ratio: { // https://drafts.csswg.org/css-values/#combine-ratio // Addition of s is not possible. return {}; } case StyleValue::Type::ValueList: { auto& underlying_list = underlying_value.as_value_list(); auto& animated_list = animated_value.as_value_list(); if (underlying_list.size() != animated_list.size() || underlying_list.separator() != animated_list.separator()) return {}; StyleValueVector values; values.ensure_capacity(underlying_list.size()); for (size_t i = 0; i < underlying_list.size(); ++i) { auto composited_value = composite_value(property_id, underlying_list.values()[i], animated_list.values()[i], composite_operation); if (!composited_value) return {}; values.unchecked_append(*composited_value); } return StyleValueList::create(move(values), underlying_list.separator()); } case StyleValue::Type::FilterValueList: { auto& underlying_list = underlying_value.as_filter_value_list(); auto& animated_list = animated_value.as_filter_value_list(); // https://drafts.csswg.org/filter-effects/#addition // Given two filter values representing an base value (base filter list) and a value to add (added filter list), // returns the concatenation of the the two lists: ‘base filter list added filter list’. if (composite_operation == Bindings::CompositeOperation::Add) { Vector result = underlying_list.filter_value_list(); result.extend(animated_list.filter_value_list()); return FilterValueListStyleValue::create(move(result)); } VERIFY(composite_operation == Bindings::CompositeOperation::Accumulate); auto result = accumulate_filter_function(underlying_list, animated_list); if (result.is_empty()) return {}; return FilterValueListStyleValue::create(move(result)); } default: // FIXME: Implement compositing for missing types return {}; } } }