/src/serenity/Userland/Libraries/LibWeb/Animations/Animatable.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org> |
3 | | * Copyright (c) 2024, Sam Atkins <sam@ladybird.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/QuickSort.h> |
9 | | #include <LibWeb/Animations/Animatable.h> |
10 | | #include <LibWeb/Animations/Animation.h> |
11 | | #include <LibWeb/Animations/DocumentTimeline.h> |
12 | | #include <LibWeb/CSS/CSSTransition.h> |
13 | | #include <LibWeb/CSS/StyleValues/EasingStyleValue.h> |
14 | | #include <LibWeb/CSS/StyleValues/TimeStyleValue.h> |
15 | | #include <LibWeb/DOM/Document.h> |
16 | | #include <LibWeb/DOM/Element.h> |
17 | | |
18 | | namespace Web::Animations { |
19 | | |
20 | | // https://www.w3.org/TR/web-animations-1/#dom-animatable-animate |
21 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<Animation>> Animatable::animate(Optional<JS::Handle<JS::Object>> keyframes, Variant<Empty, double, KeyframeAnimationOptions> options) |
22 | 0 | { |
23 | | // 1. Let target be the object on which this method was called. |
24 | 0 | JS::NonnullGCPtr target { *static_cast<DOM::Element*>(this) }; |
25 | 0 | auto& realm = target->realm(); |
26 | | |
27 | | // 2. Construct a new KeyframeEffect object, effect, in the relevant Realm of target by using the same procedure as |
28 | | // the KeyframeEffect(target, keyframes, options) constructor, passing target as the target argument, and the |
29 | | // keyframes and options arguments as supplied. |
30 | | // |
31 | | // If the above procedure causes an exception to be thrown, propagate the exception and abort this procedure. |
32 | 0 | auto effect = TRY(options.visit( |
33 | 0 | [&](Empty) { return KeyframeEffect::construct_impl(realm, target, keyframes); }, |
34 | 0 | [&](auto const& value) { return KeyframeEffect::construct_impl(realm, target, keyframes, value); })); |
35 | | |
36 | | // 3. If options is a KeyframeAnimationOptions object, let timeline be the timeline member of options or, if |
37 | | // timeline member of options is missing, be the default document timeline of the node document of the element |
38 | | // on which this method was called. |
39 | 0 | Optional<JS::GCPtr<AnimationTimeline>> timeline; |
40 | 0 | if (options.has<KeyframeAnimationOptions>()) |
41 | 0 | timeline = options.get<KeyframeAnimationOptions>().timeline; |
42 | 0 | if (!timeline.has_value()) |
43 | 0 | timeline = target->document().timeline(); |
44 | | |
45 | | // 4. Construct a new Animation object, animation, in the relevant Realm of target by using the same procedure as |
46 | | // the Animation() constructor, passing effect and timeline as arguments of the same name. |
47 | 0 | auto animation = TRY(Animation::construct_impl(realm, effect, move(timeline))); |
48 | | |
49 | | // 5. If options is a KeyframeAnimationOptions object, assign the value of the id member of options to animation’s |
50 | | // id attribute. |
51 | 0 | if (options.has<KeyframeAnimationOptions>()) |
52 | 0 | animation->set_id(options.get<KeyframeAnimationOptions>().id); |
53 | | |
54 | | // 6. Run the procedure to play an animation for animation with the auto-rewind flag set to true. |
55 | 0 | TRY(animation->play_an_animation(Animation::AutoRewind::Yes)); |
56 | | |
57 | | // 7. Return animation. |
58 | 0 | return animation; |
59 | 0 | } |
60 | | |
61 | | // https://www.w3.org/TR/web-animations-1/#dom-animatable-getanimations |
62 | | Vector<JS::NonnullGCPtr<Animation>> Animatable::get_animations(GetAnimationsOptions options) |
63 | 0 | { |
64 | 0 | verify_cast<DOM::Element>(*this).document().update_style(); |
65 | 0 | return get_animations_internal(options); |
66 | 0 | } |
67 | | |
68 | | Vector<JS::NonnullGCPtr<Animation>> Animatable::get_animations_internal(GetAnimationsOptions options) |
69 | 0 | { |
70 | | // Returns the set of relevant animations for this object, or, if an options parameter is passed with subtree set to |
71 | | // true, returns the set of relevant animations for a subtree for this object. |
72 | | |
73 | | // The returned list is sorted using the composite order described for the associated animations of effects in |
74 | | // §5.4.2 The effect stack. |
75 | 0 | if (!m_is_sorted_by_composite_order) { |
76 | 0 | quick_sort(m_associated_animations, [](JS::NonnullGCPtr<Animation>& a, JS::NonnullGCPtr<Animation>& b) { |
77 | 0 | auto& a_effect = verify_cast<KeyframeEffect>(*a->effect()); |
78 | 0 | auto& b_effect = verify_cast<KeyframeEffect>(*b->effect()); |
79 | 0 | return KeyframeEffect::composite_order(a_effect, b_effect) < 0; |
80 | 0 | }); |
81 | 0 | m_is_sorted_by_composite_order = true; |
82 | 0 | } |
83 | |
|
84 | 0 | Vector<JS::NonnullGCPtr<Animation>> relevant_animations; |
85 | 0 | for (auto const& animation : m_associated_animations) { |
86 | 0 | if (animation->is_relevant()) |
87 | 0 | relevant_animations.append(*animation); |
88 | 0 | } |
89 | |
|
90 | 0 | if (options.subtree) { |
91 | 0 | JS::NonnullGCPtr target { *static_cast<DOM::Element*>(this) }; |
92 | 0 | target->for_each_child_of_type<DOM::Element>([&](auto& child) { |
93 | 0 | relevant_animations.extend(child.get_animations(options)); |
94 | 0 | return IterationDecision::Continue; |
95 | 0 | }); |
96 | 0 | } |
97 | |
|
98 | 0 | return relevant_animations; |
99 | 0 | } |
100 | | |
101 | | void Animatable::associate_with_animation(JS::NonnullGCPtr<Animation> animation) |
102 | 0 | { |
103 | 0 | m_associated_animations.append(animation); |
104 | 0 | m_is_sorted_by_composite_order = false; |
105 | 0 | } |
106 | | |
107 | | void Animatable::disassociate_with_animation(JS::NonnullGCPtr<Animation> animation) |
108 | 0 | { |
109 | 0 | m_associated_animations.remove_first_matching([&](auto element) { return animation == element; }); |
110 | 0 | } |
111 | | |
112 | | void Animatable::add_transitioned_properties(Vector<Vector<CSS::PropertyID>> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions) |
113 | 0 | { |
114 | 0 | VERIFY(properties.size() == delays.size()); |
115 | 0 | VERIFY(properties.size() == durations.size()); |
116 | 0 | VERIFY(properties.size() == timing_functions.size()); |
117 | | |
118 | 0 | for (size_t i = 0; i < properties.size(); i++) { |
119 | 0 | size_t index_of_this_transition = m_transition_attributes.size(); |
120 | 0 | auto delay = delays[i]->is_time() ? delays[i]->as_time().time().to_milliseconds() : 0; |
121 | 0 | auto duration = durations[i]->is_time() ? durations[i]->as_time().time().to_milliseconds() : 0; |
122 | 0 | auto timing_function = timing_functions[i]->is_easing() ? timing_functions[i]->as_easing().function() : CSS::EasingStyleValue::CubicBezier::ease(); |
123 | 0 | VERIFY(timing_functions[i]->is_easing()); |
124 | 0 | m_transition_attributes.empend(delay, duration, timing_function); |
125 | |
|
126 | 0 | for (auto const& property : properties[i]) |
127 | 0 | m_transition_attribute_indices.set(property, index_of_this_transition); |
128 | 0 | } |
129 | 0 | } |
130 | | |
131 | | Optional<Animatable::TransitionAttributes const&> Animatable::property_transition_attributes(CSS::PropertyID property) const |
132 | 0 | { |
133 | 0 | if (auto maybe_index = m_transition_attribute_indices.get(property); maybe_index.has_value()) |
134 | 0 | return m_transition_attributes[maybe_index.value()]; |
135 | 0 | return {}; |
136 | 0 | } |
137 | | |
138 | | JS::GCPtr<CSS::CSSTransition> Animatable::property_transition(CSS::PropertyID property) const |
139 | 0 | { |
140 | 0 | if (auto maybe_animation = m_associated_transitions.get(property); maybe_animation.has_value()) |
141 | 0 | return maybe_animation.value(); |
142 | 0 | return {}; |
143 | 0 | } |
144 | | |
145 | | void Animatable::set_transition(CSS::PropertyID property, JS::NonnullGCPtr<CSS::CSSTransition> animation) |
146 | 0 | { |
147 | 0 | VERIFY(!m_associated_transitions.contains(property)); |
148 | 0 | m_associated_transitions.set(property, animation); |
149 | 0 | } |
150 | | |
151 | | void Animatable::remove_transition(CSS::PropertyID property_id) |
152 | 0 | { |
153 | 0 | VERIFY(m_associated_transitions.contains(property_id)); |
154 | 0 | m_associated_transitions.remove(property_id); |
155 | 0 | } |
156 | | |
157 | | void Animatable::clear_transitions() |
158 | 0 | { |
159 | 0 | m_associated_transitions.clear(); |
160 | 0 | m_transition_attribute_indices.clear(); |
161 | 0 | m_transition_attributes.clear(); |
162 | 0 | } |
163 | | |
164 | | void Animatable::visit_edges(JS::Cell::Visitor& visitor) |
165 | 0 | { |
166 | 0 | visitor.visit(m_associated_animations); |
167 | 0 | for (auto const& cached_animation_source : m_cached_animation_name_source) |
168 | 0 | visitor.visit(cached_animation_source); |
169 | 0 | for (auto const& cached_animation_name : m_cached_animation_name_animation) |
170 | 0 | visitor.visit(cached_animation_name); |
171 | 0 | visitor.visit(m_cached_transition_property_source); |
172 | 0 | visitor.visit(m_associated_transitions); |
173 | 0 | } |
174 | | |
175 | | JS::GCPtr<CSS::CSSStyleDeclaration const> Animatable::cached_animation_name_source(Optional<CSS::Selector::PseudoElement::Type> pseudo_element) const |
176 | 0 | { |
177 | 0 | if (pseudo_element.has_value()) { |
178 | 0 | if (!CSS::Selector::PseudoElement::is_known_pseudo_element_type(pseudo_element.value())) { |
179 | 0 | return {}; |
180 | 0 | } |
181 | 0 | return m_cached_animation_name_source[to_underlying(pseudo_element.value()) + 1]; |
182 | 0 | } |
183 | 0 | return m_cached_animation_name_source[0]; |
184 | 0 | } |
185 | | |
186 | | void Animatable::set_cached_animation_name_source(JS::GCPtr<CSS::CSSStyleDeclaration const> value, Optional<CSS::Selector::PseudoElement::Type> pseudo_element) |
187 | 0 | { |
188 | 0 | if (pseudo_element.has_value()) { |
189 | 0 | if (!CSS::Selector::PseudoElement::is_known_pseudo_element_type(pseudo_element.value())) { |
190 | 0 | return; |
191 | 0 | } |
192 | 0 | m_cached_animation_name_source[to_underlying(pseudo_element.value()) + 1] = value; |
193 | 0 | } else { |
194 | 0 | m_cached_animation_name_source[0] = value; |
195 | 0 | } |
196 | 0 | } |
197 | | |
198 | | JS::GCPtr<Animations::Animation> Animatable::cached_animation_name_animation(Optional<CSS::Selector::PseudoElement::Type> pseudo_element) const |
199 | 0 | { |
200 | 0 | if (pseudo_element.has_value()) { |
201 | 0 | if (!CSS::Selector::PseudoElement::is_known_pseudo_element_type(pseudo_element.value())) { |
202 | 0 | return {}; |
203 | 0 | } |
204 | | |
205 | 0 | return m_cached_animation_name_animation[to_underlying(pseudo_element.value()) + 1]; |
206 | 0 | } |
207 | 0 | return m_cached_animation_name_animation[0]; |
208 | 0 | } |
209 | | |
210 | | void Animatable::set_cached_animation_name_animation(JS::GCPtr<Animations::Animation> value, Optional<CSS::Selector::PseudoElement::Type> pseudo_element) |
211 | 0 | { |
212 | |
|
213 | 0 | if (pseudo_element.has_value()) { |
214 | 0 | if (!CSS::Selector::PseudoElement::is_known_pseudo_element_type(pseudo_element.value())) { |
215 | 0 | return; |
216 | 0 | } |
217 | | |
218 | 0 | m_cached_animation_name_animation[to_underlying(pseudo_element.value()) + 1] = value; |
219 | 0 | } else { |
220 | 0 | m_cached_animation_name_animation[0] = value; |
221 | 0 | } |
222 | 0 | } |
223 | | } |