Coverage Report

Created: 2026-05-16 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}