Coverage Report

Created: 2025-11-16 07:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/CSS/Interpolation.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2021, the SerenityOS developers.
4
 * Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
5
 * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
6
 *
7
 * SPDX-License-Identifier: BSD-2-Clause
8
 */
9
10
#include "Interpolation.h"
11
#include <LibWeb/CSS/PropertyID.h>
12
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
13
#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
14
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
15
#include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h>
16
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
17
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
18
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
19
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
20
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
21
#include <LibWeb/CSS/StyleValues/RectStyleValue.h>
22
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
23
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
24
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
25
#include <LibWeb/CSS/Transformation.h>
26
#include <LibWeb/DOM/Element.h>
27
#include <LibWeb/Layout/Node.h>
28
#include <LibWeb/Painting/PaintableBox.h>
29
30
namespace Web::CSS {
31
32
template<typename T>
33
static T interpolate_raw(T from, T to, float delta)
34
0
{
35
0
    if constexpr (AK::Detail::IsSame<T, double>) {
36
0
        return from + (to - from) * static_cast<double>(delta);
37
0
    } else {
38
0
        return static_cast<AK::Detail::RemoveCVReference<T>>(from + (to - from) * delta);
39
0
    }
40
0
}
Unexecuted instantiation: Interpolation.cpp:Gfx::VectorN<3ul, float> Web::CSS::interpolate_raw<Gfx::VectorN<3ul, float> >(Gfx::VectorN<3ul, float>, Gfx::VectorN<3ul, float>, float)
Unexecuted instantiation: Interpolation.cpp:Gfx::VectorN<4ul, float> Web::CSS::interpolate_raw<Gfx::VectorN<4ul, float> >(Gfx::VectorN<4ul, float>, Gfx::VectorN<4ul, float>, float)
Unexecuted instantiation: Interpolation.cpp:float Web::CSS::interpolate_raw<float>(float, float, float)
Unexecuted instantiation: Interpolation.cpp:unsigned char Web::CSS::interpolate_raw<unsigned char>(unsigned char, unsigned char, float)
Unexecuted instantiation: Interpolation.cpp:double Web::CSS::interpolate_raw<double>(double, double, float)
Unexecuted instantiation: Interpolation.cpp:long Web::CSS::interpolate_raw<long>(long, long, float)
41
42
ValueComparingRefPtr<CSSStyleValue const> interpolate_property(DOM::Element& element, PropertyID property_id, CSSStyleValue const& from, CSSStyleValue const& to, float delta)
43
0
{
44
0
    auto animation_type = animation_type_from_longhand_property(property_id);
45
0
    switch (animation_type) {
46
0
    case AnimationType::ByComputedValue:
47
0
        return interpolate_value(element, from, to, delta);
48
0
    case AnimationType::None:
49
0
        return to;
50
0
    case AnimationType::Custom: {
51
0
        if (property_id == PropertyID::Transform) {
52
0
            if (auto interpolated_transform = interpolate_transform(element, from, to, delta))
53
0
                return *interpolated_transform;
54
55
            // https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
56
            // In some cases, an animation might cause a transformation matrix to be singular or non-invertible.
57
            // For example, an animation in which scale moves from 1 to -1. At the time when the matrix is in
58
            // such a state, the transformed element is not rendered.
59
0
            return {};
60
0
        }
61
0
        if (property_id == PropertyID::BoxShadow)
62
0
            return interpolate_box_shadow(element, from, to, delta);
63
64
        // FIXME: Handle all custom animatable properties
65
0
        [[fallthrough]];
66
0
    }
67
    // FIXME: Handle repeatable-list animatable properties
68
0
    case AnimationType::RepeatableList:
69
0
    case AnimationType::Discrete:
70
0
    default:
71
0
        return delta >= 0.5f ? to : from;
72
0
    }
73
0
}
74
75
// https://drafts.csswg.org/css-transitions/#transitionable
76
bool property_values_are_transitionable(PropertyID property_id, CSSStyleValue const& old_value, CSSStyleValue const& new_value)
77
0
{
78
    // When comparing the before-change style and after-change style for a given property,
79
    // the property values are transitionable if they have an animation type that is neither not animatable nor discrete.
80
81
0
    auto animation_type = animation_type_from_longhand_property(property_id);
82
0
    if (animation_type == AnimationType::None || animation_type == AnimationType::Discrete)
83
0
        return false;
84
85
    // FIXME: Even when a property is transitionable, the two values may not be. The spec uses the example of inset/non-inset shadows.
86
0
    (void)old_value;
87
0
    (void)new_value;
88
0
    return true;
89
0
}
90
91
// A null return value means the interpolated matrix was not invertible or otherwise invalid
92
RefPtr<CSSStyleValue const> interpolate_transform(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta)
93
0
{
94
    // Note that the spec uses column-major notation, so all the matrix indexing is reversed.
95
96
0
    static constexpr auto make_transformation = [](TransformationStyleValue const& transformation) -> AK::Optional<Transformation> {
97
0
        AK::Vector<TransformValue> values;
98
99
0
        for (auto const& value : transformation.values()) {
100
0
            switch (value->type()) {
101
0
            case CSSStyleValue::Type::Angle:
102
0
                values.append(AngleOrCalculated { value->as_angle().angle() });
103
0
                break;
104
0
            case CSSStyleValue::Type::Math:
105
0
                values.append(LengthPercentage { value->as_math() });
106
0
                break;
107
0
            case CSSStyleValue::Type::Length:
108
0
                values.append(LengthPercentage { value->as_length().length() });
109
0
                break;
110
0
            case CSSStyleValue::Type::Percentage:
111
0
                values.append(LengthPercentage { value->as_percentage().percentage() });
112
0
                break;
113
0
            case CSSStyleValue::Type::Number:
114
0
                values.append(NumberPercentage { Number(Number::Type::Number, value->as_number().number()) });
115
0
                break;
116
0
            default:
117
0
                return {};
118
0
            }
119
0
        }
120
121
0
        return Transformation { transformation.transform_function(), move(values) };
122
0
    };
123
124
0
    static constexpr auto transformation_style_value_to_matrix = [](DOM::Element& element, TransformationStyleValue const& value) -> Optional<FloatMatrix4x4> {
125
0
        auto transformation = make_transformation(value);
126
0
        if (!transformation.has_value())
127
0
            return {};
128
0
        Optional<Painting::PaintableBox const&> paintable_box;
129
0
        if (auto layout_node = element.layout_node()) {
130
0
            if (auto paintable = layout_node->paintable(); paintable && is<Painting::PaintableBox>(paintable))
131
0
                paintable_box = *static_cast<Painting::PaintableBox*>(paintable);
132
0
        }
133
0
        if (auto matrix = transformation->to_matrix(paintable_box); !matrix.is_error())
134
0
            return matrix.value();
135
0
        return {};
136
0
    };
137
138
0
    static constexpr auto style_value_to_matrix = [](DOM::Element& element, CSSStyleValue const& value) -> FloatMatrix4x4 {
139
0
        if (value.is_transformation())
140
0
            return transformation_style_value_to_matrix(element, value.as_transformation()).value_or(FloatMatrix4x4::identity());
141
142
        // This encompasses both the allowed value "none" and any invalid values
143
0
        if (!value.is_value_list())
144
0
            return FloatMatrix4x4::identity();
145
146
0
        auto matrix = FloatMatrix4x4::identity();
147
0
        for (auto const& value_element : value.as_value_list().values()) {
148
0
            if (value_element->is_transformation()) {
149
0
                if (auto value_matrix = transformation_style_value_to_matrix(element, value_element->as_transformation()); value_matrix.has_value())
150
0
                    matrix = matrix * value_matrix.value();
151
0
            }
152
0
        }
153
154
0
        return matrix;
155
0
    };
156
157
0
    struct DecomposedValues {
158
0
        FloatVector3 translation;
159
0
        FloatVector3 scale;
160
0
        FloatVector3 skew;
161
0
        FloatVector4 rotation;
162
0
        FloatVector4 perspective;
163
0
    };
164
    // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
165
0
    static constexpr auto decompose = [](FloatMatrix4x4 matrix) -> Optional<DecomposedValues> {
166
        // https://drafts.csswg.org/css-transforms-1/#supporting-functions
167
0
        static constexpr auto combine = [](auto a, auto b, float ascl, float bscl) {
168
0
            return FloatVector3 {
169
0
                ascl * a[0] + bscl * b[0],
170
0
                ascl * a[1] + bscl * b[1],
171
0
                ascl * a[2] + bscl * b[2],
172
0
            };
173
0
        };
174
175
        // Normalize the matrix.
176
0
        if (matrix(3, 3) == 0.f)
177
0
            return {};
178
179
0
        for (int i = 0; i < 4; i++)
180
0
            for (int j = 0; j < 4; j++)
181
0
                matrix(i, j) /= matrix(3, 3);
182
183
        // perspectiveMatrix is used to solve for perspective, but it also provides
184
        // an easy way to test for singularity of the upper 3x3 component.
185
0
        auto perspective_matrix = matrix;
186
0
        for (int i = 0; i < 3; i++)
187
0
            perspective_matrix(3, i) = 0.f;
188
0
        perspective_matrix(3, 3) = 1.f;
189
190
0
        if (!perspective_matrix.is_invertible())
191
0
            return {};
192
193
0
        DecomposedValues values;
194
195
        // First, isolate perspective.
196
0
        if (matrix(3, 0) != 0.f || matrix(3, 1) != 0.f || matrix(3, 2) != 0.f) {
197
            // rightHandSide is the right hand side of the equation.
198
            // Note: It is the bottom side in a row-major matrix
199
0
            FloatVector4 bottom_side = {
200
0
                matrix(3, 0),
201
0
                matrix(3, 1),
202
0
                matrix(3, 2),
203
0
                matrix(3, 3),
204
0
            };
205
206
            // Solve the equation by inverting perspectiveMatrix and multiplying
207
            // rightHandSide by the inverse.
208
0
            auto inverse_perspective_matrix = perspective_matrix.inverse();
209
0
            auto transposed_inverse_perspective_matrix = inverse_perspective_matrix.transpose();
210
0
            values.perspective = transposed_inverse_perspective_matrix * bottom_side;
211
0
        } else {
212
            // No perspective.
213
0
            values.perspective = { 0.0, 0.0, 0.0, 1.0 };
214
0
        }
215
216
        // Next take care of translation
217
0
        for (int i = 0; i < 3; i++)
218
0
            values.translation[i] = matrix(i, 3);
219
220
        // Now get scale and shear. 'row' is a 3 element array of 3 component vectors
221
0
        FloatVector3 row[3];
222
0
        for (int i = 0; i < 3; i++)
223
0
            row[i] = { matrix(0, i), matrix(1, i), matrix(2, i) };
224
225
        // Compute X scale factor and normalize first row.
226
0
        values.scale[0] = row[0].length();
227
0
        row[0].normalize();
228
229
        // Compute XY shear factor and make 2nd row orthogonal to 1st.
230
0
        values.skew[0] = row[0].dot(row[1]);
231
0
        row[1] = combine(row[1], row[0], 1.f, -values.skew[0]);
232
233
        // Now, compute Y scale and normalize 2nd row.
234
0
        values.scale[1] = row[1].length();
235
0
        row[1].normalize();
236
0
        values.skew[0] /= values.scale[1];
237
238
        // Compute XZ and YZ shears, orthogonalize 3rd row
239
0
        values.skew[1] = row[0].dot(row[2]);
240
0
        row[2] = combine(row[2], row[0], 1.f, -values.skew[1]);
241
0
        values.skew[2] = row[1].dot(row[2]);
242
0
        row[2] = combine(row[2], row[1], 1.f, -values.skew[2]);
243
244
        // Next, get Z scale and normalize 3rd row.
245
0
        values.scale[2] = row[2].length();
246
0
        row[2].normalize();
247
0
        values.skew[1] /= values.scale[2];
248
0
        values.skew[2] /= values.scale[2];
249
250
        // At this point, the matrix (in rows) is orthonormal.
251
        // Check for a coordinate system flip.  If the determinant
252
        // is -1, then negate the matrix and the scaling factors.
253
0
        auto pdum3 = row[1].cross(row[2]);
254
0
        if (row[0].dot(pdum3) < 0.f) {
255
0
            for (int i = 0; i < 3; i++) {
256
0
                values.scale[i] *= -1.f;
257
0
                row[i][0] *= -1.f;
258
0
                row[i][1] *= -1.f;
259
0
                row[i][2] *= -1.f;
260
0
            }
261
0
        }
262
263
        // Now, get the rotations out
264
0
        values.rotation[0] = 0.5f * sqrt(max(1.f + row[0][0] - row[1][1] - row[2][2], 0.f));
265
0
        values.rotation[1] = 0.5f * sqrt(max(1.f - row[0][0] + row[1][1] - row[2][2], 0.f));
266
0
        values.rotation[2] = 0.5f * sqrt(max(1.f - row[0][0] - row[1][1] + row[2][2], 0.f));
267
0
        values.rotation[3] = 0.5f * sqrt(max(1.f + row[0][0] + row[1][1] + row[2][2], 0.f));
268
269
0
        if (row[2][1] > row[1][2])
270
0
            values.rotation[0] = -values.rotation[0];
271
0
        if (row[0][2] > row[2][0])
272
0
            values.rotation[1] = -values.rotation[1];
273
0
        if (row[1][0] > row[0][1])
274
0
            values.rotation[2] = -values.rotation[2];
275
276
        // FIXME: This accounts for the fact that the browser coordinate system is left-handed instead of right-handed.
277
        //        The reason for this is that the positive Y-axis direction points down instead of up. To fix this, we
278
        //        invert the Y axis. However, it feels like the spec pseudo-code above should have taken something like
279
        //        this into account, so we're probably doing something else wrong.
280
0
        values.rotation[2] *= -1;
281
282
0
        return values;
283
0
    };
284
285
    // https://drafts.csswg.org/css-transforms-2/#recomposing-to-a-3d-matrix
286
0
    static constexpr auto recompose = [](DecomposedValues const& values) -> FloatMatrix4x4 {
287
0
        auto matrix = FloatMatrix4x4::identity();
288
289
        // apply perspective
290
0
        for (int i = 0; i < 4; i++)
291
0
            matrix(3, i) = values.perspective[i];
292
293
        // apply translation
294
0
        for (int i = 0; i < 4; i++) {
295
0
            for (int j = 0; j < 3; j++)
296
0
                matrix(i, 3) += values.translation[j] * matrix(i, j);
297
0
        }
298
299
        // apply rotation
300
0
        auto x = values.rotation[0];
301
0
        auto y = values.rotation[1];
302
0
        auto z = values.rotation[2];
303
0
        auto w = values.rotation[3];
304
305
        // Construct a composite rotation matrix from the quaternion values
306
        // rotationMatrix is a identity 4x4 matrix initially
307
0
        auto rotation_matrix = FloatMatrix4x4::identity();
308
0
        rotation_matrix(0, 0) = 1.f - 2.f * (y * y + z * z);
309
0
        rotation_matrix(1, 0) = 2.f * (x * y - z * w);
310
0
        rotation_matrix(2, 0) = 2.f * (x * z + y * w);
311
0
        rotation_matrix(0, 1) = 2.f * (x * y + z * w);
312
0
        rotation_matrix(1, 1) = 1.f - 2.f * (x * x + z * z);
313
0
        rotation_matrix(2, 1) = 2.f * (y * z - x * w);
314
0
        rotation_matrix(0, 2) = 2.f * (x * z - y * w);
315
0
        rotation_matrix(1, 2) = 2.f * (y * z + x * w);
316
0
        rotation_matrix(2, 2) = 1.f - 2.f * (x * x + y * y);
317
318
0
        matrix = matrix * rotation_matrix;
319
320
        // apply skew
321
        // temp is a identity 4x4 matrix initially
322
0
        auto temp = FloatMatrix4x4::identity();
323
0
        if (values.skew[2] != 0.f) {
324
0
            temp(1, 2) = values.skew[2];
325
0
            matrix = matrix * temp;
326
0
        }
327
328
0
        if (values.skew[1] != 0.f) {
329
0
            temp(1, 2) = 0.f;
330
0
            temp(0, 2) = values.skew[1];
331
0
            matrix = matrix * temp;
332
0
        }
333
334
0
        if (values.skew[0] != 0.f) {
335
0
            temp(0, 2) = 0.f;
336
0
            temp(0, 1) = values.skew[0];
337
0
            matrix = matrix * temp;
338
0
        }
339
340
        // apply scale
341
0
        for (int i = 0; i < 3; i++) {
342
0
            for (int j = 0; j < 4; j++)
343
0
                matrix(j, i) *= values.scale[i];
344
0
        }
345
346
0
        return matrix;
347
0
    };
348
349
    // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values
350
0
    static constexpr auto interpolate = [](DecomposedValues& from, DecomposedValues& to, float delta) -> DecomposedValues {
351
0
        auto product = clamp(from.rotation.dot(to.rotation), -1.0f, 1.0f);
352
0
        FloatVector4 interpolated_rotation;
353
0
        if (fabsf(product) == 1.0f) {
354
0
            interpolated_rotation = from.rotation;
355
0
        } else {
356
0
            auto theta = acos(product);
357
0
            auto w = sin(delta * theta) / sqrtf(1.0f - product * product);
358
359
0
            for (int i = 0; i < 4; i++) {
360
0
                from.rotation[i] *= cos(delta * theta) - product * w;
361
0
                to.rotation[i] *= w;
362
0
                interpolated_rotation[i] = from.rotation[i] + to.rotation[i];
363
0
            }
364
0
        }
365
366
0
        return {
367
0
            interpolate_raw(from.translation, to.translation, delta),
368
0
            interpolate_raw(from.scale, to.scale, delta),
369
0
            interpolate_raw(from.skew, to.skew, delta),
370
0
            interpolated_rotation,
371
0
            interpolate_raw(from.perspective, to.perspective, delta),
372
0
        };
373
0
    };
374
375
0
    auto from_matrix = style_value_to_matrix(element, from);
376
0
    auto to_matrix = style_value_to_matrix(element, to);
377
0
    auto from_decomposed = decompose(from_matrix);
378
0
    auto to_decomposed = decompose(to_matrix);
379
0
    if (!from_decomposed.has_value() || !to_decomposed.has_value())
380
0
        return {};
381
0
    auto interpolated_decomposed = interpolate(from_decomposed.value(), to_decomposed.value(), delta);
382
0
    auto interpolated = recompose(interpolated_decomposed);
383
384
0
    StyleValueVector values;
385
0
    values.ensure_capacity(16);
386
0
    for (int i = 0; i < 16; i++)
387
0
        values.append(NumberStyleValue::create(static_cast<double>(interpolated(i % 4, i / 4))));
388
0
    return StyleValueList::create({ TransformationStyleValue::create(TransformFunction::Matrix3d, move(values)) }, StyleValueList::Separator::Comma);
389
0
}
390
391
Color interpolate_color(Color from, Color to, float delta)
392
0
{
393
    // https://drafts.csswg.org/css-color/#interpolation-space
394
    // If the host syntax does not define what color space interpolation should take place in, it defaults to Oklab.
395
0
    auto from_oklab = from.to_oklab();
396
0
    auto to_oklab = to.to_oklab();
397
398
0
    auto color = Color::from_oklab(
399
0
        interpolate_raw(from_oklab.L, to_oklab.L, delta),
400
0
        interpolate_raw(from_oklab.a, to_oklab.a, delta),
401
0
        interpolate_raw(from_oklab.b, to_oklab.b, delta));
402
0
    color.set_alpha(interpolate_raw(from.alpha(), to.alpha(), delta));
403
0
    return color;
404
0
}
405
406
NonnullRefPtr<CSSStyleValue const> interpolate_box_shadow(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta)
407
0
{
408
    // https://drafts.csswg.org/css-backgrounds/#box-shadow
409
    // Animation type: by computed value, treating none as a zero-item list and appending blank shadows
410
    //                 (transparent 0 0 0 0) with a corresponding inset keyword as needed to match the longer list if
411
    //                 the shorter list is otherwise compatible with the longer one
412
413
0
    static constexpr auto process_list = [](CSSStyleValue const& value) {
414
0
        StyleValueVector shadows;
415
0
        if (value.is_value_list()) {
416
0
            for (auto const& element : value.as_value_list().values()) {
417
0
                if (element->is_shadow())
418
0
                    shadows.append(element);
419
0
            }
420
0
        } else if (value.is_shadow()) {
421
0
            shadows.append(value);
422
0
        } else if (!value.is_keyword() || value.as_keyword().keyword() != Keyword::None) {
423
0
            VERIFY_NOT_REACHED();
424
0
        }
425
0
        return shadows;
426
0
    };
427
428
0
    static constexpr auto extend_list_if_necessary = [](StyleValueVector& values, StyleValueVector const& other) {
429
0
        values.ensure_capacity(other.size());
430
0
        for (size_t i = values.size(); i < other.size(); i++) {
431
0
            values.unchecked_append(ShadowStyleValue::create(
432
0
                CSSColorValue::create_from_color(Color::Transparent),
433
0
                LengthStyleValue::create(Length::make_px(0)),
434
0
                LengthStyleValue::create(Length::make_px(0)),
435
0
                LengthStyleValue::create(Length::make_px(0)),
436
0
                LengthStyleValue::create(Length::make_px(0)),
437
0
                other[i]->as_shadow().placement()));
438
0
        }
439
0
    };
440
441
0
    StyleValueVector from_shadows = process_list(from);
442
0
    StyleValueVector to_shadows = process_list(to);
443
444
0
    extend_list_if_necessary(from_shadows, to_shadows);
445
0
    extend_list_if_necessary(to_shadows, from_shadows);
446
447
0
    VERIFY(from_shadows.size() == to_shadows.size());
448
0
    StyleValueVector result_shadows;
449
0
    result_shadows.ensure_capacity(from_shadows.size());
450
451
0
    for (size_t i = 0; i < from_shadows.size(); i++) {
452
0
        auto const& from_shadow = from_shadows[i]->as_shadow();
453
0
        auto const& to_shadow = to_shadows[i]->as_shadow();
454
0
        auto result_shadow = ShadowStyleValue::create(
455
0
            CSSColorValue::create_from_color(interpolate_color(from_shadow.color()->to_color({}), to_shadow.color()->to_color({}), delta)),
456
0
            interpolate_value(element, from_shadow.offset_x(), to_shadow.offset_x(), delta),
457
0
            interpolate_value(element, from_shadow.offset_y(), to_shadow.offset_y(), delta),
458
0
            interpolate_value(element, from_shadow.blur_radius(), to_shadow.blur_radius(), delta),
459
0
            interpolate_value(element, from_shadow.spread_distance(), to_shadow.spread_distance(), delta),
460
0
            delta >= 0.5f ? to_shadow.placement() : from_shadow.placement());
461
0
        result_shadows.unchecked_append(result_shadow);
462
0
    }
463
464
0
    return StyleValueList::create(move(result_shadows), StyleValueList::Separator::Comma);
465
0
}
466
467
NonnullRefPtr<CSSStyleValue const> interpolate_value(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta)
468
0
{
469
0
    if (from.type() != to.type()) {
470
        // Handle mixed percentage and dimension types
471
        // https://www.w3.org/TR/css-values-4/#mixed-percentages
472
473
0
        struct NumericBaseTypeAndDefault {
474
0
            CSSNumericType::BaseType base_type;
475
0
            ValueComparingNonnullRefPtr<CSSStyleValue> default_value;
476
0
        };
477
0
        static constexpr auto numeric_base_type_and_default = [](CSSStyleValue const& value) -> Optional<NumericBaseTypeAndDefault> {
478
0
            switch (value.type()) {
479
0
            case CSSStyleValue::Type::Angle: {
480
0
                static auto default_angle_value = AngleStyleValue::create(Angle::make_degrees(0));
481
0
                return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Angle, default_angle_value };
482
0
            }
483
0
            case CSSStyleValue::Type::Frequency: {
484
0
                static auto default_frequency_value = FrequencyStyleValue::create(Frequency::make_hertz(0));
485
0
                return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Frequency, default_frequency_value };
486
0
            }
487
0
            case CSSStyleValue::Type::Length: {
488
0
                static auto default_length_value = LengthStyleValue::create(Length::make_px(0));
489
0
                return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Length, default_length_value };
490
0
            }
491
0
            case CSSStyleValue::Type::Percentage: {
492
0
                static auto default_percentage_value = PercentageStyleValue::create(Percentage { 0.0 });
493
0
                return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Percent, default_percentage_value };
494
0
            }
495
0
            case CSSStyleValue::Type::Time: {
496
0
                static auto default_time_value = TimeStyleValue::create(Time::make_seconds(0));
497
0
                return NumericBaseTypeAndDefault { CSSNumericType::BaseType::Time, default_time_value };
498
0
            }
499
0
            default:
500
0
                return {};
501
0
            }
502
0
        };
503
504
0
        static constexpr auto to_calculation_node = [](CSSStyleValue const& value) -> NonnullOwnPtr<CalculationNode> {
505
0
            switch (value.type()) {
506
0
            case CSSStyleValue::Type::Angle:
507
0
                return NumericCalculationNode::create(value.as_angle().angle());
508
0
            case CSSStyleValue::Type::Frequency:
509
0
                return NumericCalculationNode::create(value.as_frequency().frequency());
510
0
            case CSSStyleValue::Type::Length:
511
0
                return NumericCalculationNode::create(value.as_length().length());
512
0
            case CSSStyleValue::Type::Percentage:
513
0
                return NumericCalculationNode::create(value.as_percentage().percentage());
514
0
            case CSSStyleValue::Type::Time:
515
0
                return NumericCalculationNode::create(value.as_time().time());
516
0
            default:
517
0
                VERIFY_NOT_REACHED();
518
0
            }
519
0
        };
520
521
0
        auto from_base_type_and_default = numeric_base_type_and_default(from);
522
0
        auto to_base_type_and_default = numeric_base_type_and_default(to);
523
524
0
        if (from_base_type_and_default.has_value() && to_base_type_and_default.has_value() && (from_base_type_and_default->base_type == CSSNumericType::BaseType::Percent || to_base_type_and_default->base_type == CSSNumericType::BaseType::Percent)) {
525
            // This is an interpolation from a numeric unit to a percentage, or vice versa. The trick here is to
526
            // interpolate two separate values. For example, consider an interpolation from 30px to 80%. It's quite
527
            // hard to understand how this interpolation works, but if instead we rewrite the values as "30px + 0%" and
528
            // "0px + 80%", then it is very simple to understand; we just interpolate each component separately.
529
530
0
            auto interpolated_from = interpolate_value(element, from, from_base_type_and_default->default_value, delta);
531
0
            auto interpolated_to = interpolate_value(element, to_base_type_and_default->default_value, to, delta);
532
533
0
            Vector<NonnullOwnPtr<CalculationNode>> values;
534
0
            values.ensure_capacity(2);
535
0
            values.unchecked_append(to_calculation_node(interpolated_from));
536
0
            values.unchecked_append(to_calculation_node(interpolated_to));
537
0
            auto calc_node = SumCalculationNode::create(move(values));
538
0
            return CSSMathValue::create(move(calc_node), CSSNumericType { to_base_type_and_default->base_type, 1 });
539
0
        }
540
541
0
        return delta >= 0.5f ? to : from;
542
0
    }
543
544
0
    switch (from.type()) {
545
0
    case CSSStyleValue::Type::Angle:
546
0
        return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta)));
547
0
    case CSSStyleValue::Type::Color: {
548
0
        Optional<Layout::NodeWithStyle const&> layout_node;
549
0
        if (auto node = element.layout_node())
550
0
            layout_node = *node;
551
0
        return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta));
552
0
    }
553
0
    case CSSStyleValue::Type::Integer:
554
0
        return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta));
555
0
    case CSSStyleValue::Type::Length: {
556
0
        auto& from_length = from.as_length().length();
557
0
        auto& to_length = to.as_length().length();
558
0
        return LengthStyleValue::create(Length(interpolate_raw(from_length.raw_value(), to_length.raw_value(), delta), from_length.type()));
559
0
    }
560
0
    case CSSStyleValue::Type::Number:
561
0
        return NumberStyleValue::create(interpolate_raw(from.as_number().number(), to.as_number().number(), delta));
562
0
    case CSSStyleValue::Type::Percentage:
563
0
        return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value(), delta)));
564
0
    case CSSStyleValue::Type::Position: {
565
        // https://www.w3.org/TR/css-values-4/#combine-positions
566
        // 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>.
567
0
        auto& from_position = from.as_position();
568
0
        auto& to_position = to.as_position();
569
0
        return PositionStyleValue::create(
570
0
            interpolate_value(element, from_position.edge_x(), to_position.edge_x(), delta)->as_edge(),
571
0
            interpolate_value(element, from_position.edge_y(), to_position.edge_y(), delta)->as_edge());
572
0
    }
573
0
    case CSSStyleValue::Type::Ratio: {
574
0
        auto from_ratio = from.as_ratio().ratio();
575
0
        auto to_ratio = to.as_ratio().ratio();
576
577
        // The interpolation of a <ratio> is defined by converting each <ratio> to a number by dividing the first value
578
        // by the second (so a ratio of 3 / 2 would become 1.5), taking the logarithm of that result (so the 1.5 would
579
        // become approximately 0.176), then interpolating those values. The result during the interpolation is
580
        // converted back to a <ratio> by inverting the logarithm, then interpreting the result as a <ratio> with the
581
        // result as the first value and 1 as the second value.
582
0
        auto from_number = log(from_ratio.value());
583
0
        auto to_number = log(to_ratio.value());
584
0
        auto interp_number = interpolate_raw(from_number, to_number, delta);
585
0
        return RatioStyleValue::create(Ratio(pow(M_E, interp_number)));
586
0
    }
587
0
    case CSSStyleValue::Type::Rect: {
588
0
        auto from_rect = from.as_rect().rect();
589
0
        auto to_rect = to.as_rect().rect();
590
0
        return RectStyleValue::create({
591
0
            Length(interpolate_raw(from_rect.top_edge.raw_value(), to_rect.top_edge.raw_value(), delta), from_rect.top_edge.type()),
592
0
            Length(interpolate_raw(from_rect.right_edge.raw_value(), to_rect.right_edge.raw_value(), delta), from_rect.right_edge.type()),
593
0
            Length(interpolate_raw(from_rect.bottom_edge.raw_value(), to_rect.bottom_edge.raw_value(), delta), from_rect.bottom_edge.type()),
594
0
            Length(interpolate_raw(from_rect.left_edge.raw_value(), to_rect.left_edge.raw_value(), delta), from_rect.left_edge.type()),
595
0
        });
596
0
    }
597
0
    case CSSStyleValue::Type::Transformation:
598
0
        VERIFY_NOT_REACHED();
599
0
    case CSSStyleValue::Type::ValueList: {
600
0
        auto& from_list = from.as_value_list();
601
0
        auto& to_list = to.as_value_list();
602
0
        if (from_list.size() != to_list.size())
603
0
            return from;
604
605
0
        StyleValueVector interpolated_values;
606
0
        interpolated_values.ensure_capacity(from_list.size());
607
0
        for (size_t i = 0; i < from_list.size(); ++i)
608
0
            interpolated_values.append(interpolate_value(element, from_list.values()[i], to_list.values()[i], delta));
609
610
0
        return StyleValueList::create(move(interpolated_values), from_list.separator());
611
0
    }
612
0
    default:
613
0
        return from;
614
0
    }
615
0
}
616
617
}