瀏覽代碼

LibWeb: Transition StyleComputer to Web Animations

With this commit, we are finally running animations off of the web
animations spec! A lot of the work StyleComputer is doing is now done
elsewhere. For example, fill-forward animations are handled by
Animation::is_relevant() returning true in the after phase, meaning the
"active_state_if_fill_forward" map is no longer needed.
Matthew Olsson 1 年之前
父節點
當前提交
ae3326a447

+ 4 - 0
Userland/Libraries/LibWeb/Animations/Animatable.h

@@ -33,9 +33,13 @@ public:
     void associate_with_effect(JS::NonnullGCPtr<AnimationEffect> effect);
     void disassociate_with_effect(JS::NonnullGCPtr<AnimationEffect> effect);
 
+    JS::GCPtr<CSS::CSSStyleDeclaration const> cached_animation_name_source() const { return m_cached_animation_name_source; }
+    void set_cached_animation_name_source(JS::GCPtr<CSS::CSSStyleDeclaration const> value) { m_cached_animation_name_source = value; }
+
 private:
     Vector<JS::NonnullGCPtr<AnimationEffect>> m_associated_effects;
     bool m_is_sorted_by_composite_order { true };
+    JS::GCPtr<CSS::CSSStyleDeclaration const> m_cached_animation_name_source;
 };
 
 }

+ 3 - 0
Userland/Libraries/LibWeb/Animations/Animation.cpp

@@ -370,6 +370,9 @@ WebIDL::ExceptionOr<void> Animation::play()
 // https://www.w3.org/TR/web-animations-1/#play-an-animation
 WebIDL::ExceptionOr<void> Animation::play_an_animation(AutoRewind auto_rewind)
 {
+    if (auto document = document_for_timing())
+        document->ensure_animation_timer();
+
     // 1. Let aborted pause be a boolean flag that is true if animation has a pending pause task, and false otherwise.
     auto aborted_pause = m_pending_pause_task == TaskState::Scheduled;
 

+ 194 - 519
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2021, the SerenityOS developers.
  * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
+ * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -12,6 +13,7 @@
 #include <AK/Find.h>
 #include <AK/Function.h>
 #include <AK/HashMap.h>
+#include <AK/Math.h>
 #include <AK/QuickSort.h>
 #include <AK/TemporaryChange.h>
 #include <LibGfx/Font/Font.h>
@@ -22,6 +24,11 @@
 #include <LibGfx/Font/VectorFont.h>
 #include <LibGfx/Font/WOFF/Font.h>
 #include <LibGfx/Font/WOFF2/Font.h>
+#include <LibWeb/Animations/AnimationEffect.h>
+#include <LibWeb/Animations/DocumentTimeline.h>
+#include <LibWeb/Animations/TimingFunction.h>
+#include <LibWeb/CSS/AnimationEvent.h>
+#include <LibWeb/CSS/CSSAnimation.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
 #include <LibWeb/CSS/CSSImportRule.h>
 #include <LibWeb/CSS/CSSStyleRule.h>
@@ -57,11 +64,14 @@
 #include <LibWeb/DOM/Element.h>
 #include <LibWeb/HTML/HTMLBRElement.h>
 #include <LibWeb/HTML/HTMLHtmlElement.h>
+#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
+#include <LibWeb/HighResolutionTime/TimeOrigin.h>
 #include <LibWeb/Layout/Node.h>
 #include <LibWeb/Loader/ResourceLoader.h>
 #include <LibWeb/Namespace.h>
 #include <LibWeb/Platform/FontPlugin.h>
 #include <LibWeb/ReferrerPolicy/AbstractOperations.h>
+#include <math.h>
 #include <stdio.h>
 
 namespace AK {
@@ -743,46 +753,6 @@ static ErrorOr<void> cascade_custom_properties(DOM::Element& element, Optional<C
     return {};
 }
 
-StyleComputer::AnimationStepTransition StyleComputer::Animation::step(CSS::Time const& time_step)
-{
-    auto delay_ms = remaining_delay.to_milliseconds();
-    auto time_step_ms = time_step.to_milliseconds();
-
-    if (delay_ms > time_step_ms) {
-        remaining_delay = CSS::Time { delay_ms - time_step_ms, CSS::Time::Type::Ms };
-        return AnimationStepTransition::NoTransition;
-    }
-
-    remaining_delay = CSS::Time { 0, CSS::Time::Type::Ms };
-    time_step_ms -= delay_ms;
-
-    // "auto": For time-driven animations, equivalent to 0s.
-    // https://www.w3.org/TR/2023/WD-css-animations-2-20230602/#valdef-animation-duration-auto
-    auto used_duration = duration.value_or(CSS::Time { 0, CSS::Time::Type::S });
-
-    auto added_progress = time_step_ms / used_duration.to_milliseconds();
-    auto new_progress = progress.as_fraction() + added_progress;
-    auto changed_iteration = false;
-    if (new_progress >= 1) {
-        if (iteration_count.has_value()) {
-            if (iteration_count.value() <= 1) {
-                progress = CSS::Percentage(100);
-                return AnimationStepTransition::ActiveToAfter;
-            }
-            --iteration_count.value();
-            changed_iteration = true;
-        }
-        ++current_iteration;
-        new_progress = 0;
-    }
-    progress = CSS::Percentage(new_progress * 100);
-
-    if (changed_iteration)
-        return AnimationStepTransition::ActiveToActiveChangingTheIteration;
-
-    return AnimationStepTransition::AfterToActive;
-}
-
 static ErrorOr<NonnullRefPtr<StyleValue>> interpolate_property(StyleValue const& from, StyleValue const& to, float delta)
 {
     if (from.type() != to.type()) {
@@ -878,31 +848,23 @@ static ErrorOr<NonnullRefPtr<StyleValue>> interpolate_property(StyleValue const&
     }
 }
 
-bool StyleComputer::Animation::is_animating_backwards() const
+ErrorOr<void> StyleComputer::collect_animation_into(JS::NonnullGCPtr<Animations::KeyframeEffect> effect, StyleProperties& style_properties) const
 {
-    return (direction == CSS::AnimationDirection::AlternateReverse && current_iteration % 2 == 1)
-        || (direction == CSS::AnimationDirection::Alternate && current_iteration % 2 == 0)
-        || direction == CSS::AnimationDirection::Reverse;
-}
-
-ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_properties, RuleCache const& rule_cache) const
-{
-    if (remaining_delay.to_milliseconds() != 0) {
-        // If the fill mode is backwards or both, we'll pretend that the animation is started, but stuck at progress 0
-        if (fill_mode != CSS::AnimationFillMode::Backwards && fill_mode != CSS::AnimationFillMode::Both)
-            return {};
-    }
+    auto animation = effect->associated_animation();
+    if (!animation)
+        return {};
 
-    auto matching_keyframes = rule_cache.rules_by_animation_keyframes.get(name);
-    if (!matching_keyframes.has_value())
+    auto output_progress = effect->transformed_progress();
+    if (!output_progress.has_value())
         return {};
 
-    auto& keyframes = matching_keyframes.value()->keyframes_by_key;
+    if (!effect->key_frame_set())
+        return {};
 
-    auto output_progress = compute_output_progress(progress.as_fraction()) * 100.f;
-    auto is_backwards = is_animating_backwards();
+    auto& keyframes = effect->key_frame_set()->keyframes_by_key;
+    auto is_backwards = effect->current_direction() == Animations::AnimationDirection::Backwards;
 
-    auto key = static_cast<u64>(output_progress * AnimationKeyFrameKeyScaleFactor);
+    auto key = static_cast<u64>(output_progress.value() * 100.0 * Animations::KeyframeEffect::AnimationKeyFrameKeyScaleFactor);
     auto matching_keyframe_it = is_backwards ? keyframes.find_smallest_not_below_iterator(key) : keyframes.find_largest_not_above_iterator(key);
     if (matching_keyframe_it.is_end()) {
         if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
@@ -934,52 +896,32 @@ ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_prop
             : static_cast<float>(key - keyframe_start) / static_cast<float>(keyframe_end - keyframe_start);
     }();
 
-    auto valid_properties = 0;
-    for (auto const& property : keyframe_values.resolved_properties) {
-        if (property.has<Empty>())
-            continue;
-        valid_properties++;
+    if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
+        auto valid_properties = keyframe_values.resolved_properties.size();
+        dbgln("Animation {} contains {} properties to interpolate, progress = {}%", animation->id(), valid_properties, progress_in_keyframe * 100);
     }
 
-    dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Animation {} contains {} properties to interpolate, progress = {}%", name, valid_properties, progress_in_keyframe * 100);
-
-    if (fill_mode == CSS::AnimationFillMode::Forwards || fill_mode == CSS::AnimationFillMode::Both) {
-        if (!active_state_if_fill_forward)
-            active_state_if_fill_forward = make<AnimationStateSnapshot>();
-    }
-
-    UnderlyingType<PropertyID> property_id_value = 0;
-    for (auto const& property : keyframe_values.resolved_properties) {
-        auto property_id = static_cast<PropertyID>(property_id_value++);
-        if (property.has<Empty>())
-            continue;
-
+    for (auto const& it : keyframe_values.resolved_properties) {
         auto resolve_property = [&](auto& property) {
             return property.visit(
-                [](Empty) -> RefPtr<StyleValue const> { VERIFY_NOT_REACHED(); },
-                [&](AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial) {
-                    if (auto value = initial_state.state[to_underlying(property_id)])
-                        return value;
-
-                    auto value = style_properties.maybe_null_property(property_id);
-                    initial_state.state[to_underlying(property_id)] = value;
-                    return value;
+                [&](Animations::KeyframeEffect::KeyFrameSet::UseInitial) -> RefPtr<StyleValue const> {
+                    return style_properties.maybe_null_property(it.key);
                 },
                 [&](RefPtr<StyleValue const> value) { return value; });
         };
 
-        auto resolved_start_property = resolve_property(property);
+        auto resolved_start_property = resolve_property(it.value);
 
-        auto const& end_property = keyframe_end_values.resolved_properties[to_underlying(property_id)];
-        if (end_property.has<Empty>()) {
+        auto const& end_property = keyframe_end_values.resolved_properties.get(it.key);
+        if (!end_property.has_value()) {
             if (resolved_start_property) {
-                style_properties.set_property(property_id, resolved_start_property.release_nonnull());
-                dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "No end property for property {}, using {}", string_from_property_id(property_id), resolved_start_property->to_string());
+                style_properties.set_property(it.key, *resolved_start_property);
+                dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "No end property for property {}, using {}", string_from_property_id(it.key), resolved_start_property->to_string());
             }
             continue;
         }
 
-        auto resolved_end_property = resolve_property(end_property);
+        auto resolved_end_property = resolve_property(end_property.value());
 
         if (!resolved_start_property || !resolved_end_property)
             continue;
@@ -988,227 +930,13 @@ ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_prop
         auto end = resolved_end_property.release_nonnull();
 
         auto next_value = TRY(interpolate_property(*start, *end, progress_in_keyframe));
-        dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Interpolated value for property {} at {}: {} -> {} = {}", string_from_property_id(property_id), progress_in_keyframe, start->to_string(), end->to_string(), next_value->to_string());
-        style_properties.set_property(property_id, next_value);
-        if (active_state_if_fill_forward)
-            active_state_if_fill_forward->state[to_underlying(property_id)] = next_value;
+        dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Interpolated value for property {} at {}: {} -> {} = {}", string_from_property_id(it.key), progress_in_keyframe, start->to_string(), end->to_string(), next_value->to_string());
+        style_properties.set_property(it.key, next_value);
     }
 
     return {};
 }
 
-bool StyleComputer::Animation::is_done() const
-{
-    return progress.as_fraction() >= 0.9999 && iteration_count.has_value() && iteration_count.value() == 0;
-}
-
-// NOTE: Magic values from <https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease>
-static auto ease_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.25, 0.1, 0.25, 1.0 };
-static auto ease_in_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.42, 0.0, 1.0, 1.0 };
-static auto ease_out_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.0, 0.0, 0.58, 1.0 };
-static auto ease_in_out_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.42, 0.0, 0.58, 1.0 };
-
-float StyleComputer::Animation::compute_output_progress(float input_progress) const
-{
-    auto output_progress = input_progress;
-    auto going_forwards = true;
-    switch (direction) {
-    case AnimationDirection::Alternate:
-        if (current_iteration % 2 == 0) {
-            output_progress = 1.0f - output_progress;
-            going_forwards = false;
-        }
-        break;
-    case AnimationDirection::AlternateReverse:
-        if (current_iteration % 2 == 1) {
-            output_progress = 1.0f - output_progress;
-            going_forwards = false;
-        }
-        break;
-    case AnimationDirection::Normal:
-        break;
-    case AnimationDirection::Reverse:
-        output_progress = 1.0f - output_progress;
-        going_forwards = false;
-        break;
-    }
-
-    if (remaining_delay.to_milliseconds() != 0)
-        return output_progress;
-
-    return timing_function.timing_function.visit(
-        [&](AnimationTiming::Linear) { return output_progress; },
-        [&](AnimationTiming::Steps const& steps) {
-            auto before_flag = (current_state == AnimationState::Before && going_forwards) || (current_state == AnimationState::After && !going_forwards);
-            auto progress_step = output_progress * static_cast<float>(steps.number_of_steps);
-            auto current_step = floorf(progress_step);
-            if (steps.jump_at_start)
-                current_step += 1;
-            if (before_flag && truncf(progress_step) == progress_step)
-                current_step -= 1;
-            if (output_progress >= 0 && current_step < 0)
-                current_step = 0;
-            size_t jumps;
-            if (steps.jump_at_start ^ steps.jump_at_end)
-                jumps = steps.number_of_steps;
-            else if (steps.jump_at_start && steps.jump_at_end)
-                jumps = steps.number_of_steps + 1;
-            else
-                jumps = steps.number_of_steps - 1;
-
-            if (output_progress <= 1 && current_step > static_cast<float>(jumps))
-                current_step = static_cast<float>(jumps);
-            return current_step / static_cast<float>(steps.number_of_steps);
-        },
-        [&](AnimationTiming::CubicBezier const& bezier) {
-            // Special cases first:
-            if (bezier == AnimationTiming::CubicBezier { 0.0, 0.0, 1.0, 1.0 })
-                return output_progress;
-            // FIXME: This is quite inefficient on memory and CPU, find a better way to do this.
-            auto sample = bezier.sample_around(static_cast<double>(output_progress));
-            return static_cast<float>(sample.y);
-        });
-}
-
-static double cubic_bezier_at(double x1, double x2, double t)
-{
-    auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
-    auto b = 3.0 * x2 - 6.0 * x1;
-    auto c = 3.0 * x1;
-
-    auto t2 = t * t;
-    auto t3 = t2 * t;
-
-    return (a * t3) + (b * t2) + (c * t);
-}
-
-StyleComputer::AnimationTiming::CubicBezier::CachedSample StyleComputer::AnimationTiming::CubicBezier::sample_around(double x) const
-{
-    x = clamp(x, 0, 1);
-
-    auto solve = [&](auto t) {
-        auto x = cubic_bezier_at(x1, x2, t);
-        auto y = cubic_bezier_at(y1, y2, t);
-        return CachedSample { x, y, t };
-    };
-
-    if (m_cached_x_samples.is_empty())
-        m_cached_x_samples.append(solve(0.));
-
-    size_t nearby_index = 0;
-    if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
-            if (x > sample.x)
-                return 1;
-            if (x < sample.x)
-                return -1;
-            return 0;
-        }))
-        return *found;
-
-    if (nearby_index == m_cached_x_samples.size() || nearby_index + 1 == m_cached_x_samples.size()) {
-        // Produce more samples until we have enough.
-        auto last_t = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().t;
-        auto last_x = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().x;
-        while (last_x <= x) {
-            last_t += 1. / 60.;
-            auto solution = solve(last_t);
-            m_cached_x_samples.append(solution);
-            last_x = solution.x;
-        }
-
-        if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
-                if (x > sample.x)
-                    return 1;
-                if (x < sample.x)
-                    return -1;
-                return 0;
-            }))
-            return *found;
-    }
-
-    // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
-    auto& sample1 = m_cached_x_samples[nearby_index];
-    auto& sample2 = m_cached_x_samples[nearby_index + 1];
-    auto factor = (x - sample1.x) / (sample2.x - sample1.x);
-    return CachedSample {
-        x,
-        clamp(sample1.y + factor * (sample2.y - sample1.y), 0, 1),
-        sample1.t + factor * (sample2.t - sample1.t),
-    };
-}
-
-void StyleComputer::ensure_animation_timer() const
-{
-    constexpr static auto timer_delay_ms = 1000 / 60;
-    if (!m_animation_driver_timer) {
-        m_animation_driver_timer = Platform::Timer::create_repeating(timer_delay_ms, [this] {
-            // If we run out of animations, stop the timer - it'll turn back on the next time we have an active animation.
-            if (m_active_animations.is_empty()) {
-                m_animation_driver_timer->stop();
-                return;
-            }
-
-            HashTable<AnimationKey> animations_to_remove;
-            HashTable<DOM::Element*> owning_elements_to_invalidate;
-
-            for (auto& it : m_active_animations) {
-                if (!it.value->owning_element) {
-                    // The element disappeared since we last ran, just discard the animation.
-                    animations_to_remove.set(it.key);
-                    continue;
-                }
-
-                auto transition = it.value->step(CSS::Time { timer_delay_ms, CSS::Time::Type::Ms });
-                owning_elements_to_invalidate.set(it.value->owning_element);
-
-                switch (transition) {
-                case AnimationStepTransition::NoTransition:
-                    break;
-                case AnimationStepTransition::IdleOrBeforeToActive:
-                    // FIXME: Dispatch `animationstart`.
-                    break;
-                case AnimationStepTransition::IdleOrBeforeToAfter:
-                    // FIXME: Dispatch `animationstart` then `animationend`.
-                    m_finished_animations.set(it.key, move(it.value->active_state_if_fill_forward));
-                    break;
-                case AnimationStepTransition::ActiveToBefore:
-                    // FIXME: Dispatch `animationend`.
-                    m_finished_animations.set(it.key, move(it.value->active_state_if_fill_forward));
-                    break;
-                case AnimationStepTransition::ActiveToActiveChangingTheIteration:
-                    // FIXME: Dispatch `animationiteration`.
-                    break;
-                case AnimationStepTransition::ActiveToAfter:
-                    // FIXME: Dispatch `animationend`.
-                    m_finished_animations.set(it.key, move(it.value->active_state_if_fill_forward));
-                    break;
-                case AnimationStepTransition::AfterToActive:
-                    // FIXME: Dispatch `animationstart`.
-                    break;
-                case AnimationStepTransition::AfterToBefore:
-                    // FIXME: Dispatch `animationstart` then `animationend`.
-                    m_finished_animations.set(it.key, move(it.value->active_state_if_fill_forward));
-                    break;
-                case AnimationStepTransition::Cancelled:
-                    // FIXME: Dispatch `animationcancel`.
-                    m_finished_animations.set(it.key, nullptr);
-                    break;
-                }
-                if (it.value->is_done())
-                    animations_to_remove.set(it.key);
-            }
-
-            for (auto key : animations_to_remove)
-                m_active_animations.remove(key);
-
-            for (auto* element : owning_elements_to_invalidate)
-                element->set_needs_style_update(true);
-        });
-    }
-
-    m_animation_driver_timer->start();
-}
-
 // https://www.w3.org/TR/css-cascade/#cascading
 ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
 {
@@ -1230,7 +958,7 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
         did_match_any_pseudo_element_rules = true;
     }
 
-    // Then we resolve all the CSS custom pr`operties ("variables") for this element:
+    // Then we resolve all the CSS custom properties ("variables") for this element:
     TRY(cascade_custom_properties(element, pseudo_element, matching_rule_set.author_rules));
 
     // Then we apply the declarations from the matched rules in cascade order:
@@ -1261,176 +989,175 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No);
 
     // Animation declarations [css-animations-2]
-    auto get_animation_name = [&]() -> Optional<String> {
+    auto animation_name = [&]() -> Optional<String> {
         auto animation_name = style.maybe_null_property(PropertyID::AnimationName);
         if (animation_name.is_null())
             return OptionalNone {};
         if (animation_name->is_string())
             return animation_name->as_string().string_value();
         return animation_name->to_string();
-    };
-    if (auto animation_name = get_animation_name(); animation_name.has_value()) {
-        if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName)) {
-            AnimationKey animation_key {
-                .source_declaration = source_declaration,
-                .element = &element,
-            };
+    }();
 
-            if (auto finished_state = m_finished_animations.get(animation_key); finished_state.has_value()) {
-                // We've already finished going through this animation, so drop it from the active animations.
-                m_active_animations.remove(animation_key);
-                // If the animation's fill mode was set to forwards/both, we need to collect and use the final frame's styles.
-                if (*finished_state) {
-                    auto& state = (*finished_state)->state;
-                    for (size_t property_id_value = 0; property_id_value < state.size(); ++property_id_value) {
-                        if (auto& property_value = state[property_id_value])
-                            style.set_property(static_cast<PropertyID>(property_id_value), *property_value);
-                    }
+    if (animation_name.has_value()) {
+        if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName); source_declaration && source_declaration != element.cached_animation_name_source()) {
+            // This animation name is new, so we need to create a new animation for it.
+            element.set_cached_animation_name_source(source_declaration);
+
+            Optional<CSS::Time> duration;
+            if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) {
+                if (duration_value->is_time()) {
+                    duration = duration_value->as_time().time();
+                } else if (duration_value->is_identifier() && duration_value->as_identifier().id() == ValueID::Auto) {
+                    // We use empty optional to represent "auto".
+                    duration = {};
                 }
-            } else if (!animation_name->is_empty()) {
-                auto active_animation = m_active_animations.get(animation_key);
-                if (!active_animation.has_value()) {
-                    // New animation!
-                    Optional<CSS::Time> duration;
-                    if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) {
-                        if (duration_value->is_time()) {
-                            duration = duration_value->as_time().time();
-                        } else if (duration_value->is_identifier() && duration_value->as_identifier().id() == ValueID::Auto) {
-                            // We use empty optional to represent "auto".
-                            duration = {};
-                        }
-                    }
+            }
 
-                    CSS::Time delay { 0, CSS::Time::Type::S };
-                    if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time())
-                        delay = delay_value->as_time().time();
+            CSS::Time delay { 0, CSS::Time::Type::S };
+            if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time())
+                delay = delay_value->as_time().time();
 
-                    Optional<size_t> iteration_count = 1;
-                    if (auto iteration_count_value = style.maybe_null_property(PropertyID::AnimationIterationCount); iteration_count_value) {
-                        if (iteration_count_value->is_identifier() && iteration_count_value->to_identifier() == ValueID::Infinite)
-                            iteration_count = {};
-                        else if (iteration_count_value->is_number())
-                            iteration_count = static_cast<size_t>(iteration_count_value->as_number().number());
-                    }
+            double iteration_count = 1.0;
+            if (auto iteration_count_value = style.maybe_null_property(PropertyID::AnimationIterationCount); iteration_count_value) {
+                if (iteration_count_value->is_identifier() && iteration_count_value->to_identifier() == ValueID::Infinite)
+                    iteration_count = HUGE_VAL;
+                else if (iteration_count_value->is_number())
+                    iteration_count = iteration_count_value->as_number().number();
+            }
 
-                    CSS::AnimationFillMode fill_mode { CSS::AnimationFillMode::None };
-                    if (auto fill_mode_property = style.maybe_null_property(PropertyID::AnimationFillMode); fill_mode_property && fill_mode_property->is_identifier()) {
-                        if (auto fill_mode_value = value_id_to_animation_fill_mode(fill_mode_property->to_identifier()); fill_mode_value.has_value())
-                            fill_mode = *fill_mode_value;
-                    }
+            CSS::AnimationFillMode fill_mode { CSS::AnimationFillMode::None };
+            if (auto fill_mode_property = style.maybe_null_property(PropertyID::AnimationFillMode); fill_mode_property && fill_mode_property->is_identifier()) {
+                if (auto fill_mode_value = value_id_to_animation_fill_mode(fill_mode_property->to_identifier()); fill_mode_value.has_value())
+                    fill_mode = *fill_mode_value;
+            }
 
-                    CSS::AnimationDirection direction { CSS::AnimationDirection::Normal };
-                    if (auto direction_property = style.maybe_null_property(PropertyID::AnimationDirection); direction_property && direction_property->is_identifier()) {
-                        if (auto direction_value = value_id_to_animation_direction(direction_property->to_identifier()); direction_value.has_value())
-                            direction = *direction_value;
-                    }
+            CSS::AnimationDirection direction { CSS::AnimationDirection::Normal };
+            if (auto direction_property = style.maybe_null_property(PropertyID::AnimationDirection); direction_property && direction_property->is_identifier()) {
+                if (auto direction_value = value_id_to_animation_direction(direction_property->to_identifier()); direction_value.has_value())
+                    direction = *direction_value;
+            }
 
-                    AnimationTiming timing_function { ease_timing_function };
-                    if (auto timing_property = style.maybe_null_property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) {
-                        auto& easing_value = timing_property->as_easing();
-                        switch (easing_value.easing_function()) {
-                        case EasingFunction::Linear:
-                            timing_function = AnimationTiming { AnimationTiming::Linear {} };
-                            break;
-                        case EasingFunction::Ease:
-                            timing_function = AnimationTiming { ease_timing_function };
-                            break;
-                        case EasingFunction::EaseIn:
-                            timing_function = AnimationTiming { ease_in_timing_function };
-                            break;
-                        case EasingFunction::EaseOut:
-                            timing_function = AnimationTiming { ease_out_timing_function };
-                            break;
-                        case EasingFunction::EaseInOut:
-                            timing_function = AnimationTiming { ease_in_out_timing_function };
-                            break;
-                        case EasingFunction::CubicBezier: {
-                            auto values = easing_value.values();
-                            timing_function = AnimationTiming {
-                                AnimationTiming::CubicBezier {
-                                    values[0]->as_number().number(),
-                                    values[1]->as_number().number(),
-                                    values[2]->as_number().number(),
-                                    values[3]->as_number().number(),
-                                },
-                            };
+            Animations::TimingFunction timing_function = Animations::ease_timing_function;
+            if (auto timing_property = style.maybe_null_property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) {
+                auto& easing_value = timing_property->as_easing();
+                switch (easing_value.easing_function()) {
+                case EasingFunction::Linear:
+                    timing_function = Animations::linear_timing_function;
+                    break;
+                case EasingFunction::Ease:
+                    timing_function = Animations::ease_timing_function;
+                    break;
+                case EasingFunction::EaseIn:
+                    timing_function = Animations::ease_in_timing_function;
+                    break;
+                case EasingFunction::EaseOut:
+                    timing_function = Animations::ease_out_timing_function;
+                    break;
+                case EasingFunction::EaseInOut:
+                    timing_function = Animations::ease_in_out_timing_function;
+                    break;
+                case EasingFunction::CubicBezier: {
+                    auto values = easing_value.values();
+                    timing_function = {
+                        Animations::CubicBezierTimingFunction {
+                            values[0]->as_number().number(),
+                            values[1]->as_number().number(),
+                            values[2]->as_number().number(),
+                            values[3]->as_number().number(),
+                        },
+                    };
+                    break;
+                }
+                case EasingFunction::Steps: {
+                    auto values = easing_value.values();
+                    auto jump_at_start = false;
+                    auto jump_at_end = true;
+
+                    if (values.size() > 1) {
+                        auto identifier = values[1]->to_identifier();
+                        switch (identifier) {
+                        case ValueID::JumpStart:
+                        case ValueID::Start:
+                            jump_at_start = true;
+                            jump_at_end = false;
                             break;
-                        }
-                        case EasingFunction::Steps: {
-                            auto values = easing_value.values();
-                            auto jump_at_start = false;
-                            auto jump_at_end = true;
-
-                            if (values.size() > 1) {
-                                auto identifier = values[1]->to_identifier();
-                                switch (identifier) {
-                                case ValueID::JumpStart:
-                                case ValueID::Start:
-                                    jump_at_start = true;
-                                    jump_at_end = false;
-                                    break;
-                                case ValueID::JumpEnd:
-                                case ValueID::End:
-                                    jump_at_start = false;
-                                    jump_at_end = true;
-                                    break;
-                                case ValueID::JumpNone:
-                                    jump_at_start = false;
-                                    jump_at_end = false;
-                                    break;
-                                default:
-                                    break;
-                                }
-                            }
-
-                            timing_function = AnimationTiming { AnimationTiming::Steps {
-                                .number_of_steps = static_cast<size_t>(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)),
-                                .jump_at_start = jump_at_start,
-                                .jump_at_end = jump_at_end,
-                            } };
+                        case ValueID::JumpEnd:
+                        case ValueID::End:
+                            jump_at_start = false;
+                            jump_at_end = true;
                             break;
-                        }
-                        case EasingFunction::StepEnd:
-                            timing_function = AnimationTiming { AnimationTiming::Steps {
-                                .number_of_steps = 1,
-                                .jump_at_start = false,
-                                .jump_at_end = true,
-                            } };
+                        case ValueID::JumpNone:
+                            jump_at_start = false;
+                            jump_at_end = false;
                             break;
-                        case EasingFunction::StepStart:
-                            timing_function = AnimationTiming { AnimationTiming::Steps {
-                                .number_of_steps = 1,
-                                .jump_at_start = true,
-                                .jump_at_end = false,
-                            } };
+                        default:
                             break;
                         }
                     }
 
-                    auto animation = make<Animation>(Animation {
-                        .name = animation_name.release_value(),
-                        .duration = duration,
-                        .delay = delay,
-                        .iteration_count = iteration_count,
-                        .timing_function = timing_function,
-                        .direction = direction,
-                        .fill_mode = fill_mode,
-                        .owning_element = TRY(element.try_make_weak_ptr<DOM::Element>()),
-                        .progress = CSS::Percentage(0),
-                        .remaining_delay = delay,
-                    });
-                    active_animation = animation;
-                    m_active_animations.set(animation_key, move(animation));
+                    timing_function = Animations::TimingFunction { Animations::StepsTimingFunction {
+                        .number_of_steps = static_cast<size_t>(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)),
+                        .jump_at_start = jump_at_start,
+                        .jump_at_end = jump_at_end,
+                    } };
+                    break;
+                }
+                case EasingFunction::StepEnd:
+                    timing_function = Animations::TimingFunction { Animations::StepsTimingFunction {
+                        .number_of_steps = 1,
+                        .jump_at_start = false,
+                        .jump_at_end = true,
+                    } };
+                    break;
+                case EasingFunction::StepStart:
+                    timing_function = Animations::TimingFunction { Animations::StepsTimingFunction {
+                        .number_of_steps = 1,
+                        .jump_at_start = true,
+                        .jump_at_end = false,
+                    } };
+                    break;
                 }
-
-                TRY((*active_animation)->collect_into(style, rule_cache_for_cascade_origin(CascadeOrigin::Author)));
-            } else {
-                m_active_animations.remove(animation_key);
             }
+
+            auto& realm = element.realm();
+
+            auto effect = Animations::KeyframeEffect::create(realm);
+            auto iteration_duration = duration.has_value()
+                ? Variant<double, String> { duration.release_value().to_milliseconds() }
+                : "auto"_string;
+            effect->set_iteration_duration(iteration_duration);
+            effect->set_start_delay(delay.to_milliseconds());
+            effect->set_iteration_count(iteration_count);
+            effect->set_timing_function(move(timing_function));
+            effect->set_fill_mode(Animations::css_fill_mode_to_bindings_fill_mode(fill_mode));
+            effect->set_playback_direction(Animations::css_animation_direction_to_bindings_playback_direction(direction));
+
+            auto animation = CSSAnimation::create(realm);
+            animation->set_id(animation_name.release_value());
+            animation->set_timeline(m_document->timeline());
+            animation->set_owning_element(element);
+            animation->set_effect(effect);
+
+            auto const& rule_cache = rule_cache_for_cascade_origin(CascadeOrigin::Author);
+            if (auto keyframe_set = rule_cache.rules_by_animation_keyframes.get(animation->id()); keyframe_set.has_value())
+                effect->set_key_frame_set(keyframe_set.value());
+
+            element.associate_with_effect(effect);
+
+            HTML::TemporaryExecutionContext context(m_document->relevant_settings_object());
+            animation->play().release_value_but_fixme_should_propagate_errors();
         }
+    }
+
+    auto animations = element.get_animations({ .subtree = false });
+    for (auto& animation : animations) {
+        if (!animation->is_relevant())
+            continue;
 
-        if (!m_active_animations.is_empty())
-            ensure_animation_timer();
+        if (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) {
+            auto& keyframe_effect = *static_cast<Animations::KeyframeEffect*>(effect.ptr());
+            TRY(collect_animation_into(keyframe_effect, style));
+        }
     }
 
     // Important author declarations
@@ -2235,88 +1962,36 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
             ++rule_index;
         });
 
+        // Loosely based on https://drafts.csswg.org/css-animations-2/#keyframe-processing
         sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) {
-            auto keyframe_set = make<AnimationKeyFrameSet>();
-            AnimationKeyFrameSet::ResolvedKeyFrame resolved_keyframe;
+            auto keyframe_set = adopt_ref(*new Animations::KeyframeEffect::KeyFrameSet);
+            HashTable<PropertyID> animated_properties;
 
             // Forwards pass, resolve all the user-specified keyframe properties.
             for (auto const& keyframe : rule.keyframes()) {
-                auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
-                auto keyframe_rule = keyframe->style();
-
-                if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
-                    continue;
-
-                auto current_keyframe = resolved_keyframe;
-                auto& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule);
-                for (auto& property : keyframe_style.properties())
-                    current_keyframe.resolved_properties[to_underlying(property.property_id)] = property.value;
-
-                resolved_keyframe = move(current_keyframe);
-                keyframe_set->keyframes_by_key.insert(key, resolved_keyframe);
-            }
+                Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame resolved_keyframe;
 
-            // If there is no 'from' keyframe, make a synthetic one.
-            auto made_a_synthetic_from_keyframe = false;
-            if (!keyframe_set->keyframes_by_key.find(0)) {
-                keyframe_set->keyframes_by_key.insert(0, AnimationKeyFrameSet::ResolvedKeyFrame());
-                made_a_synthetic_from_keyframe = true;
-            }
-
-            // Backwards pass, resolve all the implied properties, go read <https://drafts.csswg.org/css-animations-2/#keyframe-processing> to see why.
-            auto first = true;
-            for (auto const& keyframe : rule.keyframes().in_reverse()) {
-                auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
+                auto key = static_cast<u64>(keyframe->key().value() * Animations::KeyframeEffect::AnimationKeyFrameKeyScaleFactor);
                 auto keyframe_rule = keyframe->style();
 
                 if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
                     continue;
 
-                // The last keyframe is already fully resolved.
-                if (first) {
-                    first = false;
-                    continue;
-                }
-
-                auto next_keyframe = resolved_keyframe;
-                auto& current_keyframes = *keyframe_set->keyframes_by_key.find(key);
-
-                for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
-                    auto& current_property = current_keyframes.resolved_properties[it.index()];
-                    if (!current_property.has<Empty>() || it->has<Empty>())
-                        continue;
-
-                    if (key == 0)
-                        current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
-                    else
-                        current_property = *it;
+                auto const& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule);
+                for (auto const& property : keyframe_style.properties()) {
+                    animated_properties.set(property.property_id);
+                    resolved_keyframe.resolved_properties.set(property.property_id, property.value);
                 }
 
-                resolved_keyframe = current_keyframes;
+                keyframe_set->keyframes_by_key.insert(key, resolved_keyframe);
             }
 
-            if (made_a_synthetic_from_keyframe && !first) {
-                auto next_keyframe = resolved_keyframe;
-                auto& current_keyframes = *keyframe_set->keyframes_by_key.find(0);
-
-                for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
-                    auto& current_property = current_keyframes.resolved_properties[it.index()];
-                    if (!current_property.has<Empty>() || it->has<Empty>())
-                        continue;
-                    current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
-                }
-
-                resolved_keyframe = current_keyframes;
-            }
+            Animations::KeyframeEffect::generate_initial_and_final_frames(keyframe_set, animated_properties);
 
             if constexpr (LIBWEB_CSS_DEBUG) {
                 dbgln("Resolved keyframe set '{}' into {} keyframes:", rule.name(), keyframe_set->keyframes_by_key.size());
-                for (auto it = keyframe_set->keyframes_by_key.begin(); it != keyframe_set->keyframes_by_key.end(); ++it) {
-                    size_t props = 0;
-                    for (auto& entry : it->resolved_properties)
-                        props += !entry.has<Empty>();
-                    dbgln("    - keyframe {}: {} properties", it.key(), props);
-                }
+                for (auto it = keyframe_set->keyframes_by_key.begin(); it != keyframe_set->keyframes_by_key.end(); ++it)
+                    dbgln("    - keyframe {}: {} properties", it.key(), it->resolved_properties.size());
             }
 
             rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set));

+ 3 - 111
Userland/Libraries/LibWeb/CSS/StyleComputer.h

@@ -10,7 +10,7 @@
 #include <AK/HashMap.h>
 #include <AK/Optional.h>
 #include <AK/OwnPtr.h>
-#include <AK/RedBlackTree.h>
+#include <LibWeb/Animations/KeyframeEffect.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
 #include <LibWeb/CSS/CSSKeyframesRule.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
@@ -73,42 +73,6 @@ public:
 
     RefPtr<Gfx::FontCascadeList const> compute_font_for_style_values(DOM::Element const* element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, StyleValue const& font_family, StyleValue const& font_size, StyleValue const& font_style, StyleValue const& font_weight, StyleValue const& font_stretch, int math_depth = 0) const;
 
-    struct AnimationKey {
-        CSS::CSSStyleDeclaration const* source_declaration;
-        DOM::Element const* element;
-    };
-
-    struct AnimationTiming {
-        struct Linear { };
-        struct CubicBezier {
-            // Regular parameters
-            double x1;
-            double y1;
-            double x2;
-            double y2;
-
-            struct CachedSample {
-                double x;
-                double y;
-                double t;
-            };
-            mutable Vector<CachedSample, 64> m_cached_x_samples = {};
-
-            CachedSample sample_around(double x) const;
-            bool operator==(CubicBezier const& other) const
-            {
-                return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
-            }
-        };
-        struct Steps {
-            size_t number_of_steps;
-            bool jump_at_start;
-            bool jump_at_end;
-        };
-
-        Variant<Linear, CubicBezier, Steps> timing_function;
-    };
-
     void set_viewport_rect(Badge<DOM::Document>, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; }
 
 private:
@@ -156,28 +120,20 @@ private:
 
     JS::NonnullGCPtr<DOM::Document> m_document;
 
-    struct AnimationKeyFrameSet {
-        struct ResolvedKeyFrame {
-            struct UseInitial { };
-            Array<Variant<Empty, UseInitial, NonnullRefPtr<StyleValue const>>, to_underlying(last_property_id) + 1> resolved_properties {};
-        };
-        RedBlackTree<u64, ResolvedKeyFrame> keyframes_by_key;
-    };
-
     struct RuleCache {
         HashMap<FlyString, Vector<MatchingRule>> rules_by_id;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_class;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
         Vector<MatchingRule> other_rules;
 
-        HashMap<FlyString, NonnullOwnPtr<AnimationKeyFrameSet>> rules_by_animation_keyframes;
+        HashMap<FlyString, NonnullRefPtr<Animations::KeyframeEffect::KeyFrameSet>> rules_by_animation_keyframes;
     };
 
     NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin);
 
     RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;
 
-    void ensure_animation_timer() const;
+    ErrorOr<void> collect_animation_into(JS::NonnullGCPtr<Animations::KeyframeEffect> animation, StyleProperties& style_properties) const;
 
     OwnPtr<RuleCache> m_author_rule_cache;
     OwnPtr<RuleCache> m_user_rule_cache;
@@ -190,71 +146,7 @@ private:
     Length::FontMetrics m_default_font_metrics;
     Length::FontMetrics m_root_element_font_metrics;
 
-    constexpr static u64 AnimationKeyFrameKeyScaleFactor = 1000; // 0..100000
-
-    enum class AnimationStepTransition {
-        NoTransition,
-        IdleOrBeforeToActive,
-        IdleOrBeforeToAfter,
-        ActiveToBefore,
-        ActiveToActiveChangingTheIteration,
-        ActiveToAfter,
-        AfterToActive,
-        AfterToBefore,
-        Cancelled,
-    };
-    enum class AnimationState {
-        Before,
-        After,
-        Idle,
-        Active,
-    };
-
-    struct AnimationStateSnapshot {
-        Array<RefPtr<StyleValue const>, to_underlying(last_property_id) + 1> state;
-    };
-
-    struct Animation {
-        String name;
-        Optional<CSS::Time> duration; // "auto" if not set.
-        CSS::Time delay;
-        Optional<size_t> iteration_count; // Infinite if not set.
-        AnimationTiming timing_function;
-        CSS::AnimationDirection direction;
-        CSS::AnimationFillMode fill_mode;
-        WeakPtr<DOM::Element> owning_element;
-
-        CSS::Percentage progress { 0 };
-        CSS::Time remaining_delay { 0, CSS::Time::Type::Ms };
-        AnimationState current_state { AnimationState::Before };
-        size_t current_iteration { 1 };
-
-        mutable AnimationStateSnapshot initial_state {};
-        mutable OwnPtr<AnimationStateSnapshot> active_state_if_fill_forward {};
-
-        AnimationStepTransition step(CSS::Time const& time_step);
-        ErrorOr<void> collect_into(StyleProperties&, RuleCache const&) const;
-        bool is_done() const;
-
-    private:
-        float compute_output_progress(float input_progress) const;
-        bool is_animating_backwards() const;
-    };
-
-    mutable HashMap<AnimationKey, NonnullOwnPtr<Animation>> m_active_animations;
-    mutable HashMap<AnimationKey, OwnPtr<AnimationStateSnapshot>> m_finished_animations; // If fill-mode is forward/both, this is non-null and contains the final state.
-    mutable RefPtr<Platform::Timer> m_animation_driver_timer;
-
     CSSPixelRect m_viewport_rect;
 };
 
 }
-
-template<>
-struct AK::Traits<Web::CSS::StyleComputer::AnimationKey> : public AK::DefaultTraits<Web::CSS::StyleComputer::AnimationKey> {
-    static unsigned hash(Web::CSS::StyleComputer::AnimationKey const& k) { return pair_int_hash(ptr_hash(k.source_declaration), ptr_hash(k.element)); }
-    static bool equals(Web::CSS::StyleComputer::AnimationKey const& a, Web::CSS::StyleComputer::AnimationKey const& b)
-    {
-        return a.element == b.element && a.source_declaration == b.source_declaration;
-    }
-};

+ 44 - 0
Userland/Libraries/LibWeb/DOM/Document.cpp

@@ -23,6 +23,8 @@
 #include <LibWeb/Animations/AnimationTimeline.h>
 #include <LibWeb/Animations/DocumentTimeline.h>
 #include <LibWeb/Bindings/MainThreadVM.h>
+#include <LibWeb/CSS/AnimationEvent.h>
+#include <LibWeb/CSS/CSSAnimation.h>
 #include <LibWeb/CSS/MediaQueryList.h>
 #include <LibWeb/CSS/MediaQueryListEvent.h>
 #include <LibWeb/CSS/StyleComputer.h>
@@ -1889,6 +1891,19 @@ Element* Document::find_a_potential_indicated_element(FlyString const& fragment)
     return nullptr;
 }
 
+// https://www.w3.org/TR/css-animations-2/#event-dispatch
+void Document::dispatch_events_for_animation_if_necessary(JS::NonnullGCPtr<Animations::Animation> animation)
+{
+    // Each time a new animation frame is established and the animation does not have a pending play task or pending
+    // pause task, the events to dispatch are determined by comparing the animation’s phase before and after
+    // establishing the new animation frame as follows:
+    auto effect = animation->effect();
+    if (!effect || !effect->is_keyframe_effect() || !animation->is_css_animation() || animation->pending())
+        return;
+
+    // TODO: Dispatch events
+}
+
 // https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier
 void Document::scroll_to_the_fragment()
 {
@@ -3908,6 +3923,35 @@ void Document::remove_replaced_animations()
     }
 }
 
+void Document::ensure_animation_timer()
+{
+    constexpr static auto timer_delay_ms = 1000 / 60;
+    if (!m_animation_driver_timer) {
+        m_animation_driver_timer = Platform::Timer::create_repeating(timer_delay_ms, [this] {
+            bool has_animations = false;
+            for (auto& timeline : m_associated_animation_timelines) {
+                if (!timeline->associated_animations().is_empty()) {
+                    has_animations = true;
+                    break;
+                }
+            }
+            if (!has_animations) {
+                m_animation_driver_timer->stop();
+                return;
+            }
+
+            update_animations_and_send_events(MonotonicTime::now().milliseconds());
+
+            for (auto& timeline : m_associated_animation_timelines) {
+                for (auto& animation : timeline->associated_animations())
+                    dispatch_events_for_animation_if_necessary(animation);
+            }
+        });
+    }
+
+    m_animation_driver_timer->start();
+}
+
 // https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
 static bool is_potentially_named_element(DOM::Element const& element)
 {

+ 4 - 0
Userland/Libraries/LibWeb/DOM/Document.h

@@ -561,6 +561,7 @@ public:
     void append_pending_animation_event(PendingAnimationEvent const&);
     void update_animations_and_send_events(Optional<double> const& timestamp);
     void remove_replaced_animations();
+    void ensure_animation_timer();
 
     bool ready_to_run_scripts() const { return m_ready_to_run_scripts; }
 
@@ -620,6 +621,8 @@ private:
 
     Element* find_a_potential_indicated_element(FlyString const& fragment) const;
 
+    void dispatch_events_for_animation_if_necessary(JS::NonnullGCPtr<Animations::Animation>);
+
     JS::NonnullGCPtr<Page> m_page;
     OwnPtr<CSS::StyleComputer> m_style_computer;
     JS::GCPtr<CSS::StyleSheetList> m_style_sheets;
@@ -815,6 +818,7 @@ private:
 
     // https://www.w3.org/TR/web-animations-1/#pending-animation-event-queue
     Vector<PendingAnimationEvent> m_pending_animation_event_queue;
+    RefPtr<Platform::Timer> m_animation_driver_timer;
 
     bool m_needs_to_call_page_did_load { false };
 

+ 2 - 1
Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp

@@ -195,7 +195,8 @@ void EventLoop::process()
         document.evaluate_media_queries_and_report_changes();
     });
 
-    // FIXME:     10. For each fully active Document in docs, update animations and send events for that Document, passing in now as the timestamp. [WEBANIMATIONS]
+    // 10. For each fully active Document in docs, update animations and send events for that Document, passing in now as the timestamp. [WEBANIMATIONS]
+    // Note: This is handled by the document's animation timer
 
     // FIXME:     11. For each fully active Document in docs, run the fullscreen steps for that Document, passing in now as the timestamp. [FULLSCREEN]