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