ladybird/Libraries/LibWeb/CSS/Interpolation.cpp

2629 lines
142 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025-2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Interpolation.h"
#include <AK/IntegralMath.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/BackgroundSizeStyleValue.h>
#include <LibWeb/CSS/StyleValues/BorderImageSliceStyleValue.h>
#include <LibWeb/CSS/StyleValues/BorderRadiusRectStyleValue.h>
#include <LibWeb/CSS/StyleValues/BorderRadiusStyleValue.h>
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/StyleValues/ColorStyleValue.h>
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
#include <LibWeb/CSS/StyleValues/FitContentStyleValue.h>
#include <LibWeb/CSS/StyleValues/FlexStyleValue.h>
#include <LibWeb/CSS/StyleValues/FontStyleStyleValue.h>
#include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h>
#include <LibWeb/CSS/StyleValues/GridTrackSizeListStyleValue.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/RadialSizeStyleValue.h>
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
#include <LibWeb/CSS/StyleValues/RectStyleValue.h>
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/SuperellipseStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextIndentStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/PaintableBox.h>
namespace Web::CSS {
template<typename T>
static T interpolate_raw(T from, T to, float delta, Optional<AcceptedTypeRange> accepted_type_range = {})
{
if constexpr (AK::Detail::IsSame<T, double>) {
if (accepted_type_range.has_value())
return clamp(from + (to - from) * static_cast<double>(delta), accepted_type_range->min, accepted_type_range->max);
return from + (to - from) * static_cast<double>(delta);
} else if constexpr (AK::Detail::IsIntegral<T>) {
auto from_float = static_cast<float>(from);
auto to_float = static_cast<float>(to);
auto min = accepted_type_range.has_value() ? accepted_type_range->min : NumericLimits<T>::min();
auto max = accepted_type_range.has_value() ? accepted_type_range->max : NumericLimits<T>::max();
auto unclamped_result = roundf(from_float + (to_float - from_float) * delta);
return static_cast<AK::Detail::RemoveCVReference<T>>(clamp(unclamped_result, min, max));
}
VERIFY(!accepted_type_range.has_value());
return static_cast<AK::Detail::RemoveCVReference<T>>(from + (to - from) * delta);
}
static NonnullRefPtr<StyleValue const> 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<StyleValue const> 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<StyleValue const> 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<StyleValue const> 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<FilterValue> interpolate_filter_function(DOM::Element& element, CalculationContext const& calculation_context, FilterValue const& from, FilterValue const& to, float delta, AllowDiscrete allow_discrete)
{
VERIFY(!from.has<URL>());
VERIFY(!to.has<URL>());
if (from.index() != to.index()) {
return {};
}
auto result = from.visit(
[&](FilterOperation::Blur const& from_value) -> Optional<FilterValue> {
auto const& to_value = to.get<FilterOperation::Blur>();
CalculationContext blur_calculation_context = calculation_context;
blur_calculation_context.accepted_type_ranges.set(ValueType::Length, { 0, NumericLimits<float>::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<FilterValue> {
auto const& to_value = to.get<FilterOperation::HueRotate>();
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<FilterValue> {
auto const& to_value = to.get<FilterOperation::Color>();
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<float>::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<FilterValue> {
auto const& to_value = to.get<FilterOperation::DropShadow>();
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<StyleValue const> 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<FilterValue> {
// URL filters cannot be interpolated
return {};
});
return result;
}
// https://drafts.fxtf.org/filter-effects/#interpolation-of-filters
static RefPtr<StyleValue const> 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<FilterValueListStyleValue const> {
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<FilterValue> 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 <filter-value-list> of same length without <url> and for each <filter-function> for which there is a corresponding item in each list
if (from_list.size() == to_list.size()) {
// Interpolate each <filter-function> pair following the rules in section Interpolation of Filter Functions.
return interpolate_filter_values(a_from, a_to);
}
// If both filters have a <filter-value-list> of different length without <url> and for each <filter-function> for which there is a corresponding item in each list
// 1. Append the missing equivalent <filter-function>s from the longer list to the end of the shorter list. The new added <filter-function>s must be initialized to their initial values for interpolation.
auto append_missing_values_to = [&](FilterValueListStyleValue const& short_list, FilterValueListStyleValue const& longer_list) -> ValueComparingNonnullRefPtr<FilterValueListStyleValue const> {
Vector<FilterValue> 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<StyleValue const> from = from_list.size() < to_list.size() ? append_missing_values_to(from_list, to_list) : a_from;
ValueComparingNonnullRefPtr<StyleValue const> to = to_list.size() < from_list.size() ? append_missing_values_to(to_list, from_list) : a_to;
// 2. Interpolate each <filter-function> 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 <filter-value-list> without <url>
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 <filter-value-list> of the other filter. The new <filter-function>s must be initialized to their initial values for interpolation.
auto replace_none_with_initial_filter_list_values = [&](FilterValueListStyleValue const& filter_value_list) {
Vector<FilterValue> 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<StyleValue const> from = a_from.is_keyword() ? replace_none_with_initial_filter_list_values(a_to.as_filter_value_list()) : a_from;
ValueComparingNonnullRefPtr<StyleValue const> to = a_to.is_keyword() ? replace_none_with_initial_filter_list_values(a_from.as_filter_value_list()) : a_to;
// 2. Interpolate each <filter-function> pair following the rules in section Interpolation of Filter Functions.
return interpolate_filter_values(from, to);
}
// Otherwise:
// Use discrete interpolation
return {};
}
static RefPtr<StyleValue const> 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<StyleValue const> 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<float>::epsilon())
return from * from_multiplier;
if (abs(from_multiplier) < AK::NumericLimits<float>::epsilon())
return to * w;
return from * from_multiplier + to * w;
}
static RefPtr<StyleValue const> 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<ExplicitGridTrack> tracks;
Vector<Optional<GridLineNames>> line_names;
};
static ExpandedGridTracksAndLines expand_grid_tracks_and_lines(GridTrackSizeList const& list)
{
ExpandedGridTracksAndLines result;
Optional<ExplicitGridTrack> current_track;
Optional<GridLineNames> 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<GridLineNames>()) {
VERIFY(!current_line_names.has_value());
current_line_names = *grid_line_names;
} else if (auto const* grid_track = component.get_pointer<ExplicitGridTrack>()) {
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<GridLineNames> line_names)
{
list.append(move(track));
if (line_names.has_value())
list.append(line_names.release_value());
}
static Optional<GridTrackSizeList> interpolate_grid_track_size_list(DOM::Element& element, CalculationContext const& calculation_context, GridTrackSizeList const& from, GridTrackSizeList const& to, float delta)
{
auto interpolate_grid_size = [&](GridSize const& from_grid_size, GridSize const& to_grid_size) -> GridSize {
return GridSize { *interpolate_value(element, calculation_context, from_grid_size.style_value(), to_grid_size.style_value(), delta, AllowDiscrete::Yes) };
};
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(element, 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<StyleValue const> 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 propertys 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(element, 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<FloatMatrix4x4> 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<DecomposedValues> {
// 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<StyleValue const> 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<NonnullRefPtr<TransformationStyleValue const>> {
if (style_value.is_transformation())
return { style_value.as_transformation() };
// NB: This encompasses both the allowed value "none" and any invalid values.
if (!style_value.is_value_list())
return {};
Vector<NonnullRefPtr<TransformationStyleValue const>> result;
result.ensure_capacity(style_value.as_value_list().size());
for (auto const& value : style_value.as_value_list().values()) {
VERIFY(value->is_transformation());
result.unchecked_append(value->as_transformation());
}
return result;
};
auto from_transformations = style_value_to_transformations(from);
auto to_transformations = style_value_to_transformations(to);
if (from_transformations.size() != to_transformations.size()) {
// * extend the shorter list to the length of the longer list, setting the function at each additional
// position to the identity transform function matching the function at the corresponding position in the
// longer list. Both transform function lists are then interpolated following the next rule.
auto& shorter_list = from_transformations.size() < to_transformations.size() ? from_transformations : to_transformations;
auto const& longer_list = from_transformations.size() < to_transformations.size() ? to_transformations : from_transformations;
for (size_t i = shorter_list.size(); i < longer_list.size(); ++i) {
auto const& transformation = longer_list[i];
shorter_list.append(TransformationStyleValue::identity_transformation(transformation->transform_function()));
}
}
// https://drafts.csswg.org/css-transforms-1/#transform-primitives
auto is_2d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate,
TransformFunction::Scale,
TransformFunction::Translate);
};
auto is_2d_transform = [&is_2d_primitive](TransformFunction function) {
return is_2d_primitive(function)
|| first_is_one_of(function,
TransformFunction::ScaleX,
TransformFunction::ScaleY,
TransformFunction::TranslateX,
TransformFunction::TranslateY);
};
// https://drafts.csswg.org/css-transforms-2/#transform-primitives
auto is_3d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate3d,
TransformFunction::Scale3d,
TransformFunction::Translate3d);
};
auto is_3d_transform = [&is_2d_transform, &is_3d_primitive](TransformFunction function) {
return is_2d_transform(function)
|| is_3d_primitive(function)
|| first_is_one_of(function,
TransformFunction::RotateX,
TransformFunction::RotateY,
TransformFunction::RotateZ,
TransformFunction::ScaleZ,
TransformFunction::TranslateZ);
};
auto convert_2d_transform_to_primitive = [](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Scale:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
break;
case TransformFunction::ScaleX:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleY:
generic_function = TransformFunction::Scale;
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Rotate:
generic_function = TransformFunction::Rotate;
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateX:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateY:
generic_function = TransformFunction::Translate;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
VERIFY_NOT_REACHED();
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
auto convert_3d_transform_to_primitive = [&](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
// NB: Convert to 2D primitive if possible so we don't have to deal with scale/translate X/Y separately.
if (is_2d_transform(transform->transform_function()))
transform = convert_2d_transform_to_primitive(transform);
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Rotate:
case TransformFunction::RotateZ:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateX:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateY:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Scale:
generic_function = TransformFunction::Scale3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleZ:
generic_function = TransformFunction::Scale3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateZ:
generic_function = TransformFunction::Translate3d;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
generic_function = TransformFunction::Matrix3d;
// NB: Called during animation interpolation.
auto paintable_box = [&] -> Optional<Painting::PaintableBox const&> {
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 <matrix()>, matrix3d() and perspective() get converted into 4x4 matrices first and
// interpolated as defined in section Interpolation of Matrices afterwards.
if (first_is_one_of(TransformFunction::Matrix, from_function, to_function)
|| first_is_one_of(TransformFunction::Matrix3d, from_function, to_function)
|| first_is_one_of(TransformFunction::Perspective, from_function, to_function)) {
break;
}
// If both transform functions share a primitive in the two-dimensional space, both transform functions get
// converted to the two-dimensional primitive. If one or both transform functions are three-dimensional
// transform functions, the common three-dimensional primitive is used.
if (is_2d_transform(from_function) && is_2d_transform(to_function)) {
from_transformation = convert_2d_transform_to_primitive(from_transformation);
to_transformation = convert_2d_transform_to_primitive(to_transformation);
} else if (is_3d_transform(from_function) || is_3d_transform(to_function)) {
// NB: 3D primitives do not support value expansion like their 2D counterparts do (e.g. scale(1.5) ->
// scale(1.5, 1.5), so we check if they are already a primitive first.
if (!is_3d_primitive(from_function))
from_transformation = convert_3d_transform_to_primitive(from_transformation);
if (!is_3d_primitive(to_function))
to_transformation = convert_3d_transform_to_primitive(to_transformation);
}
from_function = from_transformation->transform_function();
to_function = to_transformation->transform_function();
// NB: We converted both functions to their primitives. But if they're different primitives or if they have a
// different number of values, we can't interpolate numerically between them. Break here so the next loop
// can take care of the remaining functions.
auto const& from_values = from_transformation->values();
auto const& to_values = to_transformation->values();
if (from_function != to_function || from_values.size() != to_values.size())
break;
// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
if (from_function == TransformFunction::Rotate3d) {
// FIXME: For interpolations with the primitive rotate3d(), the direction vectors of the transform functions get
// normalized first. If the normalized vectors are not equal and both rotation angles are non-zero the
// transform functions get converted into 4x4 matrices first and interpolated as defined in section
// Interpolation of Matrices afterwards. Otherwise the rotation angle gets interpolated numerically and the
// rotation vector of the non-zero angle is used or (0, 0, 1) if both angles are zero.
auto interpolated_rotation = interpolate_rotate(element, calculation_context, from_transformation,
to_transformation, delta, AllowDiscrete::No);
if (!interpolated_rotation)
break;
result.unchecked_append(*interpolated_rotation);
} else {
StyleValueVector interpolated;
interpolated.ensure_capacity(from_values.size());
for (size_t i = 0; i < from_values.size(); ++i) {
auto interpolated_value = interpolate_value(element, calculation_context, from_values[i], to_values[i],
delta, AllowDiscrete::No);
if (!interpolated_value)
break;
interpolated.unchecked_append(*interpolated_value);
}
if (interpolated.size() != from_values.size())
break;
result.unchecked_append(TransformationStyleValue::create(PropertyID::Transform, from_function, move(interpolated)));
}
}
// NB: Return if we're done.
if (index == from_transformations.size())
return StyleValueList::create(move(result), StyleValueList::Separator::Space);
// * 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<Painting::PaintableBox const&> paintable_box;
if (auto* paintable = as_if<Painting::PaintableBox>(element.unsafe_paintable()))
paintable_box = *paintable;
auto post_multiply_remaining_transformations = [&paintable_box](size_t start_index, Vector<NonnullRefPtr<TransformationStyleValue const>> const& transformations) -> Optional<FloatMatrix4x4> {
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);
}
RefPtr<StyleValue const> 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 interpolated_color_value = interpolate_color(*from_shadow.color(), *to_shadow.color(), delta, {}, color_resolution_context);
if (!interpolated_color_value)
interpolated_color_value = ColorStyleValue::create_from_color(Color::Black, ColorSyntax::Modern);
auto result_shadow = ShadowStyleValue::create(
from_shadow.shadow_type(),
interpolated_color_value.release_nonnull(),
*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<ValueType> 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<StyleValue const> 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<DimensionStyleValue>(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<DimensionStyleValue>(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<typename T>
static NonnullRefPtr<StyleValue const> 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();
}
static RefPtr<StyleValue const> 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<StyleValue const> from_position, RefPtr<StyleValue const> to_position) -> Optional<RefPtr<StyleValue const>> {
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<BasicShape> {
// If both shapes are of type inset(), interpolate between each value in the shape functions.
auto& to_inset = to_shape.get<Inset>();
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<BasicShape> {
// If both shapes are the same type, that type is ellipse() or circle(), and the radiuses are specified
// as <length-percentage> (rather than keywords), interpolate between each value in the shape functions.
auto const& to_circle = to_shape.get<Circle>();
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<BasicShape> {
auto const& to_ellipse = to_shape.get<Ellipse>();
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<BasicShape> {
// 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<Polygon>();
if (from_polygon.fill_rule != to_polygon.fill_rule)
return {};
if (from_polygon.points.size() != to_polygon.points.size())
return {};
Vector<Polygon::Point> 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<BasicShape> {
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<float>::max() } }, { ValueType::Percentage, { 0, AK::NumericLimits<float>::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());
}
if (auto interpolated = interpolate_color(from, to, delta, {}, color_resolution_context))
return interpolated;
return ColorStyleValue::create_from_color(Color::Black, 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::FitContent: {
auto const& from_length_percentage = from.as_fit_content().length_percentage_style_value();
auto const& to_length_percentage = to.as_fit_content().length_percentage_style_value();
if (!from_length_percentage || !to_length_percentage)
return {};
auto interpolated_length_percentage = interpolate_value_impl(element, calculation_context, *from_length_percentage, *to_length_percentage, delta, allow_discrete);
if (!interpolated_length_percentage)
return {};
return FitContentStyleValue::create(interpolated_length_percentage.release_nonnull());
}
case StyleValue::Type::Flex: {
auto interpolated_value = interpolate_raw(from.as_flex().flex().to_fr(), to.as_flex().flex().to_fr(), delta, calculation_context.accepted_type_ranges.get(ValueType::Flex));
return FlexStyleValue::create(Flex::make_fr(interpolated_value));
}
case StyleValue::Type::Integer: {
// https://drafts.csswg.org/css-values/#combine-integers
// Interpolation of <integer> is defined as Vresult = round((1 - p) × VA + p × VB);
// that is, interpolation happens in the real number space as for <number>s, and the result is converted to an <integer> 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 <position> is defined as the independent interpolation of each component (x, y) normalized as an offset from the top left corner as a <length-percentage>.
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<RadialExtent>(); };
// 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<float>::max() } },
}
};
if (from_components.size() == 1 && to_components.size() == 1) {
auto const& from_component = from_components[0].get<NonnullRefPtr<StyleValue const>>();
auto const& to_component = to_components[0].get<NonnullRefPtr<StyleValue const>>();
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<NonnullRefPtr<StyleValue const>>();
auto const& from_vertical_component = from_components.size() > 1 ? from_components[1].get<NonnullRefPtr<StyleValue const>>() : from_horizontal_component;
auto const& to_horizontal_component = to_components[0].get<NonnullRefPtr<StyleValue const>>();
auto const& to_vertical_component = to_components.size() > 1 ? to_components[1].get<NonnullRefPtr<StyleValue const>>() : 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 <ratio> is degenerate, the values cannot be interpolated.
if (from_ratio.is_degenerate() || to_ratio.is_degenerate())
return {};
// The interpolation of a <ratio> is defined by converting each <ratio> 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 <ratio> by inverting the logarithm, then interpreting the result as a <ratio> 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<double>)
return 0;
// ∞ Return 1.
if (s == AK::Infinity<double>)
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 <number [0,1]> interpolationValue back to a superellipse parameter, switch on interpolationValue:
// 0 Return -∞.
if (interpolation_value == 0)
return -AK::Infinity<double>;
// 0.5 Return 0.
if (interpolation_value == 0.5)
return 0;
// 1 Return ∞.
if (interpolation_value == 1)
return AK::Infinity<double>;
// 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<float>::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<StyleValue const> 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<void(NonnullRefPtr<StyleValue const>)> 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<FilterValue> accumulate_filter_function(FilterValueListStyleValue const& underlying_list, FilterValueListStyleValue const& animated_list)
{
// Accumulation of <filter-value-list>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
// <filter-function> pairs, their arguments are arithmetically added together - except in the case of
// <filter-function>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<FilterValue> {
return underlying.visit(
[&](FilterOperation::Blur const& underlying_blur) -> Optional<FilterValue> {
if (!animated.has<FilterOperation::Blur>())
return {};
auto const& animated_blur = animated.get<FilterOperation::Blur>();
return FilterOperation::Blur { .radius = LengthStyleValue::create(Length::make_px(underlying_blur.resolved_radius() + animated_blur.resolved_radius())) };
},
[&](FilterOperation::HueRotate const& underlying_rotate) -> Optional<FilterValue> {
if (!animated.has<FilterOperation::HueRotate>())
return {};
auto const& animated_rotate = animated.get<FilterOperation::HueRotate>();
return FilterOperation::HueRotate { .angle = AngleStyleValue::create(Angle::make_degrees(underlying_rotate.angle_degrees() + animated_rotate.angle_degrees())) };
},
[&](FilterOperation::Color const& underlying_color) -> Optional<FilterValue> {
if (!animated.has<FilterOperation::Color>())
return {};
auto const& animated_color = animated.get<FilterOperation::Color>();
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<FilterValue> {
if (!animated.has<FilterOperation::DropShadow>())
return {};
auto const& animated_shadow = animated.get<FilterOperation::DropShadow>();
auto add_lengths = [](NonnullRefPtr<StyleValue const> const& a, NonnullRefPtr<StyleValue const> const& b) -> NonnullRefPtr<StyleValue const> {
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<StyleValue const> 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<StyleValue const> 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<FilterValue> {
return {};
});
};
// Extend shorter list with initial values
size_t max_size = max(underlying_list.size(), animated_list.size());
Vector<FilterValue> extended_underlying;
Vector<FilterValue> 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<FilterValue> 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<StyleValue const> 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<typename T>
static T composite_raw_values(T underlying_raw_value, T animated_raw_value)
{
return underlying_raw_value + animated_raw_value;
}
static Optional<GridTrackSizeList> composite_grid_track_size_list(PropertyID property_id, CalculationContext const& calculation_context, GridTrackSizeList const& underlying, GridTrackSizeList const& animated, Bindings::CompositeOperation composite_operation)
{
auto composite_grid_size = [&](GridSize const& underlying_grid_size, GridSize const& animated_grid_size) -> Optional<GridSize> {
if (auto composited_value = composite_value(property_id, underlying_grid_size.style_value(), animated_grid_size.style_value(), composite_operation))
return GridSize { *composited_value };
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(property_id, calculation_context, underlying_repeat.grid_track_size_list(), animated_repeat.grid_track_size_list(), composite_operation);
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<StyleValue const> 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 <percentage> 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<DimensionStyleValue>(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<DimensionStyleValue>(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<StyleValue const> 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<double> {
auto const& underlying_dimension = as<DimensionStyleValue>(underlying_value);
auto const& animated_dimension = as<DimensionStyleValue>(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<StyleValue const> {
auto const& animated_inset = animated_basic_shape.basic_shape().get<Inset>();
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<StyleValue const> {
auto const& animated_circle = animated_basic_shape.basic_shape().get<Circle>();
auto composited_radius = composite_value(property_id, underlying_circle.radius, animated_circle.radius, composite_operation);
if (!composited_radius)
return {};
RefPtr<StyleValue const> composited_position;
if (underlying_circle.position || animated_circle.position) {
auto const& underlying_position_with_default = underlying_circle.position ? ValueComparingNonnullRefPtr<StyleValue const> { *underlying_circle.position } : PositionStyleValue::create_computed_center();
auto const& animated_position_with_default = animated_circle.position ? ValueComparingNonnullRefPtr<StyleValue const> { *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<StyleValue const> {
auto const& animated_ellipse = animated_basic_shape.basic_shape().get<Ellipse>();
auto composited_radius = composite_value(property_id, underlying_ellipse.radius, animated_ellipse.radius, composite_operation);
if (!composited_radius)
return {};
RefPtr<StyleValue const> composited_position;
if (underlying_ellipse.position || animated_ellipse.position) {
auto const& underlying_position_with_default = underlying_ellipse.position ? ValueComparingNonnullRefPtr<StyleValue const> { *underlying_ellipse.position } : PositionStyleValue::create_computed_center();
auto const& animated_position_with_default = animated_ellipse.position ? ValueComparingNonnullRefPtr<StyleValue const> { *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<StyleValue const> {
auto const& animated_polygon = animated_basic_shape.basic_shape().get<Polygon>();
if (underlying_polygon.fill_rule != animated_polygon.fill_rule)
return {};
if (underlying_polygon.points.size() != animated_polygon.points.size())
return {};
Vector<Polygon::Point> 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<StyleValue const> {
// xywh() should have been absolutized into inset() before now
VERIFY_NOT_REACHED();
},
[&](Rect const&) -> RefPtr<StyleValue const> {
// rect() should have been absolutized into inset() before now
VERIFY_NOT_REACHED();
},
[&](Path const&) -> RefPtr<StyleValue const> {
// 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::FitContent: {
auto underlying_length_percentage = underlying_value.as_fit_content().length_percentage_style_value();
auto animated_length_percentage = animated_value.as_fit_content().length_percentage_style_value();
if (!underlying_length_percentage || !animated_length_percentage)
return {};
auto composited_length_percentage = composite_value(property_id, *underlying_length_percentage, *animated_length_percentage, composite_operation);
if (!composited_length_percentage)
return {};
return FitContentStyleValue::create(composited_length_percentage.release_nonnull());
}
case StyleValue::Type::Flex: {
auto result = composite_raw_values(underlying_value.as_flex().flex().to_fr(), animated_value.as_flex().flex().to_fr());
return FlexStyleValue::create(Flex::make_fr(result));
}
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(property_id, calculation_context, underlying_list, animated_list, composite_operation);
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<RadialExtent>(); };
// 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<NonnullRefPtr<StyleValue const>>();
auto const& animated_component = animated_components[0].get<NonnullRefPtr<StyleValue const>>();
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<NonnullRefPtr<StyleValue const>>();
auto const& underlying_vertical_component = underlying_components.size() > 1 ? underlying_components[1].get<NonnullRefPtr<StyleValue const>>() : underlying_horizontal_component;
auto const& animated_horizontal_component = animated_components[0].get<NonnullRefPtr<StyleValue const>>();
auto const& animated_vertical_component = animated_components.size() > 1 ? animated_components[1].get<NonnullRefPtr<StyleValue const>>() : 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 <ratio>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<FilterValue> 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 {};
}
}
}