From ae3326a447b715ed3fc01fea334b5edafaf00adf Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Thu, 22 Feb 2024 13:56:15 +0000 Subject: [PATCH] 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. --- .../Libraries/LibWeb/Animations/Animatable.h | 4 + .../Libraries/LibWeb/Animations/Animation.cpp | 3 + .../Libraries/LibWeb/CSS/StyleComputer.cpp | 745 +++++------------- Userland/Libraries/LibWeb/CSS/StyleComputer.h | 114 +-- Userland/Libraries/LibWeb/DOM/Document.cpp | 44 ++ Userland/Libraries/LibWeb/DOM/Document.h | 4 + .../LibWeb/HTML/EventLoop/EventLoop.cpp | 3 +- 7 files changed, 270 insertions(+), 647 deletions(-) diff --git a/Userland/Libraries/LibWeb/Animations/Animatable.h b/Userland/Libraries/LibWeb/Animations/Animatable.h index 6b5d5c79a8..b093361e11 100644 --- a/Userland/Libraries/LibWeb/Animations/Animatable.h +++ b/Userland/Libraries/LibWeb/Animations/Animatable.h @@ -33,9 +33,13 @@ public: void associate_with_effect(JS::NonnullGCPtr effect); void disassociate_with_effect(JS::NonnullGCPtr effect); + JS::GCPtr cached_animation_name_source() const { return m_cached_animation_name_source; } + void set_cached_animation_name_source(JS::GCPtr value) { m_cached_animation_name_source = value; } + private: Vector> m_associated_effects; bool m_is_sorted_by_composite_order { true }; + JS::GCPtr m_cached_animation_name_source; }; } diff --git a/Userland/Libraries/LibWeb/Animations/Animation.cpp b/Userland/Libraries/LibWeb/Animations/Animation.cpp index 523c463209..da0d8ee050 100644 --- a/Userland/Libraries/LibWeb/Animations/Animation.cpp +++ b/Userland/Libraries/LibWeb/Animations/Animation.cpp @@ -370,6 +370,9 @@ WebIDL::ExceptionOr Animation::play() // https://www.w3.org/TR/web-animations-1/#play-an-animation WebIDL::ExceptionOr 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; diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 6d5c2637a8..5b8b8de5be 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021-2023, Sam Atkins + * Copyright (c) 2024, Matthew Olsson * * SPDX-License-Identifier: BSD-2-Clause */ @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +24,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -57,11 +64,14 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include namespace AK { @@ -743,46 +753,6 @@ static ErrorOr cascade_custom_properties(DOM::Element& element, Optional 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> interpolate_property(StyleValue const& from, StyleValue const& to, float delta) { if (from.type() != to.type()) { @@ -878,31 +848,23 @@ static ErrorOr> interpolate_property(StyleValue const& } } -bool StyleComputer::Animation::is_animating_backwards() const +ErrorOr StyleComputer::collect_animation_into(JS::NonnullGCPtr 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 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 matching_keyframes = rule_cache.rules_by_animation_keyframes.get(name); - if (!matching_keyframes.has_value()) + auto animation = effect->associated_animation(); + if (!animation) return {}; - auto& keyframes = matching_keyframes.value()->keyframes_by_key; + auto output_progress = effect->transformed_progress(); + if (!output_progress.has_value()) + return {}; - auto output_progress = compute_output_progress(progress.as_fraction()) * 100.f; - auto is_backwards = is_animating_backwards(); + if (!effect->key_frame_set()) + return {}; - auto key = static_cast(output_progress * AnimationKeyFrameKeyScaleFactor); + auto& keyframes = effect->key_frame_set()->keyframes_by_key; + auto is_backwards = effect->current_direction() == Animations::AnimationDirection::Backwards; + + auto key = static_cast(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 StyleComputer::Animation::collect_into(StyleProperties& style_prop : static_cast(key - keyframe_start) / static_cast(keyframe_end - keyframe_start); }(); - auto valid_properties = 0; - for (auto const& property : keyframe_values.resolved_properties) { - if (property.has()) - 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(); - } - - UnderlyingType property_id_value = 0; - for (auto const& property : keyframe_values.resolved_properties) { - auto property_id = static_cast(property_id_value++); - if (property.has()) - continue; - + for (auto const& it : keyframe_values.resolved_properties) { auto resolve_property = [&](auto& property) { return property.visit( - [](Empty) -> RefPtr { 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 { + return style_properties.maybe_null_property(it.key); }, [&](RefPtr 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()) { + 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 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 -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(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(jumps)) - current_step = static_cast(jumps); - return current_step / static_cast(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(output_progress)); - return static_cast(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 animations_to_remove; - HashTable 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 StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const { @@ -1230,7 +958,7 @@ ErrorOr 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 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 { + auto animation_name = [&]() -> Optional { 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(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 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 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(); - - Optional 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(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::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(), - }, - }; - 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(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 = AnimationTiming { AnimationTiming::Steps { - .number_of_steps = 1, - .jump_at_start = false, - .jump_at_end = true, - } }; - break; - case EasingFunction::StepStart: - timing_function = AnimationTiming { AnimationTiming::Steps { - .number_of_steps = 1, - .jump_at_start = true, - .jump_at_end = false, - } }; - break; - } - } - - auto animation = make(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()), - .progress = CSS::Percentage(0), - .remaining_delay = delay, - }); - active_animation = animation; - m_active_animations.set(animation_key, move(animation)); - } - - TRY((*active_animation)->collect_into(style, rule_cache_for_cascade_origin(CascadeOrigin::Author))); - } else { - m_active_animations.remove(animation_key); } - } - if (!m_active_animations.is_empty()) - ensure_animation_timer(); + 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(); + + 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::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; + } + + 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 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 = Animations::TimingFunction { Animations::StepsTimingFunction { + .number_of_steps = static_cast(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; + } + } + + auto& realm = element.realm(); + + auto effect = Animations::KeyframeEffect::create(realm); + auto iteration_duration = duration.has_value() + ? Variant { 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 (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) { + auto& keyframe_effect = *static_cast(effect.ptr()); + TRY(collect_animation_into(keyframe_effect, style)); + } } // Important author declarations @@ -2235,88 +1962,36 @@ NonnullOwnPtr 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::ResolvedKeyFrame resolved_keyframe; + auto keyframe_set = adopt_ref(*new Animations::KeyframeEffect::KeyFrameSet); + HashTable animated_properties; // Forwards pass, resolve all the user-specified keyframe properties. for (auto const& keyframe : rule.keyframes()) { - auto key = static_cast(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor); + Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame resolved_keyframe; + + auto key = static_cast(keyframe->key().value() * Animations::KeyframeEffect::AnimationKeyFrameKeyScaleFactor); auto keyframe_rule = keyframe->style(); if (!is(*keyframe_rule)) continue; - auto current_keyframe = resolved_keyframe; - auto& keyframe_style = static_cast(*keyframe_rule); - for (auto& property : keyframe_style.properties()) - current_keyframe.resolved_properties[to_underlying(property.property_id)] = property.value; + auto const& keyframe_style = static_cast(*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 = move(current_keyframe); keyframe_set->keyframes_by_key.insert(key, 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 to see why. - auto first = true; - for (auto const& keyframe : rule.keyframes().in_reverse()) { - auto key = static_cast(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor); - auto keyframe_rule = keyframe->style(); - - if (!is(*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() || it->has()) - continue; - - if (key == 0) - current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial(); - else - current_property = *it; - } - - resolved_keyframe = current_keyframes; - } - - 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() || it->has()) - 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(); - 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)); diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h index 9c5a92157f..9f0d37730e 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include @@ -73,42 +73,6 @@ public: RefPtr compute_font_for_style_values(DOM::Element const* element, Optional 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 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 timing_function; - }; - void set_viewport_rect(Badge, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; } private: @@ -156,28 +120,20 @@ private: JS::NonnullGCPtr m_document; - struct AnimationKeyFrameSet { - struct ResolvedKeyFrame { - struct UseInitial { }; - Array>, to_underlying(last_property_id) + 1> resolved_properties {}; - }; - RedBlackTree keyframes_by_key; - }; - struct RuleCache { HashMap> rules_by_id; HashMap> rules_by_class; HashMap> rules_by_tag_name; Vector other_rules; - HashMap> rules_by_animation_keyframes; + HashMap> rules_by_animation_keyframes; }; NonnullOwnPtr make_rule_cache_for_cascade_origin(CascadeOrigin); RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const; - void ensure_animation_timer() const; + ErrorOr collect_animation_into(JS::NonnullGCPtr animation, StyleProperties& style_properties) const; OwnPtr m_author_rule_cache; OwnPtr 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, to_underlying(last_property_id) + 1> state; - }; - - struct Animation { - String name; - Optional duration; // "auto" if not set. - CSS::Time delay; - Optional iteration_count; // Infinite if not set. - AnimationTiming timing_function; - CSS::AnimationDirection direction; - CSS::AnimationFillMode fill_mode; - WeakPtr 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 active_state_if_fill_forward {}; - - AnimationStepTransition step(CSS::Time const& time_step); - ErrorOr 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> m_active_animations; - mutable HashMap> m_finished_animations; // If fill-mode is forward/both, this is non-null and contains the final state. - mutable RefPtr m_animation_driver_timer; - CSSPixelRect m_viewport_rect; }; } - -template<> -struct AK::Traits : public AK::DefaultTraits { - 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; - } -}; diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index bcacebf986..48377658e6 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 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) { diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 15757ed1fc..bc53f0ee3e 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/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 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); + JS::NonnullGCPtr m_page; OwnPtr m_style_computer; JS::GCPtr m_style_sheets; @@ -815,6 +818,7 @@ private: // https://www.w3.org/TR/web-animations-1/#pending-animation-event-queue Vector m_pending_animation_event_queue; + RefPtr m_animation_driver_timer; bool m_needs_to_call_page_did_load { false }; diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index a4ae628dee..85442591d6 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/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]