/*
 * 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/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
#include <LibWeb/CSS/StyleValues/FlexStyleValue.h>
#include <LibWeb/CSS/StyleValues/FontStyleStyleValue.h>
#include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h>
#include <LibWeb/CSS/StyleValues/FunctionStyleValue.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/OpacityValueStyleValue.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<NumericRange> accepted_range = {})
{
    if constexpr (AK::Detail::IsSame<T, double>) {
        if (accepted_range.has_value())
            return clamp(from + (to - from) * static_cast<double>(delta), accepted_range->min, accepted_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_range.has_value() ? accepted_range->min : NumericLimits<T>::min();
        auto max = accepted_range.has_value() ? accepted_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_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(StyleValue const& a_from, StyleValue const& a_to, float delta)
{
    if (a_from.to_keyword() == Keyword::None && a_to.to_keyword() == Keyword::None)
        return a_from;

    static auto const& one = TransformationStyleValue::create(PropertyID::Scale, TransformFunction::Scale, { NumberStyleValue::create(1), NumberStyleValue::create(1) }).leak_ref();

    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_raw(number_from_style_value(from_transform.values()[0], 1), number_from_style_value(to_transform.values()[0], 1), delta, infinite_range);
    auto interpolated_y = interpolate_raw(number_from_style_value(from_transform.values()[1], 1), number_from_style_value(to_transform.values()[1], 1), delta, infinite_range);
    Optional<double> interpolated_z;

    if (from_transform.values().size() == 3 || to_transform.values().size() == 3) {
        static auto const& one_value = NumberStyleValue::create(1).leak_ref();
        auto from = from_transform.values().size() == 3 ? from_transform.values()[2] : ValueComparingNonnullRefPtr<StyleValue const> { one_value };
        auto to = to_transform.values().size() == 3 ? to_transform.values()[2] : ValueComparingNonnullRefPtr<StyleValue const> { one_value };
        interpolated_z = interpolate_raw(number_from_style_value(from, 1), number_from_style_value(to, 1), delta, infinite_range);
    }

    StyleValueVector new_values = { NumberStyleValue::create(interpolated_x), NumberStyleValue::create(interpolated_y) };
    if (interpolated_z.has_value())
        new_values.append(NumberStyleValue::create(interpolated_z.value()));

    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_ranges_by_type.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_ranges_by_type.set(ValueType::Number, { 0, 1 });
                break;
            case Gfx::ColorFilterType::Brightness:
            case Gfx::ColorFilterType::Contrast:
            case Gfx::ColorFilterType::Saturate:
                filter_function_calculation_context.accepted_ranges_by_type.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 const& zero_px = LengthStyleValue::create(Length::make_px(0)).leak_ref();
    static auto const& zero = TransformationStyleValue::create(PropertyID::Translate, TransformFunction::Translate, { zero_px, zero_px }).leak_ref();

    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 const& zero_degrees_value = AngleStyleValue::create(Angle::make_degrees(0)).leak_ref();
    static auto const& zero = TransformationStyleValue::create(PropertyID::Rotate, TransformFunction::Rotate, { zero_degrees_value }).leak_ref();

    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)
{
    // https://drafts.csswg.org/css-grid-2/#track-sizing
    // Animation type: if the list lengths match, by computed value type per item in the computed track list;
    // discrete otherwise.
    //
    // https://drafts.csswg.org/css-grid-2/#computed-track-list-subgrid
    // The computed track list of a subgrid axis is the subgrid keyword followed by a list of line names.
    if (from.is_subgrid() || to.is_subgrid())
        return {};

    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) {
            static auto const& oblique_0deg_value = FontStyleStyleValue::create(FontStyleKeyword::Oblique, AngleStyleValue::create(Angle::make_degrees(0))).leak_ref();
            auto from_value = from->as_font_style().font_style() == FontStyleKeyword::Normal ? ValueComparingNonnullRefPtr<StyleValue const> { oblique_0deg_value } : from;
            auto to_value = to->as_font_style().font_style() == FontStyleKeyword::Normal ? ValueComparingNonnullRefPtr<StyleValue const> { oblique_0deg_value } : to;
            return interpolate_value(element, calculation_context, from_value, to_value, delta, allow_discrete);
        }

        if (property_id == PropertyID::FontVariationSettings) {
            // https://drafts.csswg.org/css-fonts/#font-variation-settings-def
            // Two declarations of font-feature-settings can be animated between if they are "like". "Like" declarations
            // are ones where the same set of properties appear (in any order). Because successive duplicate properties
            // are applied instead of prior duplicate properties, two declarations can be "like" even if they have
            // differing number of properties. If two declarations are "like" then animation occurs pairwise between
            // corresponding values in the declarations. Otherwise, animation is not possible.
            if (!from->is_value_list() || !to->is_value_list())
                return interpolate_discrete(from, to, delta, allow_discrete);

            // The values in these lists have already been deduplicated and sorted at this point, so we can use
            // interpolate_value() to interpolate them pairwise.
            return interpolate_value(element, calculation_context, from, to, delta, allow_discrete);
        }

        // https://drafts.csswg.org/web-animations-1/#animating-visibility
        if (property_id == PropertyID::Visibility) {
            // For the visibility property, visible is interpolated as a discrete step where values of p between 0 and 1 map to visible and other values of p map to the closer endpoint.
            // If neither value is visible, then discrete animation is used.
            if (from->equals(to))
                return from;

            auto from_is_visible = from->to_keyword() == Keyword::Visible;
            auto to_is_visible = to->to_keyword() == Keyword::Visible;

            if (from_is_visible || to_is_visible) {
                if (delta <= 0)
                    return from;
                if (delta >= 1)
                    return to;
                return KeywordStyleValue::create(Keyword::Visible);
            }

            return interpolate_discrete(from, to, delta, allow_discrete);
        }

        // https://drafts.csswg.org/css-contain/#content-visibility-animation
        if (property_id == PropertyID::ContentVisibility) {
            // In general, the content-visibility property’s animation type is discrete.
            // However, similar to interpolation of visibility, during interpolation between hidden and any other content-visibility value,
            // p values between 0 and 1 map to the non-hidden value.
            if (from->equals(to))
                return from;

            auto from_is_hidden = from->to_keyword() == Keyword::Hidden;
            auto to_is_hidden = to->to_keyword() == Keyword::Hidden;

            if (from_is_hidden || to_is_hidden) {
                auto non_hidden_value = from_is_hidden ? to : from;
                if (delta <= 0)
                    return from;
                if (delta >= 1)
                    return to;
                return non_hidden_value;
            }
            return interpolate_discrete(from, to, delta, allow_discrete);
        }

        // https://drafts.csswg.org/css-display-4/#display-animation
        if (property_id == PropertyID::Display) {
            // In general, the display property’s animation type is discrete. However, similar to interpolation of
            // visibility (see Web Animations §  Animation of visibility), during interpolation between none and any
            // other display value, p values between 0 and 1 map to the non-none value. Additionally, the element is
            // inert as long as its display value would compute to none when ignoring the Transitions and Animations
            // cascade origins.
            // FIXME: Implement the inertness portion of this.

            if (from->equals(to))
                return from;

            auto from_is_none = from->as_display().display().is_none();
            auto to_is_none = to->as_display().display().is_none();

            if (from_is_none || to_is_none) {
                auto non_none_value = from_is_none ? to : from;
                if (delta <= 0)
                    return from;
                if (delta >= 1)
                    return to;
                return non_none_value;
            }

            return interpolate_discrete(from, to, delta, allow_discrete);
        }

        if (property_id == PropertyID::Scale) {
            if (auto result = interpolate_scale(from, to, delta))
                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());
        }

        if (property_id == PropertyID::StrokeDasharray) {
            // https://svgwg.org/svg2-draft/painting.html#StrokeDashing
            // If either start or end compute to none or are invalid, start or end are combined using the discrete animation type.
            if (!from->is_value_list() || !to->is_value_list())
                return interpolate_discrete(from, to, delta, allow_discrete);

            // Otherwise, repeat both dash patterns of start and end value list until the length of elements in
            // both value lists match. Each item is then combined by computed value.
            if (auto result = interpolate_repeatable_list(element, calculation_context, from, to, delta, allow_discrete))
                return result.release_nonnull();
            return interpolate_discrete(from, to, delta, allow_discrete);
        }

        // 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(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;
    auto paintable = element.unsafe_paintable();
    if (auto const* box = as_if<Painting::PaintableBox>(paintable.ptr()))
        paintable_box = *box;

    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 const& transformation = transformations[index];
            if (!paintable_box.has_value() && !transformation->can_be_converted_to_matrix_without_reference_box())
                return {};
            result = result * transformation->to_matrix(paintable_box);
        }

        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(*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_ranges_by_type.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_ranges_by_type = { { 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(*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_ranges_by_type = { { 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::Flex: {
        auto interpolated_value = interpolate_raw(from.as_flex().flex().to_fr(), to.as_flex().flex().to_fr(), delta, calculation_context.accepted_ranges_by_type.get(ValueType::Flex));
        return FlexStyleValue::create(Flex::make_fr(interpolated_value));
    }
    case StyleValue::Type::Function: {
        auto const& from_function = from.as_function();
        auto const& to_function = to.as_function();

        if (from_function.name() != to_function.name())
            return {};

        auto interpolated_value = interpolate_value(element, calculation_context, from_function.value(), to_function.value(), delta, allow_discrete);
        if (!interpolated_value)
            return {};

        return FunctionStyleValue::create(from_function.name(), interpolated_value.release_nonnull());
    }
    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_ranges_by_type.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_ranges_by_type.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_ranges_by_type.get(ValueType::Number));
        return NumberStyleValue::create(interpolated_value);
    }
    case StyleValue::Type::OpacityValue: {
        auto interpolated_value = interpolate_raw(from.as_opacity_value().resolved(), to.as_opacity_value().resolved(), delta, NumericRange { .min = 0, .max = 1 });
        return OpacityValueStyleValue::create(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_ranges_by_type.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_ranges_by_type = {
                { 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_ranges_by_type.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, NumericRange { .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)
{
    // https://drafts.csswg.org/css-grid-2/#track-sizing
    // Animation type: if the list lengths match, by computed value type per item in the computed track list;
    // discrete otherwise.
    //
    // https://drafts.csswg.org/css-grid-2/#computed-track-list-subgrid
    // The computed track list of a subgrid axis is the subgrid keyword followed by a list of line names.
    if (underlying.is_subgrid() || animated.is_subgrid())
        return {};

    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::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::Function: {
        auto const& underlying_function = underlying_value.as_function();
        auto const& animated_function = animated_value.as_function();

        if (underlying_function.name() != animated_function.name())
            return {};

        auto composited_value = composite_value(property_id, underlying_function.value(), animated_function.value(), composite_operation);
        if (!composited_value)
            return {};

        return FunctionStyleValue::create(underlying_function.name(), composited_value.release_nonnull());
    }
    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 {};
    }
}

}
