/src/serenity/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023-2024, Matthew Olsson <mattco@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/QuickSort.h> |
8 | | #include <LibJS/Runtime/Iterator.h> |
9 | | #include <LibWeb/Animations/Animation.h> |
10 | | #include <LibWeb/Animations/KeyframeEffect.h> |
11 | | #include <LibWeb/Bindings/KeyframeEffectPrototype.h> |
12 | | #include <LibWeb/CSS/Parser/Parser.h> |
13 | | #include <LibWeb/CSS/StyleComputer.h> |
14 | | #include <LibWeb/Layout/Node.h> |
15 | | #include <LibWeb/Painting/Paintable.h> |
16 | | #include <LibWeb/WebIDL/ExceptionOr.h> |
17 | | |
18 | | namespace Web::Animations { |
19 | | |
20 | | JS_DEFINE_ALLOCATOR(KeyframeEffect); |
21 | | |
22 | | template<typename T> |
23 | | WebIDL::ExceptionOr<Variant<T, Vector<T>>> convert_value_to_maybe_list(JS::Realm& realm, JS::Value value, Function<WebIDL::ExceptionOr<T>(JS::Value)>& value_converter) |
24 | 0 | { |
25 | 0 | auto& vm = realm.vm(); |
26 | |
|
27 | 0 | if (TRY(value.is_array(vm))) { |
28 | 0 | Vector<T> offsets; |
29 | |
|
30 | 0 | auto iterator = TRY(JS::get_iterator(vm, value, JS::IteratorHint::Sync)); |
31 | 0 | auto values = TRY(JS::iterator_to_list(vm, iterator)); |
32 | 0 | for (auto const& element : values) { |
33 | 0 | if (element.is_undefined()) { |
34 | 0 | offsets.append({}); |
35 | 0 | } else { |
36 | 0 | offsets.append(TRY(value_converter(element))); |
37 | 0 | } |
38 | 0 | } |
39 | |
|
40 | 0 | return offsets; |
41 | 0 | } |
42 | | |
43 | 0 | return TRY(value_converter(value)); |
44 | 0 | } Unexecuted instantiation: Web::WebIDL::ExceptionOr<AK::Variant<Web::Bindings::CompositeOperationOrAuto, AK::Vector<Web::Bindings::CompositeOperationOrAuto, 0ul> > > Web::Animations::convert_value_to_maybe_list<Web::Bindings::CompositeOperationOrAuto>(JS::Realm&, JS::Value, AK::Function<Web::WebIDL::ExceptionOr<Web::Bindings::CompositeOperationOrAuto> (JS::Value)>&) Unexecuted instantiation: Web::WebIDL::ExceptionOr<AK::Variant<AK::String, AK::Vector<AK::String, 0ul> > > Web::Animations::convert_value_to_maybe_list<AK::String>(JS::Realm&, JS::Value, AK::Function<Web::WebIDL::ExceptionOr<AK::String> (JS::Value)>&) Unexecuted instantiation: Web::WebIDL::ExceptionOr<AK::Variant<AK::Optional<double>, AK::Vector<AK::Optional<double>, 0ul> > > Web::Animations::convert_value_to_maybe_list<AK::Optional<double> >(JS::Realm&, JS::Value, AK::Function<Web::WebIDL::ExceptionOr<AK::Optional<double> > (JS::Value)>&) |
45 | | |
46 | | enum class AllowLists { |
47 | | Yes, |
48 | | No, |
49 | | }; |
50 | | |
51 | | template<AllowLists AL> |
52 | | using KeyframeType = Conditional<AL == AllowLists::Yes, BasePropertyIndexedKeyframe, BaseKeyframe>; |
53 | | |
54 | | // https://www.w3.org/TR/web-animations-1/#process-a-keyframe-like-object |
55 | | template<AllowLists AL> |
56 | | static WebIDL::ExceptionOr<KeyframeType<AL>> process_a_keyframe_like_object(JS::Realm& realm, JS::Value keyframe_input) |
57 | 0 | { |
58 | 0 | auto& vm = realm.vm(); |
59 | |
|
60 | 0 | Function<WebIDL::ExceptionOr<Optional<double>>(JS::Value)> to_offset = [&vm](JS::Value value) -> WebIDL::ExceptionOr<Optional<double>> { |
61 | 0 | if (value.is_undefined()) |
62 | 0 | return Optional<double> {}; |
63 | 0 | auto double_value = TRY(value.to_double(vm)); |
64 | 0 | if (isnan(double_value) || isinf(double_value)) |
65 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid offset value: {}", TRY(value.to_string(vm)))) }; |
66 | 0 | return double_value; |
67 | 0 | }; Unexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)1>(JS::Realm&, JS::Value)::{lambda(JS::Value)#1}::operator()(JS::Value) constUnexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)0>(JS::Realm&, JS::Value)::{lambda(JS::Value)#1}::operator()(JS::Value) const |
68 | |
|
69 | 0 | Function<WebIDL::ExceptionOr<String>(JS::Value)> to_string = [&vm](JS::Value value) -> WebIDL::ExceptionOr<String> { |
70 | 0 | return TRY(value.to_string(vm)); |
71 | 0 | }; Unexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)1>(JS::Realm&, JS::Value)::{lambda(JS::Value)#2}::operator()(JS::Value) constUnexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)0>(JS::Realm&, JS::Value)::{lambda(JS::Value)#2}::operator()(JS::Value) const |
72 | |
|
73 | 0 | Function<WebIDL::ExceptionOr<Bindings::CompositeOperationOrAuto>(JS::Value)> to_composite_operation = [&vm](JS::Value value) -> WebIDL::ExceptionOr<Bindings::CompositeOperationOrAuto> { |
74 | 0 | if (value.is_undefined()) |
75 | 0 | return Bindings::CompositeOperationOrAuto::Auto; |
76 | | |
77 | 0 | auto string_value = TRY(value.to_string(vm)); |
78 | 0 | if (string_value == "replace") |
79 | 0 | return Bindings::CompositeOperationOrAuto::Replace; |
80 | 0 | if (string_value == "add") |
81 | 0 | return Bindings::CompositeOperationOrAuto::Add; |
82 | 0 | if (string_value == "accumulate") |
83 | 0 | return Bindings::CompositeOperationOrAuto::Accumulate; |
84 | 0 | if (string_value == "auto") |
85 | 0 | return Bindings::CompositeOperationOrAuto::Auto; |
86 | | |
87 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid composite value"sv }; |
88 | 0 | }; Unexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)1>(JS::Realm&, JS::Value)::{lambda(JS::Value)#3}::operator()(JS::Value) constUnexecuted instantiation: KeyframeEffect.cpp:Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)0>(JS::Realm&, JS::Value)::{lambda(JS::Value)#3}::operator()(JS::Value) const |
89 | | |
90 | | // 1. Run the procedure to convert an ECMAScript value to a dictionary type with keyframe input as the ECMAScript |
91 | | // value, and the dictionary type depending on the value of the allow lists flag as follows: |
92 | | // |
93 | | // -> If allow lists is true, use the following dictionary type: <BasePropertyIndexedKeyframe>. |
94 | | // -> Otherwise, use the following dictionary type: <BaseKeyframe>. |
95 | | // |
96 | | // Store the result of this procedure as keyframe output. |
97 | |
|
98 | 0 | KeyframeType<AL> keyframe_output; |
99 | 0 | if (keyframe_input.is_nullish()) |
100 | 0 | return keyframe_output; |
101 | | |
102 | 0 | auto& keyframe_object = keyframe_input.as_object(); |
103 | 0 | auto composite = TRY(keyframe_object.get("composite")); |
104 | 0 | if (composite.is_undefined()) |
105 | 0 | composite = JS::PrimitiveString::create(vm, "auto"_string); |
106 | 0 | auto easing = TRY(keyframe_object.get("easing")); |
107 | 0 | if (easing.is_undefined()) |
108 | 0 | easing = JS::PrimitiveString::create(vm, "linear"_string); |
109 | 0 | auto offset = TRY(keyframe_object.get("offset")); |
110 | |
|
111 | 0 | if constexpr (AL == AllowLists::Yes) { |
112 | 0 | keyframe_output.composite = TRY(convert_value_to_maybe_list(realm, composite, to_composite_operation)); |
113 | |
|
114 | 0 | auto easing_maybe_list = TRY(convert_value_to_maybe_list(realm, easing, to_string)); |
115 | 0 | easing_maybe_list.visit( |
116 | 0 | [&](String const& value) { |
117 | 0 | keyframe_output.easing = EasingValue { value }; |
118 | 0 | }, |
119 | 0 | [&](Vector<String> const& values) { |
120 | 0 | Vector<EasingValue> easing_values; |
121 | 0 | for (auto& easing_value : values) |
122 | 0 | easing_values.append(easing_value); |
123 | 0 | keyframe_output.easing = move(easing_values); |
124 | 0 | }); |
125 | |
|
126 | 0 | keyframe_output.offset = TRY(convert_value_to_maybe_list(realm, offset, to_offset)); |
127 | 0 | } else { |
128 | 0 | keyframe_output.composite = TRY(to_composite_operation(composite)); |
129 | 0 | keyframe_output.easing = TRY(to_string(easing)); |
130 | 0 | keyframe_output.offset = TRY(to_offset(offset)); |
131 | 0 | } |
132 | | |
133 | | // 2. Build up a list of animatable properties as follows: |
134 | | // |
135 | | // 1. Let animatable properties be a list of property names (including shorthand properties that have longhand |
136 | | // sub-properties that are animatable) that can be animated by the implementation. |
137 | | // 2. Convert each property name in animatable properties to the equivalent IDL attribute by applying the |
138 | | // animation property name to IDL attribute name algorithm. |
139 | | |
140 | | // 3. Let input properties be the result of calling the EnumerableOwnNames operation with keyframe input as the |
141 | | // object. |
142 | | |
143 | | // 4. Make up a new list animation properties that consists of all of the properties that are in both input |
144 | | // properties and animatable properties, or which are in input properties and conform to the |
145 | | // <custom-property-name> production. |
146 | 0 | auto input_properties = TRY(keyframe_object.enumerable_own_property_names(JS::Object::PropertyKind::Key)); |
147 | |
|
148 | 0 | Vector<String> animation_properties; |
149 | 0 | Optional<JS::Value> all_value; |
150 | |
|
151 | 0 | for (auto const& input_property : input_properties) { |
152 | 0 | if (!input_property.is_string()) |
153 | 0 | continue; |
154 | | |
155 | 0 | auto name = input_property.as_string().utf8_string(); |
156 | 0 | if (name == "all"sv) { |
157 | 0 | all_value = TRY(keyframe_object.get(JS::PropertyKey { name })); |
158 | 0 | for (auto i = to_underlying(CSS::first_longhand_property_id); i <= to_underlying(CSS::last_longhand_property_id); ++i) { |
159 | 0 | auto property = static_cast<CSS::PropertyID>(i); |
160 | 0 | if (CSS::is_animatable_property(property)) |
161 | 0 | animation_properties.append(String { CSS::string_from_property_id(property) }); |
162 | 0 | } |
163 | 0 | } else { |
164 | | // Handle the two special cases |
165 | 0 | if (name == "cssFloat"sv || name == "cssOffset"sv) { |
166 | 0 | animation_properties.append(name); |
167 | 0 | } else if (name == "float"sv || name == "offset"sv) { |
168 | | // Ignore these property names |
169 | 0 | } else if (auto property = CSS::property_id_from_camel_case_string(name); property.has_value()) { |
170 | 0 | if (CSS::is_animatable_property(property.value())) |
171 | 0 | animation_properties.append(name); |
172 | 0 | } |
173 | 0 | } |
174 | 0 | } |
175 | | |
176 | | // 5. Sort animation properties in ascending order by the Unicode codepoints that define each property name. |
177 | 0 | quick_sort(animation_properties); |
178 | | |
179 | | // 6. For each property name in animation properties, |
180 | 0 | for (auto const& property_name : animation_properties) { |
181 | | // 1. Let raw value be the result of calling the [[Get]] internal method on keyframe input, with property name |
182 | | // as the property key and keyframe input as the receiver. |
183 | | // 2. Check the completion record of raw value. |
184 | 0 | JS::PropertyKey key { property_name }; |
185 | 0 | auto raw_value = TRY(keyframe_object.has_property(key)) ? TRY(keyframe_object.get(key)) : *all_value; |
186 | |
|
187 | 0 | using PropertyValuesType = Conditional<AL == AllowLists::Yes, Vector<String>, String>; |
188 | 0 | PropertyValuesType property_values; |
189 | | |
190 | | // 3. Convert raw value to a DOMString or sequence of DOMStrings property values as follows: |
191 | | |
192 | | // -> If allow lists is true, |
193 | 0 | if constexpr (AL == AllowLists::Yes) { |
194 | | // Let property values be the result of converting raw value to IDL type (DOMString or sequence<DOMString>) |
195 | | // using the procedures defined for converting an ECMAScript value to an IDL value [WEBIDL]. |
196 | 0 | auto intermediate_property_values = TRY(convert_value_to_maybe_list(realm, raw_value, to_string)); |
197 | | |
198 | | // If property values is a single DOMString, replace property values with a sequence of DOMStrings with the |
199 | | // original value of property values as the only element. |
200 | 0 | if (intermediate_property_values.has<String>()) |
201 | 0 | property_values = Vector { intermediate_property_values.get<String>() }; |
202 | 0 | else |
203 | 0 | property_values = intermediate_property_values.get<Vector<String>>(); |
204 | | } |
205 | | // -> Otherwise, |
206 | 0 | else { |
207 | | // Let property values be the result of converting raw value to a DOMString using the procedure for |
208 | | // converting an ECMAScript value to a DOMString [WEBIDL]. |
209 | 0 | property_values = TRY(raw_value.to_string(vm)); |
210 | 0 | } |
211 | | |
212 | | // 4. Calculate the normalized property name as the result of applying the IDL attribute name to animation |
213 | | // property name algorithm to property name. |
214 | | // Note: We do not need to do this, since we did not need to do the reverse step (animation property name to IDL |
215 | | // attribute name) in the steps above. |
216 | | |
217 | | // 5. Add a property to keyframe output with normalized property name as the property name, and property values |
218 | | // as the property value. |
219 | 0 | if constexpr (AL == AllowLists::Yes) { |
220 | 0 | keyframe_output.properties.set(property_name, property_values); |
221 | 0 | } else { |
222 | 0 | keyframe_output.unparsed_properties().set(property_name, property_values); |
223 | 0 | } |
224 | 0 | } |
225 | |
|
226 | 0 | return keyframe_output; |
227 | 0 | } Unexecuted instantiation: KeyframeEffect.cpp:Web::WebIDL::ExceptionOr<AK::Detail::__Conditional<((Web::Animations::AllowLists)1)==((Web::Animations::AllowLists)0), Web::Animations::BasePropertyIndexedKeyframe, Web::Animations::BaseKeyframe>::Type> Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)1>(JS::Realm&, JS::Value) Unexecuted instantiation: KeyframeEffect.cpp:Web::WebIDL::ExceptionOr<AK::Detail::__Conditional<((Web::Animations::AllowLists)0)==((Web::Animations::AllowLists)0), Web::Animations::BasePropertyIndexedKeyframe, Web::Animations::BaseKeyframe>::Type> Web::Animations::process_a_keyframe_like_object<(Web::Animations::AllowLists)0>(JS::Realm&, JS::Value) |
228 | | |
229 | | // https://www.w3.org/TR/web-animations-1/#compute-missing-keyframe-offsets |
230 | | static void compute_missing_keyframe_offsets(Vector<BaseKeyframe>& keyframes) |
231 | 0 | { |
232 | | // 1. For each keyframe, in keyframes, let the computed keyframe offset of the keyframe be equal to its keyframe |
233 | | // offset value. |
234 | 0 | for (auto& keyframe : keyframes) |
235 | 0 | keyframe.computed_offset = keyframe.offset; |
236 | | |
237 | | // 2. If keyframes contains more than one keyframe and the computed keyframe offset of the first keyframe in |
238 | | // keyframes is null, set the computed keyframe offset of the first keyframe to 0. |
239 | 0 | if (keyframes.size() > 1 && !keyframes[0].computed_offset.has_value()) |
240 | 0 | keyframes[0].computed_offset = 0.0; |
241 | | |
242 | | // 3. If the computed keyframe offset of the last keyframe in keyframes is null, set its computed keyframe offset |
243 | | // to 1. |
244 | 0 | if (!keyframes.is_empty() && !keyframes.last().computed_offset.has_value()) |
245 | 0 | keyframes.last().computed_offset = 1.0; |
246 | | |
247 | | // 4. For each pair of keyframes A and B where: |
248 | | // - A appears before B in keyframes, and |
249 | | // - A and B have a computed keyframe offset that is not null, and |
250 | | // - all keyframes between A and B have a null computed keyframe offset, |
251 | 0 | auto find_next_index_of_keyframe_with_computed_offset = [&](size_t starting_index) -> Optional<size_t> { |
252 | 0 | for (size_t index = starting_index; index < keyframes.size(); index++) { |
253 | 0 | if (keyframes[index].computed_offset.has_value()) |
254 | 0 | return index; |
255 | 0 | } |
256 | | |
257 | 0 | return {}; |
258 | 0 | }; |
259 | |
|
260 | 0 | auto maybe_index_a = find_next_index_of_keyframe_with_computed_offset(0); |
261 | 0 | if (!maybe_index_a.has_value()) |
262 | 0 | return; |
263 | | |
264 | 0 | auto index_a = maybe_index_a.value(); |
265 | 0 | auto maybe_index_b = find_next_index_of_keyframe_with_computed_offset(index_a + 1); |
266 | |
|
267 | 0 | while (maybe_index_b.has_value()) { |
268 | 0 | auto index_b = maybe_index_b.value(); |
269 | | |
270 | | // calculate the computed keyframe offset of each keyframe between A and B as follows: |
271 | 0 | for (size_t keyframe_index = index_a + 1; keyframe_index < index_b; keyframe_index++) { |
272 | | // 1. Let offsetk be the computed keyframe offset of a keyframe k. |
273 | 0 | auto offset_a = keyframes[index_a].computed_offset.value(); |
274 | 0 | auto offset_b = keyframes[index_b].computed_offset.value(); |
275 | | |
276 | | // 2. Let n be the number of keyframes between and including A and B minus 1. |
277 | 0 | auto n = static_cast<double>(index_b - index_a); |
278 | | |
279 | | // 3. Let index refer to the position of keyframe in the sequence of keyframes between A and B such that the |
280 | | // first keyframe after A has an index of 1. |
281 | 0 | auto index = static_cast<double>(keyframe_index - index_a); |
282 | | |
283 | | // 4. Set the computed keyframe offset of keyframe to offsetA + (offsetB − offsetA) × index / n. |
284 | 0 | keyframes[keyframe_index].computed_offset = (offset_a + (offset_b - offset_a)) * index / n; |
285 | 0 | } |
286 | |
|
287 | 0 | index_a = index_b; |
288 | 0 | maybe_index_b = find_next_index_of_keyframe_with_computed_offset(index_b + 1); |
289 | 0 | } |
290 | 0 | } |
291 | | |
292 | | // https://www.w3.org/TR/web-animations-1/#loosely-sorted-by-offset |
293 | | static bool is_loosely_sorted_by_offset(Vector<BaseKeyframe> const& keyframes) |
294 | 0 | { |
295 | | // The list of keyframes for a keyframe effect must be loosely sorted by offset which means that for each keyframe |
296 | | // in the list that has a keyframe offset that is not null, the offset is greater than or equal to the offset of the |
297 | | // previous keyframe in the list with a keyframe offset that is not null, if any. |
298 | |
|
299 | 0 | Optional<double> last_offset; |
300 | 0 | for (auto const& keyframe : keyframes) { |
301 | 0 | if (!keyframe.offset.has_value()) |
302 | 0 | continue; |
303 | | |
304 | 0 | if (last_offset.has_value() && keyframe.offset.value() < last_offset.value()) |
305 | 0 | return false; |
306 | | |
307 | 0 | last_offset = keyframe.offset; |
308 | 0 | } |
309 | | |
310 | 0 | return true; |
311 | 0 | } |
312 | | |
313 | | // https://www.w3.org/TR/web-animations-1/#process-a-keyframes-argument |
314 | | static WebIDL::ExceptionOr<Vector<BaseKeyframe>> process_a_keyframes_argument(JS::Realm& realm, JS::GCPtr<JS::Object> object) |
315 | 0 | { |
316 | 0 | auto& vm = realm.vm(); |
317 | | |
318 | | // 1. If object is null, return an empty sequence of keyframes. |
319 | 0 | if (!object) |
320 | 0 | return Vector<BaseKeyframe> {}; |
321 | | |
322 | | // 2. Let processed keyframes be an empty sequence of keyframes. |
323 | 0 | Vector<BaseKeyframe> processed_keyframes; |
324 | 0 | Vector<EasingValue> unused_easings; |
325 | | |
326 | | // 3. Let method be the result of GetMethod(object, @@iterator). |
327 | | // 4. Check the completion record of method. |
328 | 0 | auto method = TRY(JS::Value(object).get_method(vm, vm.well_known_symbol_iterator())); |
329 | | |
330 | | // 5. Perform the steps corresponding to the first matching condition from below, |
331 | | |
332 | | // -> If method is not undefined, |
333 | 0 | if (method) { |
334 | | // 1. Let iter be GetIterator(object, method). |
335 | | // 2. Check the completion record of iter. |
336 | 0 | auto iter = TRY(JS::get_iterator_from_method(vm, object, *method)); |
337 | | |
338 | | // 3. Repeat: |
339 | 0 | while (true) { |
340 | | // 1. Let next be IteratorStep(iter). |
341 | | // 2. Check the completion record of next. |
342 | 0 | auto next = TRY(JS::iterator_step(vm, iter)); |
343 | | |
344 | | // 3. If next is false abort this loop. |
345 | 0 | if (!next) |
346 | 0 | break; |
347 | | |
348 | | // 4. Let nextItem be IteratorValue(next). |
349 | | // 5. Check the completion record of nextItem. |
350 | 0 | auto next_item = TRY(JS::iterator_value(vm, *next)); |
351 | | |
352 | | // 6. If Type(nextItem) is not Undefined, Null or Object, then throw a TypeError and abort these steps. |
353 | 0 | if (!next_item.is_nullish() && !next_item.is_object()) |
354 | 0 | return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOrNull, next_item.to_string_without_side_effects()); |
355 | | |
356 | | // 7. Append to processed keyframes the result of running the procedure to process a keyframe-like object |
357 | | // passing nextItem as the keyframe input and with the allow lists flag set to false. |
358 | 0 | processed_keyframes.append(TRY(process_a_keyframe_like_object<AllowLists::No>(realm, next_item))); |
359 | 0 | } |
360 | 0 | } |
361 | | // -> Otherwise, |
362 | 0 | else { |
363 | | // 1. Let property-indexed keyframe be the result of running the procedure to process a keyframe-like object |
364 | | // passing object as the keyframe input and with the allow lists flag set to true. |
365 | 0 | auto property_indexed_keyframe = TRY(process_a_keyframe_like_object<AllowLists::Yes>(realm, object)); |
366 | | |
367 | | // 2. For each member, m, in property-indexed keyframe, perform the following steps: |
368 | 0 | for (auto const& [property_name, property_values] : property_indexed_keyframe.properties) { |
369 | | // 1. Let property name be the key for m. |
370 | | |
371 | | // 2. If property name is "composite", or "easing", or "offset", skip the remaining steps in this loop and |
372 | | // continue from the next member in property-indexed keyframe after m. |
373 | | // Note: This will never happen, since these fields have dedicated members on BasePropertyIndexedKeyframe |
374 | | |
375 | | // 3. Let property values be the value for m. |
376 | | |
377 | | // 4. Let property keyframes be an empty sequence of keyframes. |
378 | 0 | Vector<BaseKeyframe> property_keyframes; |
379 | | |
380 | | // 5. For each value, v, in property values perform the following steps: |
381 | 0 | for (auto const& value : property_values) { |
382 | | // 1. Let k be a new keyframe with a null keyframe offset. |
383 | 0 | BaseKeyframe keyframe; |
384 | | |
385 | | // 2. Add the property-value pair, property name → v, to k. |
386 | 0 | keyframe.unparsed_properties().set(property_name, value); |
387 | | |
388 | | // 3. Append k to property keyframes. |
389 | 0 | property_keyframes.append(keyframe); |
390 | 0 | } |
391 | | |
392 | | // 6. Apply the procedure to compute missing keyframe offsets to property keyframes. |
393 | 0 | compute_missing_keyframe_offsets(property_keyframes); |
394 | | |
395 | | // 7. Add keyframes in property keyframes to processed keyframes. |
396 | 0 | processed_keyframes.extend(move(property_keyframes)); |
397 | 0 | } |
398 | | |
399 | | // 3. Sort processed keyframes by the computed keyframe offset of each keyframe in increasing order. |
400 | 0 | quick_sort(processed_keyframes, [](auto const& a, auto const& b) { |
401 | 0 | return a.computed_offset.value() < b.computed_offset.value(); |
402 | 0 | }); |
403 | | |
404 | | // 4. Merge adjacent keyframes in processed keyframes when they have equal computed keyframe offsets. |
405 | | // Note: The spec doesn't specify how to merge them, but WebKit seems to just override the properties of the |
406 | | // earlier keyframe with the properties of the later keyframe. |
407 | 0 | for (int i = 0; i < static_cast<int>(processed_keyframes.size() - 1); i++) { |
408 | 0 | auto& keyframe_a = processed_keyframes[i]; |
409 | 0 | auto& keyframe_b = processed_keyframes[i + 1]; |
410 | |
|
411 | 0 | if (keyframe_a.computed_offset.value() == keyframe_b.computed_offset.value()) { |
412 | 0 | keyframe_a.easing = keyframe_b.easing; |
413 | 0 | keyframe_a.composite = keyframe_b.composite; |
414 | 0 | for (auto const& [property_name, property_value] : keyframe_b.unparsed_properties()) |
415 | 0 | keyframe_a.unparsed_properties().set(property_name, property_value); |
416 | 0 | processed_keyframes.remove(i + 1); |
417 | 0 | i--; |
418 | 0 | } |
419 | 0 | } |
420 | | |
421 | | // 5. Let offsets be a sequence of nullable double values assigned based on the type of the "offset" member |
422 | | // of the property-indexed keyframe as follows: |
423 | | // |
424 | | // -> sequence<double?>, |
425 | | // The value of "offset" as-is. |
426 | | // -> double?, |
427 | | // A sequence of length one with the value of "offset" as its single item, i.e. « offset », |
428 | 0 | auto offsets = property_indexed_keyframe.offset.has<Optional<double>>() |
429 | 0 | ? Vector { property_indexed_keyframe.offset.get<Optional<double>>() } |
430 | 0 | : property_indexed_keyframe.offset.get<Vector<Optional<double>>>(); |
431 | | |
432 | | // 6. Assign each value in offsets to the keyframe offset of the keyframe with corresponding position in |
433 | | // processed keyframes until the end of either sequence is reached. |
434 | 0 | for (size_t i = 0; i < offsets.size() && i < processed_keyframes.size(); i++) |
435 | 0 | processed_keyframes[i].offset = offsets[i]; |
436 | | |
437 | | // 7. Let easings be a sequence of DOMString values assigned based on the type of the "easing" member of the |
438 | | // property-indexed keyframe as follows: |
439 | | // |
440 | | // -> sequence<DOMString>, |
441 | | // The value of "easing" as-is. |
442 | | // -> DOMString, |
443 | | // A sequence of length one with the value of "easing" as its single item, i.e. « easing », |
444 | 0 | auto easings = property_indexed_keyframe.easing.has<EasingValue>() |
445 | 0 | ? Vector { property_indexed_keyframe.easing.get<EasingValue>() } |
446 | 0 | : property_indexed_keyframe.easing.get<Vector<EasingValue>>(); |
447 | | |
448 | | // 8. If easings is an empty sequence, let it be a sequence of length one containing the single value "linear", |
449 | | // i.e. « "linear" ». |
450 | 0 | if (easings.is_empty()) |
451 | 0 | easings.append("linear"_string); |
452 | | |
453 | | // 9. If easings has fewer items than processed keyframes, repeat the elements in easings successively starting |
454 | | // from the beginning of the list until easings has as many items as processed keyframes. |
455 | | // |
456 | | // For example, if processed keyframes has five items, and easings is the sequence « "ease-in", "ease-out" », |
457 | | // easings would be repeated to become « "ease-in", "ease-out", "ease-in", "ease-out", "ease-in" ». |
458 | 0 | size_t num_easings = easings.size(); |
459 | 0 | size_t index = 0; |
460 | 0 | while (easings.size() < processed_keyframes.size()) |
461 | 0 | easings.append(easings[index++ % num_easings]); |
462 | | |
463 | | // 10. If easings has more items than processed keyframes, store the excess items as unused easings. |
464 | 0 | while (easings.size() > processed_keyframes.size()) |
465 | 0 | unused_easings.append(easings.take_last()); |
466 | | |
467 | | // 11. Assign each value in easings to a property named "easing" on the keyframe with the corresponding position |
468 | | // in processed keyframes until the end of processed keyframes is reached. |
469 | 0 | for (size_t i = 0; i < processed_keyframes.size(); i++) |
470 | 0 | processed_keyframes[i].easing = easings[i]; |
471 | | |
472 | | // 12. If the "composite" member of the property-indexed keyframe is not an empty sequence: |
473 | 0 | auto composite_value = property_indexed_keyframe.composite; |
474 | 0 | if (!composite_value.has<Vector<Bindings::CompositeOperationOrAuto>>() || !composite_value.get<Vector<Bindings::CompositeOperationOrAuto>>().is_empty()) { |
475 | | // 1. Let composite modes be a sequence of CompositeOperationOrAuto values assigned from the "composite" |
476 | | // member of property-indexed keyframe. If that member is a single CompositeOperationOrAuto value |
477 | | // operation, let composite modes be a sequence of length one, with the value of the "composite" as its |
478 | | // single item. |
479 | 0 | auto composite_modes = composite_value.has<Bindings::CompositeOperationOrAuto>() |
480 | 0 | ? Vector { composite_value.get<Bindings::CompositeOperationOrAuto>() } |
481 | 0 | : composite_value.get<Vector<Bindings::CompositeOperationOrAuto>>(); |
482 | | |
483 | | // 2. As with easings, if composite modes has fewer items than processed keyframes, repeat the elements in |
484 | | // composite modes successively starting from the beginning of the list until composite modes has as |
485 | | // many items as processed keyframes. |
486 | 0 | size_t num_composite_modes = composite_modes.size(); |
487 | 0 | index = 0; |
488 | 0 | while (composite_modes.size() < processed_keyframes.size()) |
489 | 0 | composite_modes.append(composite_modes[index++ % num_composite_modes]); |
490 | | |
491 | | // 3. Assign each value in composite modes that is not auto to the keyframe-specific composite operation on |
492 | | // the keyframe with the corresponding position in processed keyframes until the end of processed |
493 | | // keyframes is reached. |
494 | 0 | for (size_t i = 0; i < processed_keyframes.size(); i++) { |
495 | 0 | if (composite_modes[i] != Bindings::CompositeOperationOrAuto::Auto) |
496 | 0 | processed_keyframes[i].composite = composite_modes[i]; |
497 | 0 | } |
498 | 0 | } |
499 | 0 | } |
500 | | |
501 | | // 6. If processed keyframes is not loosely sorted by offset, throw a TypeError and abort these steps. |
502 | 0 | if (!is_loosely_sorted_by_offset(processed_keyframes)) |
503 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Keyframes are not in ascending order based on offset"sv }; |
504 | | |
505 | | // 7. If there exist any keyframe in processed keyframes whose keyframe offset is non-null and less than zero or |
506 | | // greater than one, throw a TypeError and abort these steps. |
507 | 0 | for (size_t i = 0; i < processed_keyframes.size(); i++) { |
508 | 0 | auto const& keyframe = processed_keyframes[i]; |
509 | 0 | if (!keyframe.offset.has_value()) |
510 | 0 | continue; |
511 | | |
512 | 0 | auto offset = keyframe.offset.value(); |
513 | 0 | if (offset < 0.0 || offset > 1.0) |
514 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Keyframe {} has invalid offset value {}"sv, i, offset)) }; |
515 | 0 | } |
516 | | |
517 | | // 8. For each frame in processed keyframes, perform the following steps: |
518 | 0 | for (auto& keyframe : processed_keyframes) { |
519 | | // 1. For each property-value pair in frame, parse the property value using the syntax specified for that |
520 | | // property. |
521 | | // |
522 | | // If the property value is invalid according to the syntax for the property, discard the property-value pair. |
523 | | // User agents that provide support for diagnosing errors in content SHOULD produce an appropriate warning |
524 | | // highlight |
525 | 0 | BaseKeyframe::ParsedProperties parsed_properties; |
526 | 0 | for (auto& [property_string, value_string] : keyframe.unparsed_properties()) { |
527 | 0 | Optional<CSS::PropertyID> property_id; |
528 | | |
529 | | // Handle some special cases |
530 | 0 | if (property_string == "cssFloat"sv) { |
531 | 0 | property_id = CSS::PropertyID::Float; |
532 | 0 | } else if (property_string == "cssOffset"sv) { |
533 | | // FIXME: Support CSS offset property |
534 | 0 | } else if (property_string == "float"sv || property_string == "offset"sv) { |
535 | | // Ignore these properties |
536 | 0 | } else if (auto property = CSS::property_id_from_camel_case_string(property_string); property.has_value()) { |
537 | 0 | property_id = *property; |
538 | 0 | } |
539 | |
|
540 | 0 | if (!property_id.has_value()) |
541 | 0 | continue; |
542 | | |
543 | 0 | auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), value_string); |
544 | |
|
545 | 0 | if (auto style_value = parser.parse_as_css_value(*property_id)) { |
546 | | // Handle 'initial' here so we don't have to get the default value of the property every frame in StyleComputer |
547 | 0 | if (style_value->is_initial()) |
548 | 0 | style_value = CSS::property_initial_value(realm, *property_id); |
549 | 0 | parsed_properties.set(*property_id, *style_value); |
550 | 0 | } |
551 | 0 | } |
552 | 0 | keyframe.properties.set(move(parsed_properties)); |
553 | | |
554 | | // 2. Let the timing function of frame be the result of parsing the "easing" property on frame using the CSS |
555 | | // syntax defined for the easing member of the EffectTiming dictionary. |
556 | | // |
557 | | // If parsing the "easing" property fails, throw a TypeError and abort this procedure. |
558 | 0 | auto easing_string = keyframe.easing.get<String>(); |
559 | 0 | auto easing_value = AnimationEffect::parse_easing_string(realm, easing_string); |
560 | |
|
561 | 0 | if (!easing_value) |
562 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid animation easing value: \"{}\"", easing_string)) }; |
563 | | |
564 | 0 | keyframe.easing.set(NonnullRefPtr<CSS::CSSStyleValue const> { *easing_value }); |
565 | 0 | } |
566 | | |
567 | | // 9. Parse each of the values in unused easings using the CSS syntax defined for easing member of the EffectTiming |
568 | | // interface, and if any of the values fail to parse, throw a TypeError and abort this procedure. |
569 | 0 | for (auto& unused_easing : unused_easings) { |
570 | 0 | auto easing_string = unused_easing.get<String>(); |
571 | 0 | auto easing_value = AnimationEffect::parse_easing_string(realm, easing_string); |
572 | 0 | if (!easing_value) |
573 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid animation easing value: \"{}\"", easing_string)) }; |
574 | 0 | } |
575 | | |
576 | 0 | return processed_keyframes; |
577 | 0 | } |
578 | | |
579 | | // https://www.w3.org/TR/css-animations-2/#keyframe-processing |
580 | | void KeyframeEffect::generate_initial_and_final_frames(RefPtr<KeyFrameSet> keyframe_set, HashTable<CSS::PropertyID> const& animated_properties) |
581 | 0 | { |
582 | | // 1. Find or create the initial keyframe, a keyframe with a keyframe offset of 0%, default timing function |
583 | | // as its keyframe timing function, and default composite as its keyframe composite. |
584 | 0 | KeyFrameSet::ResolvedKeyFrame* initial_keyframe; |
585 | 0 | if (auto existing_keyframe = keyframe_set->keyframes_by_key.find(0)) { |
586 | 0 | initial_keyframe = existing_keyframe; |
587 | 0 | } else { |
588 | 0 | keyframe_set->keyframes_by_key.insert(0, {}); |
589 | 0 | initial_keyframe = keyframe_set->keyframes_by_key.find(0); |
590 | 0 | } |
591 | | |
592 | | // 2. For any property in animated properties that is not otherwise present in a keyframe with an offset of |
593 | | // 0% or one that would be positioned earlier in the used keyframe order, add the computed value of that |
594 | | // property on element to initial keyframe’s keyframe values. |
595 | 0 | for (auto property : animated_properties) { |
596 | 0 | if (!initial_keyframe->properties.contains(property)) |
597 | 0 | initial_keyframe->properties.set(property, KeyFrameSet::UseInitial {}); |
598 | 0 | } |
599 | | |
600 | | // 3. If initial keyframe’s keyframe values is not empty, prepend initial keyframe to keyframes. |
601 | | |
602 | | // 4. Repeat for final keyframe, using an offset of 100%, considering keyframes positioned later in the used |
603 | | // keyframe order, and appending to keyframes. |
604 | 0 | KeyFrameSet::ResolvedKeyFrame* final_keyframe; |
605 | 0 | if (auto existing_keyframe = keyframe_set->keyframes_by_key.find(100 * AnimationKeyFrameKeyScaleFactor)) { |
606 | 0 | final_keyframe = existing_keyframe; |
607 | 0 | } else { |
608 | 0 | keyframe_set->keyframes_by_key.insert(100 * AnimationKeyFrameKeyScaleFactor, {}); |
609 | 0 | final_keyframe = keyframe_set->keyframes_by_key.find(100 * AnimationKeyFrameKeyScaleFactor); |
610 | 0 | } |
611 | |
|
612 | 0 | for (auto property : animated_properties) { |
613 | 0 | if (!final_keyframe->properties.contains(property)) |
614 | 0 | final_keyframe->properties.set(property, KeyFrameSet::UseInitial {}); |
615 | 0 | } |
616 | 0 | } |
617 | | |
618 | | // https://www.w3.org/TR/web-animations-1/#animation-composite-order |
619 | | int KeyframeEffect::composite_order(JS::NonnullGCPtr<KeyframeEffect> a, JS::NonnullGCPtr<KeyframeEffect> b) |
620 | 0 | { |
621 | | // 1. Let the associated animation of an animation effect be the animation associated with the animation effect. |
622 | 0 | auto a_animation = a->associated_animation(); |
623 | 0 | auto b_animation = b->associated_animation(); |
624 | | |
625 | | // 2. Sort A and B by applying the following conditions in turn until the order is resolved, |
626 | | |
627 | | // 1. If A and B’s associated animations differ by class, sort by any inter-class composite order defined for |
628 | | // the corresponding classes. |
629 | 0 | auto a_class = a_animation->animation_class(); |
630 | 0 | auto b_class = b_animation->animation_class(); |
631 | | |
632 | | // From https://www.w3.org/TR/css-animations-2/#animation-composite-order: |
633 | | // "CSS Animations with an owning element have a later composite order than CSS Transitions but an earlier |
634 | | // composite order than animations without a specific animation class." |
635 | 0 | if (a_class != b_class) |
636 | 0 | return to_underlying(a_class) - to_underlying(b_class); |
637 | | |
638 | | // 2. If A and B are still not sorted, sort by any class-specific composite order defined by the common class of |
639 | | // A and B’s associated animations. |
640 | 0 | if (auto order = a_animation->class_specific_composite_order(*b_animation); order.has_value()) |
641 | 0 | return order.value(); |
642 | | |
643 | | // 3. If A and B are still not sorted, sort by the position of their associated animations in the global |
644 | | // animation list. |
645 | 0 | return a_animation->global_animation_list_order() - b_animation->global_animation_list_order(); |
646 | 0 | } |
647 | | |
648 | | JS::NonnullGCPtr<KeyframeEffect> KeyframeEffect::create(JS::Realm& realm) |
649 | 0 | { |
650 | 0 | return realm.heap().allocate<KeyframeEffect>(realm, realm); |
651 | 0 | } |
652 | | |
653 | | // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-keyframeeffect |
654 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<KeyframeEffect>> KeyframeEffect::construct_impl( |
655 | | JS::Realm& realm, |
656 | | JS::Handle<DOM::Element> const& target, |
657 | | Optional<JS::Handle<JS::Object>> const& keyframes, |
658 | | Variant<double, KeyframeEffectOptions> options) |
659 | 0 | { |
660 | 0 | auto& vm = realm.vm(); |
661 | | |
662 | | // 1. Create a new KeyframeEffect object, effect. |
663 | 0 | auto effect = vm.heap().allocate<KeyframeEffect>(realm, realm); |
664 | | |
665 | | // 2. Set the target element of effect to target. |
666 | 0 | effect->set_target(target); |
667 | | |
668 | | // 3. Set the target pseudo-selector to the result corresponding to the first matching condition from below. |
669 | | |
670 | | // If options is a KeyframeEffectOptions object with a pseudoElement property, |
671 | 0 | if (options.has<KeyframeEffectOptions>()) { |
672 | | // Set the target pseudo-selector to the value of the pseudoElement property. |
673 | | // |
674 | | // When assigning this property, the error-handling defined for the pseudoElement setter on the interface is |
675 | | // applied. If the setter requires an exception to be thrown, this procedure must throw the same exception and |
676 | | // abort all further steps. |
677 | 0 | TRY(effect->set_pseudo_element(options.get<KeyframeEffectOptions>().pseudo_element)); |
678 | 0 | } |
679 | | // Otherwise, |
680 | 0 | else { |
681 | | // Set the target pseudo-selector to null. |
682 | | // Note: This is the default when constructed |
683 | 0 | } |
684 | | |
685 | | // 4. Let timing input be the result corresponding to the first matching condition from below. |
686 | 0 | KeyframeEffectOptions timing_input; |
687 | | |
688 | | // If options is a KeyframeEffectOptions object, |
689 | 0 | if (options.has<KeyframeEffectOptions>()) { |
690 | | // Let timing input be options. |
691 | 0 | timing_input = options.get<KeyframeEffectOptions>(); |
692 | 0 | } |
693 | | // Otherwise (if options is a double), |
694 | 0 | else { |
695 | | // Let timing input be a new EffectTiming object with all members set to their default values and duration set |
696 | | // to options. |
697 | 0 | timing_input.duration = options.get<double>(); |
698 | 0 | } |
699 | | |
700 | | // 5. Call the procedure to update the timing properties of an animation effect of effect from timing input. |
701 | | // If that procedure causes an exception to be thrown, propagate the exception and abort this procedure. |
702 | 0 | TRY(effect->update_timing(timing_input.to_optional_effect_timing())); |
703 | | |
704 | | // 6. If options is a KeyframeEffectOptions object, assign the composite property of effect to the corresponding |
705 | | // value from options. |
706 | | // |
707 | | // When assigning this property, the error-handling defined for the corresponding setter on the KeyframeEffect |
708 | | // interface is applied. If the setter requires an exception to be thrown for the value specified by options, |
709 | | // this procedure must throw the same exception and abort all further steps. |
710 | 0 | if (options.has<KeyframeEffectOptions>()) |
711 | 0 | effect->set_composite(options.get<KeyframeEffectOptions>().composite); |
712 | | |
713 | | // 7. Initialize the set of keyframes by performing the procedure defined for setKeyframes() passing keyframes as |
714 | | // the input. |
715 | 0 | TRY(effect->set_keyframes(keyframes)); |
716 | |
|
717 | 0 | return effect; |
718 | 0 | } |
719 | | |
720 | | // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-keyframeeffect-source |
721 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<KeyframeEffect>> KeyframeEffect::construct_impl(JS::Realm& realm, JS::NonnullGCPtr<KeyframeEffect> source) |
722 | 0 | { |
723 | 0 | auto& vm = realm.vm(); |
724 | | |
725 | | // 1. Create a new KeyframeEffect object, effect. |
726 | 0 | auto effect = vm.heap().allocate<KeyframeEffect>(realm, realm); |
727 | | |
728 | | // 2. Set the following properties of effect using the corresponding values of source: |
729 | | |
730 | | // - effect target, |
731 | 0 | effect->m_target_element = source->target(); |
732 | | |
733 | | // - keyframes, |
734 | 0 | effect->m_keyframes = source->m_keyframes; |
735 | | |
736 | | // - composite operation, and |
737 | 0 | effect->set_composite(source->composite()); |
738 | | |
739 | | // - all specified timing properties: |
740 | | |
741 | | // - start delay, |
742 | 0 | effect->m_start_delay = source->m_start_delay; |
743 | | |
744 | | // - end delay, |
745 | 0 | effect->m_end_delay = source->m_end_delay; |
746 | | |
747 | | // - fill mode, |
748 | 0 | effect->m_fill_mode = source->m_fill_mode; |
749 | | |
750 | | // - iteration start, |
751 | 0 | effect->m_iteration_start = source->m_iteration_start; |
752 | | |
753 | | // - iteration count, |
754 | 0 | effect->m_iteration_count = source->m_iteration_count; |
755 | | |
756 | | // - iteration duration, |
757 | 0 | effect->m_iteration_duration = source->m_iteration_duration; |
758 | | |
759 | | // - playback direction, and |
760 | 0 | effect->m_playback_direction = source->m_playback_direction; |
761 | | |
762 | | // - timing function. |
763 | 0 | effect->m_timing_function = source->m_timing_function; |
764 | |
|
765 | 0 | return effect; |
766 | 0 | } |
767 | | |
768 | | void KeyframeEffect::set_target(DOM::Element* target) |
769 | 0 | { |
770 | 0 | if (auto animation = this->associated_animation()) { |
771 | 0 | if (m_target_element) |
772 | 0 | m_target_element->disassociate_with_animation(*animation); |
773 | 0 | if (target) |
774 | 0 | target->associate_with_animation(*animation); |
775 | 0 | } |
776 | 0 | m_target_element = target; |
777 | 0 | } |
778 | | |
779 | | Optional<String> KeyframeEffect::pseudo_element() const |
780 | 0 | { |
781 | 0 | if (!m_target_pseudo_selector.has_value()) |
782 | 0 | return {}; |
783 | 0 | return MUST(String::formatted("::{}", m_target_pseudo_selector->name())); |
784 | 0 | } |
785 | | |
786 | | // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-pseudoelement |
787 | | WebIDL::ExceptionOr<void> KeyframeEffect::set_pseudo_element(Optional<String> pseudo_element) |
788 | 0 | { |
789 | 0 | auto& realm = this->realm(); |
790 | | |
791 | | // On setting, sets the target pseudo-selector of the animation effect to the provided value after applying the |
792 | | // following exceptions: |
793 | | |
794 | | // FIXME: |
795 | | // - If one of the legacy Selectors Level 2 single-colon selectors (':before', ':after', ':first-letter', or |
796 | | // ':first-line') is specified, the target pseudo-selector must be set to the equivalent two-colon selector |
797 | | // (e.g. '::before'). |
798 | 0 | if (pseudo_element.has_value()) { |
799 | 0 | auto value = pseudo_element.value(); |
800 | |
|
801 | 0 | if (value == ":before" || value == ":after" || value == ":first-letter" || value == ":first-line") { |
802 | 0 | m_target_pseudo_selector = CSS::Selector::PseudoElement::from_string(MUST(value.substring_from_byte_offset(1))); |
803 | 0 | return {}; |
804 | 0 | } |
805 | 0 | } |
806 | | |
807 | | // - If the provided value is not null and is an invalid <pseudo-element-selector>, the user agent must throw a |
808 | | // DOMException with error name SyntaxError and leave the target pseudo-selector of this animation effect |
809 | | // unchanged. |
810 | 0 | if (pseudo_element.has_value()) { |
811 | 0 | if (pseudo_element->starts_with_bytes("::"sv)) { |
812 | 0 | if (auto value = CSS::Selector::PseudoElement::from_string(MUST(pseudo_element->substring_from_byte_offset(2))); value.has_value()) { |
813 | 0 | m_target_pseudo_selector = value; |
814 | 0 | return {}; |
815 | 0 | } |
816 | 0 | } |
817 | | |
818 | 0 | return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid pseudo-element selector: \"{}\"", pseudo_element.value()))); |
819 | 0 | } |
820 | | |
821 | 0 | m_target_pseudo_selector = {}; |
822 | 0 | return {}; |
823 | 0 | } |
824 | | |
825 | | Optional<CSS::Selector::PseudoElement::Type> KeyframeEffect::pseudo_element_type() const |
826 | 0 | { |
827 | 0 | if (!m_target_pseudo_selector.has_value()) |
828 | 0 | return {}; |
829 | 0 | return m_target_pseudo_selector->type(); |
830 | 0 | } |
831 | | |
832 | | // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-getkeyframes |
833 | | WebIDL::ExceptionOr<JS::MarkedVector<JS::Object*>> KeyframeEffect::get_keyframes() |
834 | 0 | { |
835 | 0 | if (m_keyframe_objects.size() != m_keyframes.size()) { |
836 | 0 | auto& vm = this->vm(); |
837 | 0 | auto& realm = this->realm(); |
838 | | |
839 | | // Recalculate the keyframe objects |
840 | 0 | VERIFY(m_keyframe_objects.size() == 0); |
841 | | |
842 | 0 | for (auto& keyframe : m_keyframes) { |
843 | 0 | auto object = JS::Object::create(realm, realm.intrinsics().object_prototype()); |
844 | 0 | TRY(object->set(vm.names.offset, keyframe.offset.has_value() ? JS::Value(keyframe.offset.value()) : JS::js_null(), ShouldThrowExceptions::Yes)); |
845 | 0 | TRY(object->set(vm.names.computedOffset, JS::Value(keyframe.computed_offset.value()), ShouldThrowExceptions::Yes)); |
846 | 0 | auto easing_value = keyframe.easing.get<NonnullRefPtr<CSS::CSSStyleValue const>>(); |
847 | 0 | TRY(object->set(vm.names.easing, JS::PrimitiveString::create(vm, easing_value->to_string()), ShouldThrowExceptions::Yes)); |
848 | |
|
849 | 0 | if (keyframe.composite == Bindings::CompositeOperationOrAuto::Replace) { |
850 | 0 | TRY(object->set(vm.names.composite, JS::PrimitiveString::create(vm, "replace"sv), ShouldThrowExceptions::Yes)); |
851 | 0 | } else if (keyframe.composite == Bindings::CompositeOperationOrAuto::Add) { |
852 | 0 | TRY(object->set(vm.names.composite, JS::PrimitiveString::create(vm, "add"sv), ShouldThrowExceptions::Yes)); |
853 | 0 | } else if (keyframe.composite == Bindings::CompositeOperationOrAuto::Accumulate) { |
854 | 0 | TRY(object->set(vm.names.composite, JS::PrimitiveString::create(vm, "accumulate"sv), ShouldThrowExceptions::Yes)); |
855 | 0 | } else { |
856 | 0 | TRY(object->set(vm.names.composite, JS::PrimitiveString::create(vm, "auto"sv), ShouldThrowExceptions::Yes)); |
857 | 0 | } |
858 | |
|
859 | 0 | for (auto const& [id, value] : keyframe.parsed_properties()) { |
860 | 0 | auto value_string = JS::PrimitiveString::create(vm, value->to_string()); |
861 | 0 | TRY(object->set(JS::PropertyKey(DeprecatedFlyString(CSS::camel_case_string_from_property_id(id))), value_string, ShouldThrowExceptions::Yes)); |
862 | 0 | } |
863 | |
|
864 | 0 | m_keyframe_objects.append(object); |
865 | 0 | } |
866 | 0 | } |
867 | | |
868 | 0 | JS::MarkedVector<JS::Object*> keyframes { heap() }; |
869 | 0 | for (auto const& keyframe : m_keyframe_objects) |
870 | 0 | keyframes.append(keyframe); |
871 | 0 | return keyframes; |
872 | 0 | } |
873 | | |
874 | | // https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-setkeyframes |
875 | | WebIDL::ExceptionOr<void> KeyframeEffect::set_keyframes(Optional<JS::Handle<JS::Object>> const& keyframe_object) |
876 | 0 | { |
877 | 0 | m_keyframe_objects.clear(); |
878 | 0 | m_keyframes = TRY(process_a_keyframes_argument(realm(), keyframe_object.has_value() ? JS::GCPtr { keyframe_object->ptr() } : JS::GCPtr<Object> {})); |
879 | | // FIXME: After processing the keyframe argument, we need to turn the set of keyframes into a set of computed |
880 | | // keyframes using the procedure outlined in the second half of |
881 | | // https://www.w3.org/TR/web-animations-1/#calculating-computed-keyframes. For now, just compute the |
882 | | // missing keyframe offsets |
883 | 0 | compute_missing_keyframe_offsets(m_keyframes); |
884 | |
|
885 | 0 | auto keyframe_set = adopt_ref(*new KeyFrameSet); |
886 | 0 | m_target_properties.clear(); |
887 | 0 | auto target = this->target(); |
888 | |
|
889 | 0 | for (auto& keyframe : m_keyframes) { |
890 | 0 | Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame resolved_keyframe; |
891 | |
|
892 | 0 | auto key = static_cast<u64>(keyframe.computed_offset.value() * 100 * AnimationKeyFrameKeyScaleFactor); |
893 | |
|
894 | 0 | for (auto [property_id, property_value] : keyframe.parsed_properties()) { |
895 | 0 | if (property_value->is_unresolved() && target) |
896 | 0 | property_value = CSS::Parser::Parser::resolve_unresolved_style_value(CSS::Parser::ParsingContext { target->document() }, *target, pseudo_element_type(), property_id, property_value->as_unresolved()); |
897 | 0 | CSS::StyleComputer::for_each_property_expanding_shorthands(property_id, property_value, CSS::StyleComputer::AllowUnresolved::Yes, [&](CSS::PropertyID shorthand_id, CSS::CSSStyleValue const& shorthand_value) { |
898 | 0 | m_target_properties.set(shorthand_id); |
899 | 0 | resolved_keyframe.properties.set(shorthand_id, NonnullRefPtr<CSS::CSSStyleValue const> { shorthand_value }); |
900 | 0 | }); |
901 | 0 | } |
902 | |
|
903 | 0 | keyframe_set->keyframes_by_key.insert(key, resolved_keyframe); |
904 | 0 | } |
905 | |
|
906 | 0 | generate_initial_and_final_frames(keyframe_set, m_target_properties); |
907 | 0 | m_key_frame_set = keyframe_set; |
908 | |
|
909 | 0 | return {}; |
910 | 0 | } |
911 | | |
912 | | KeyframeEffect::KeyframeEffect(JS::Realm& realm) |
913 | 0 | : AnimationEffect(realm) |
914 | 0 | { |
915 | 0 | } |
916 | | |
917 | | void KeyframeEffect::initialize(JS::Realm& realm) |
918 | 0 | { |
919 | 0 | Base::initialize(realm); |
920 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(KeyframeEffect); |
921 | 0 | } |
922 | | |
923 | | void KeyframeEffect::visit_edges(Cell::Visitor& visitor) |
924 | 0 | { |
925 | 0 | Base::visit_edges(visitor); |
926 | 0 | visitor.visit(m_target_element); |
927 | 0 | visitor.visit(m_keyframe_objects); |
928 | 0 | } |
929 | | |
930 | | static CSS::RequiredInvalidationAfterStyleChange compute_required_invalidation(HashMap<CSS::PropertyID, NonnullRefPtr<CSS::CSSStyleValue const>> const& old_properties, HashMap<CSS::PropertyID, NonnullRefPtr<CSS::CSSStyleValue const>> const& new_properties) |
931 | 0 | { |
932 | 0 | CSS::RequiredInvalidationAfterStyleChange invalidation; |
933 | 0 | auto old_and_new_properties = MUST(Bitmap::create(to_underlying(CSS::last_property_id) + 1, 0)); |
934 | 0 | for (auto const& [property_id, _] : old_properties) |
935 | 0 | old_and_new_properties.set(to_underlying(property_id), 1); |
936 | 0 | for (auto const& [property_id, _] : new_properties) |
937 | 0 | old_and_new_properties.set(to_underlying(property_id), 1); |
938 | 0 | for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) { |
939 | 0 | if (!old_and_new_properties.get(i)) |
940 | 0 | continue; |
941 | 0 | auto property_id = static_cast<CSS::PropertyID>(i); |
942 | 0 | auto old_value = old_properties.get(property_id).value_or({}); |
943 | 0 | auto new_value = new_properties.get(property_id).value_or({}); |
944 | 0 | if (!old_value && !new_value) |
945 | 0 | continue; |
946 | 0 | invalidation |= compute_property_invalidation(property_id, old_value, new_value); |
947 | 0 | } |
948 | 0 | return invalidation; |
949 | 0 | } |
950 | | |
951 | | void KeyframeEffect::update_style_properties() |
952 | 0 | { |
953 | 0 | auto target = this->target(); |
954 | 0 | if (!target) |
955 | 0 | return; |
956 | | |
957 | 0 | CSS::StyleProperties* style = nullptr; |
958 | 0 | if (!pseudo_element_type().has_value()) |
959 | 0 | style = target->computed_css_values(); |
960 | 0 | else |
961 | 0 | style = target->pseudo_element_computed_css_values(pseudo_element_type().value()); |
962 | |
|
963 | 0 | if (!style) |
964 | 0 | return; |
965 | | |
966 | 0 | auto animated_properties_before_update = style->animated_property_values(); |
967 | |
|
968 | 0 | auto& document = target->document(); |
969 | 0 | document.style_computer().collect_animation_into(*target, pseudo_element_type(), *this, *style, CSS::StyleComputer::AnimationRefresh::Yes); |
970 | | |
971 | | // Traversal of the subtree is necessary to update the animated properties inherited from the target element. |
972 | 0 | target->for_each_in_subtree_of_type<DOM::Element>([&](auto& element) { |
973 | 0 | auto* element_style = element.computed_css_values(); |
974 | 0 | if (!element_style || !element.layout_node()) |
975 | 0 | return TraversalDecision::Continue; |
976 | | |
977 | 0 | for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) { |
978 | 0 | if (element_style->is_property_inherited(static_cast<CSS::PropertyID>(i))) { |
979 | 0 | auto new_value = CSS::StyleComputer::get_inherit_value(document.realm(), static_cast<CSS::PropertyID>(i), &element); |
980 | 0 | element_style->set_property(static_cast<CSS::PropertyID>(i), *new_value, CSS::StyleProperties::Inherited::Yes); |
981 | 0 | } |
982 | 0 | } |
983 | |
|
984 | 0 | element.layout_node()->apply_style(*element_style); |
985 | 0 | return TraversalDecision::Continue; |
986 | 0 | }); |
987 | |
|
988 | 0 | auto invalidation = compute_required_invalidation(animated_properties_before_update, style->animated_property_values()); |
989 | |
|
990 | 0 | if (!pseudo_element_type().has_value()) { |
991 | 0 | if (target->layout_node()) |
992 | 0 | target->layout_node()->apply_style(*style); |
993 | 0 | } else { |
994 | 0 | auto pseudo_element_node = target->get_pseudo_element_node(pseudo_element_type().value()); |
995 | 0 | if (auto* node_with_style = dynamic_cast<Layout::NodeWithStyle*>(pseudo_element_node.ptr())) { |
996 | 0 | node_with_style->apply_style(*style); |
997 | 0 | } |
998 | 0 | } |
999 | |
|
1000 | 0 | if (invalidation.relayout) |
1001 | 0 | document.set_needs_layout(); |
1002 | 0 | if (invalidation.rebuild_layout_tree) |
1003 | 0 | document.invalidate_layout_tree(); |
1004 | 0 | if (invalidation.repaint) |
1005 | 0 | document.set_needs_to_resolve_paint_only_properties(); |
1006 | 0 | if (invalidation.rebuild_stacking_context_tree) |
1007 | 0 | document.invalidate_stacking_context_tree(); |
1008 | 0 | } |
1009 | | |
1010 | | } |