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/StyleValues/EasingStyleValue.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
4
 * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
5
 * Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
6
 * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
7
 *
8
 * SPDX-License-Identifier: BSD-2-Clause
9
 */
10
11
#include "EasingStyleValue.h"
12
#include <AK/BinarySearch.h>
13
#include <AK/StringBuilder.h>
14
15
namespace Web::CSS {
16
17
// NOTE: Magic cubic bezier values from https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease
18
19
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease()
20
0
{
21
0
    static CubicBezier bezier { 0.25, 0.1, 0.25, 1.0 };
22
0
    return bezier;
23
0
}
24
25
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in()
26
0
{
27
0
    static CubicBezier bezier { 0.42, 0.0, 1.0, 1.0 };
28
0
    return bezier;
29
0
}
30
31
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_out()
32
0
{
33
0
    static CubicBezier bezier { 0.0, 0.0, 0.58, 1.0 };
34
0
    return bezier;
35
0
}
36
37
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in_out()
38
0
{
39
0
    static CubicBezier bezier { 0.42, 0.0, 0.58, 1.0 };
40
0
    return bezier;
41
0
}
42
43
EasingStyleValue::Steps EasingStyleValue::Steps::step_start()
44
0
{
45
0
    static Steps steps { 1, Steps::Position::Start };
46
0
    return steps;
47
0
}
48
49
EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
50
0
{
51
0
    static Steps steps { 1, Steps::Position::End };
52
0
    return steps;
53
0
}
54
55
bool EasingStyleValue::CubicBezier::operator==(Web::CSS::EasingStyleValue::CubicBezier const& other) const
56
0
{
57
0
    return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
58
0
}
59
60
double EasingStyleValue::Function::evaluate_at(double input_progress, bool before_flag) const
61
0
{
62
0
    constexpr static auto cubic_bezier_at = [](double x1, double x2, double t) {
63
0
        auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
64
0
        auto b = 3.0 * x2 - 6.0 * x1;
65
0
        auto c = 3.0 * x1;
66
67
0
        auto t2 = t * t;
68
0
        auto t3 = t2 * t;
69
70
0
        return (a * t3) + (b * t2) + (c * t);
71
0
    };
72
73
0
    return visit(
74
0
        [&](Linear const&) { return input_progress; },
75
0
        [&](CubicBezier const& bezier) {
76
0
            auto const& [x1, y1, x2, y2, cached_x_samples] = bezier;
77
78
            // https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
79
            // For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
80
            // at the closest endpoint as follows:
81
82
            // - For input progress values less than zero,
83
0
            if (input_progress < 0.0) {
84
                // 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
85
                //    tangent.
86
0
                if (x1 > 0.0)
87
0
                    return y1 / x1 * input_progress;
88
89
                // 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
90
                //    the tangent.
91
0
                if (x2 > 0.0)
92
0
                    return y2 / x2 * input_progress;
93
94
                // 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
95
0
                return 0.0;
96
0
            }
97
98
            // - For input progress values greater than one,
99
0
            if (input_progress > 1.0) {
100
                // 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
101
0
                if (x2 < 1.0)
102
0
                    return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
103
104
                // 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
105
                //    tangent.
106
0
                if (x1 < 1.0)
107
0
                    return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
108
109
                // 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
110
0
                return 1.0;
111
0
            }
112
113
            // Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
114
            //       "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
115
116
0
            auto x = input_progress;
117
118
0
            auto solve = [&](auto t) {
119
0
                auto x = cubic_bezier_at(bezier.x1, bezier.x2, t);
120
0
                auto y = cubic_bezier_at(bezier.y1, bezier.y2, t);
121
0
                return CubicBezier::CachedSample { x, y, t };
122
0
            };
123
124
0
            if (cached_x_samples.is_empty())
125
0
                cached_x_samples.append(solve(0.));
126
127
0
            size_t nearby_index = 0;
128
0
            if (auto found = binary_search(cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
129
0
                    if (x > sample.x)
130
0
                        return 1;
131
0
                    if (x < sample.x)
132
0
                        return -1;
133
0
                    return 0;
134
0
                }))
135
0
                return found->y;
136
137
0
            if (nearby_index == cached_x_samples.size() || nearby_index + 1 == cached_x_samples.size()) {
138
                // Produce more samples until we have enough.
139
0
                auto last_t = cached_x_samples.last().t;
140
0
                auto last_x = cached_x_samples.last().x;
141
0
                while (last_x <= x && last_t < 1.0) {
142
0
                    last_t += 1. / 60.;
143
0
                    auto solution = solve(last_t);
144
0
                    cached_x_samples.append(solution);
145
0
                    last_x = solution.x;
146
0
                }
147
148
0
                if (auto found = binary_search(cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
149
0
                        if (x > sample.x)
150
0
                            return 1;
151
0
                        if (x < sample.x)
152
0
                            return -1;
153
0
                        return 0;
154
0
                    }))
155
0
                    return found->y;
156
0
            }
157
158
            // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
159
0
            VERIFY(nearby_index > 0);
160
0
            auto& sample1 = cached_x_samples[nearby_index - 1];
161
0
            auto& sample2 = cached_x_samples[nearby_index];
162
0
            auto factor = (x - sample1.x) / (sample2.x - sample1.x);
163
0
            return sample1.y + factor * (sample2.y - sample1.y);
164
0
        },
165
0
        [&](Steps const& steps) {
166
            // https://www.w3.org/TR/css-easing-1/#step-easing-algo
167
            // 1. Calculate the current step as floor(input progress value × steps).
168
0
            auto [number_of_steps, position] = steps;
169
0
            auto current_step = floor(input_progress * number_of_steps);
170
171
            // 2. If the step position property is one of:
172
            //    - jump-start,
173
            //    - jump-both,
174
            //    increment current step by one.
175
0
            if (position == Steps::Position::JumpStart || position == Steps::Position::JumpBoth)
176
0
                current_step += 1;
177
178
            // 3. If both of the following conditions are true:
179
            //    - the before flag is set, and
180
            //    - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
181
            //    decrement current step by one.
182
0
            auto step_progress = input_progress * number_of_steps;
183
0
            if (before_flag && trunc(step_progress) == step_progress)
184
0
                current_step -= 1;
185
186
            // 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
187
0
            if (input_progress >= 0.0 && current_step < 0.0)
188
0
                current_step = 0.0;
189
190
            // 5. Calculate jumps based on the step position as follows:
191
192
            //    jump-start or jump-end -> steps
193
            //    jump-none -> steps - 1
194
            //    jump-both -> steps + 1
195
0
            auto jumps = steps.number_of_intervals;
196
0
            if (position == Steps::Position::JumpNone) {
197
0
                jumps--;
198
0
            } else if (position == Steps::Position::JumpBoth) {
199
0
                jumps++;
200
0
            }
201
202
            // 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
203
0
            if (input_progress <= 1.0 && current_step > jumps)
204
0
                current_step = jumps;
205
206
            // 7. The output progress value is current step / jumps.
207
0
            return current_step / jumps;
208
0
        });
209
0
}
210
211
String EasingStyleValue::Function::to_string() const
212
0
{
213
0
    StringBuilder builder;
214
0
    visit(
215
0
        [&](Linear const& linear) {
216
0
            builder.append("linear"sv);
217
0
            if (!linear.stops.is_empty()) {
218
0
                builder.append('(');
219
220
0
                bool first = true;
221
0
                for (auto const& stop : linear.stops) {
222
0
                    if (!first)
223
0
                        builder.append(", "sv);
224
0
                    first = false;
225
0
                    builder.appendff("{}"sv, stop.offset);
226
0
                    if (stop.position.has_value())
227
0
                        builder.appendff(" {}"sv, stop.position.value());
228
0
                }
229
230
0
                builder.append(')');
231
0
            }
232
0
        },
233
0
        [&](CubicBezier const& bezier) {
234
0
            if (bezier == CubicBezier::ease()) {
235
0
                builder.append("ease"sv);
236
0
            } else if (bezier == CubicBezier::ease_in()) {
237
0
                builder.append("ease-in"sv);
238
0
            } else if (bezier == CubicBezier::ease_out()) {
239
0
                builder.append("ease-out"sv);
240
0
            } else if (bezier == CubicBezier::ease_in_out()) {
241
0
                builder.append("ease-in-out"sv);
242
0
            } else {
243
0
                builder.appendff("cubic-bezier({}, {}, {}, {})", bezier.x1, bezier.y1, bezier.x2, bezier.y2);
244
0
            }
245
0
        },
246
0
        [&](Steps const& steps) {
247
0
            if (steps == Steps::step_start()) {
248
0
                builder.append("step-start"sv);
249
0
            } else if (steps == Steps::step_end()) {
250
0
                builder.append("step-end"sv);
251
0
            } else {
252
0
                auto position = [&] -> Optional<StringView> {
253
0
                    switch (steps.position) {
254
0
                    case Steps::Position::JumpStart:
255
0
                        return "jump-start"sv;
256
0
                    case Steps::Position::JumpNone:
257
0
                        return "jump-none"sv;
258
0
                    case Steps::Position::JumpBoth:
259
0
                        return "jump-both"sv;
260
0
                    case Steps::Position::Start:
261
0
                        return "start"sv;
262
0
                    default:
263
0
                        return {};
264
0
                    }
265
0
                }();
266
0
                if (position.has_value()) {
267
0
                    builder.appendff("steps({}, {})", steps.number_of_intervals, position.value());
268
0
                } else {
269
0
                    builder.appendff("steps({})", steps.number_of_intervals);
270
0
                }
271
0
            }
272
0
        });
273
0
    return MUST(builder.to_string());
274
0
}
275
276
}