mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 17:52:45 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
							parent
							
								
									b258ba2767
								
							
						
					
					
						commit
						ae3326a447
					
				
					 7 changed files with 270 additions and 647 deletions
				
			
		|  | @ -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; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 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<u64>(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<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,36 +989,20 @@ 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 (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); | ||||
| 
 | ||||
|             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); | ||||
|                     } | ||||
|                 } | ||||
|             } 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()) { | ||||
|  | @ -1305,12 +1017,12 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | |||
|             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; | ||||
|             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 = {}; | ||||
|                     iteration_count = HUGE_VAL; | ||||
|                 else if (iteration_count_value->is_number()) | ||||
|                             iteration_count = static_cast<size_t>(iteration_count_value->as_number().number()); | ||||
|                     iteration_count = iteration_count_value->as_number().number(); | ||||
|             } | ||||
| 
 | ||||
|             CSS::AnimationFillMode fill_mode { CSS::AnimationFillMode::None }; | ||||
|  | @ -1325,29 +1037,29 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | |||
|                     direction = *direction_value; | ||||
|             } | ||||
| 
 | ||||
|                     AnimationTiming timing_function { ease_timing_function }; | ||||
|             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 = AnimationTiming { AnimationTiming::Linear {} }; | ||||
|                     timing_function = Animations::linear_timing_function; | ||||
|                     break; | ||||
|                 case EasingFunction::Ease: | ||||
|                             timing_function = AnimationTiming { ease_timing_function }; | ||||
|                     timing_function = Animations::ease_timing_function; | ||||
|                     break; | ||||
|                 case EasingFunction::EaseIn: | ||||
|                             timing_function = AnimationTiming { ease_in_timing_function }; | ||||
|                     timing_function = Animations::ease_in_timing_function; | ||||
|                     break; | ||||
|                 case EasingFunction::EaseOut: | ||||
|                             timing_function = AnimationTiming { ease_out_timing_function }; | ||||
|                     timing_function = Animations::ease_out_timing_function; | ||||
|                     break; | ||||
|                 case EasingFunction::EaseInOut: | ||||
|                             timing_function = AnimationTiming { ease_in_out_timing_function }; | ||||
|                     timing_function = Animations::ease_in_out_timing_function; | ||||
|                     break; | ||||
|                 case EasingFunction::CubicBezier: { | ||||
|                     auto values = easing_value.values(); | ||||
|                             timing_function = AnimationTiming { | ||||
|                                 AnimationTiming::CubicBezier { | ||||
|                     timing_function = { | ||||
|                         Animations::CubicBezierTimingFunction { | ||||
|                             values[0]->as_number().number(), | ||||
|                             values[1]->as_number().number(), | ||||
|                             values[2]->as_number().number(), | ||||
|  | @ -1383,7 +1095,7 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | |||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                             timing_function = AnimationTiming { AnimationTiming::Steps { | ||||
|                     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, | ||||
|  | @ -1391,14 +1103,14 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | |||
|                     break; | ||||
|                 } | ||||
|                 case EasingFunction::StepEnd: | ||||
|                             timing_function = AnimationTiming { AnimationTiming::Steps { | ||||
|                     timing_function = Animations::TimingFunction { Animations::StepsTimingFunction { | ||||
|                         .number_of_steps = 1, | ||||
|                         .jump_at_start = false, | ||||
|                         .jump_at_end = true, | ||||
|                     } }; | ||||
|                     break; | ||||
|                 case EasingFunction::StepStart: | ||||
|                             timing_function = AnimationTiming { AnimationTiming::Steps { | ||||
|                     timing_function = Animations::TimingFunction { Animations::StepsTimingFunction { | ||||
|                         .number_of_steps = 1, | ||||
|                         .jump_at_start = true, | ||||
|                         .jump_at_end = false, | ||||
|  | @ -1407,30 +1119,45 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|                     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)); | ||||
|                 } | ||||
|             auto& realm = element.realm(); | ||||
| 
 | ||||
|                 TRY((*active_animation)->collect_into(style, rule_cache_for_cascade_origin(CascadeOrigin::Author))); | ||||
|             } else { | ||||
|                 m_active_animations.remove(animation_key); | ||||
|             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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         if (!m_active_animations.is_empty()) | ||||
|             ensure_animation_timer(); | ||||
|     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<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); | ||||
|                 Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame resolved_keyframe; | ||||
| 
 | ||||
|                 auto key = static_cast<u64>(keyframe->key().value() * Animations::KeyframeEffect::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; | ||||
|                 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 = 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 <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 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; | ||||
|                 } | ||||
| 
 | ||||
|                 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<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)); | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| }; | ||||
|  |  | |||
|  | @ -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) | ||||
| { | ||||
|  |  | |||
|  | @ -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 }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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]
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Matthew Olsson
						Matthew Olsson