/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 | | } |