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 associate_with_effect(JS::NonnullGCPtr<AnimationEffect> effect); | ||||||
|     void disassociate_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: | private: | ||||||
|     Vector<JS::NonnullGCPtr<AnimationEffect>> m_associated_effects; |     Vector<JS::NonnullGCPtr<AnimationEffect>> m_associated_effects; | ||||||
|     bool m_is_sorted_by_composite_order { true }; |     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
 | // https://www.w3.org/TR/web-animations-1/#play-an-animation
 | ||||||
| WebIDL::ExceptionOr<void> Animation::play_an_animation(AutoRewind auto_rewind) | 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.
 |     // 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; |     auto aborted_pause = m_pending_pause_task == TaskState::Scheduled; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|  * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org> |  * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org> | ||||||
|  * Copyright (c) 2021, the SerenityOS developers. |  * Copyright (c) 2021, the SerenityOS developers. | ||||||
|  * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org> |  * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org> | ||||||
|  |  * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org> | ||||||
|  * |  * | ||||||
|  * SPDX-License-Identifier: BSD-2-Clause |  * SPDX-License-Identifier: BSD-2-Clause | ||||||
|  */ |  */ | ||||||
|  | @ -12,6 +13,7 @@ | ||||||
| #include <AK/Find.h> | #include <AK/Find.h> | ||||||
| #include <AK/Function.h> | #include <AK/Function.h> | ||||||
| #include <AK/HashMap.h> | #include <AK/HashMap.h> | ||||||
|  | #include <AK/Math.h> | ||||||
| #include <AK/QuickSort.h> | #include <AK/QuickSort.h> | ||||||
| #include <AK/TemporaryChange.h> | #include <AK/TemporaryChange.h> | ||||||
| #include <LibGfx/Font/Font.h> | #include <LibGfx/Font/Font.h> | ||||||
|  | @ -22,6 +24,11 @@ | ||||||
| #include <LibGfx/Font/VectorFont.h> | #include <LibGfx/Font/VectorFont.h> | ||||||
| #include <LibGfx/Font/WOFF/Font.h> | #include <LibGfx/Font/WOFF/Font.h> | ||||||
| #include <LibGfx/Font/WOFF2/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/CSSFontFaceRule.h> | ||||||
| #include <LibWeb/CSS/CSSImportRule.h> | #include <LibWeb/CSS/CSSImportRule.h> | ||||||
| #include <LibWeb/CSS/CSSStyleRule.h> | #include <LibWeb/CSS/CSSStyleRule.h> | ||||||
|  | @ -57,11 +64,14 @@ | ||||||
| #include <LibWeb/DOM/Element.h> | #include <LibWeb/DOM/Element.h> | ||||||
| #include <LibWeb/HTML/HTMLBRElement.h> | #include <LibWeb/HTML/HTMLBRElement.h> | ||||||
| #include <LibWeb/HTML/HTMLHtmlElement.h> | #include <LibWeb/HTML/HTMLHtmlElement.h> | ||||||
|  | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> | ||||||
|  | #include <LibWeb/HighResolutionTime/TimeOrigin.h> | ||||||
| #include <LibWeb/Layout/Node.h> | #include <LibWeb/Layout/Node.h> | ||||||
| #include <LibWeb/Loader/ResourceLoader.h> | #include <LibWeb/Loader/ResourceLoader.h> | ||||||
| #include <LibWeb/Namespace.h> | #include <LibWeb/Namespace.h> | ||||||
| #include <LibWeb/Platform/FontPlugin.h> | #include <LibWeb/Platform/FontPlugin.h> | ||||||
| #include <LibWeb/ReferrerPolicy/AbstractOperations.h> | #include <LibWeb/ReferrerPolicy/AbstractOperations.h> | ||||||
|  | #include <math.h> | ||||||
| #include <stdio.h> | #include <stdio.h> | ||||||
| 
 | 
 | ||||||
| namespace AK { | namespace AK { | ||||||
|  | @ -743,46 +753,6 @@ static ErrorOr<void> cascade_custom_properties(DOM::Element& element, Optional<C | ||||||
|     return {}; |     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) | static ErrorOr<NonnullRefPtr<StyleValue>> interpolate_property(StyleValue const& from, StyleValue const& to, float delta) | ||||||
| { | { | ||||||
|     if (from.type() != to.type()) { |     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) |     auto animation = effect->associated_animation(); | ||||||
|         || (direction == CSS::AnimationDirection::Alternate && current_iteration % 2 == 0) |     if (!animation) | ||||||
|         || 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()) |  | ||||||
|         return {}; |         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; |     if (!effect->key_frame_set()) | ||||||
|     auto is_backwards = is_animating_backwards(); |         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); |     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 (matching_keyframe_it.is_end()) { | ||||||
|         if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) { |         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); |             : static_cast<float>(key - keyframe_start) / static_cast<float>(keyframe_end - keyframe_start); | ||||||
|     }(); |     }(); | ||||||
| 
 | 
 | ||||||
|     auto valid_properties = 0; |     if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) { | ||||||
|     for (auto const& property : keyframe_values.resolved_properties) { |         auto valid_properties = keyframe_values.resolved_properties.size(); | ||||||
|         if (property.has<Empty>()) |         dbgln("Animation {} contains {} properties to interpolate, progress = {}%", animation->id(), valid_properties, progress_in_keyframe * 100); | ||||||
|             continue; |  | ||||||
|         valid_properties++; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Animation {} contains {} properties to interpolate, progress = {}%", name, valid_properties, progress_in_keyframe * 100); |     for (auto const& it : keyframe_values.resolved_properties) { | ||||||
| 
 |  | ||||||
|     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; |  | ||||||
| 
 |  | ||||||
|         auto resolve_property = [&](auto& property) { |         auto resolve_property = [&](auto& property) { | ||||||
|             return property.visit( |             return property.visit( | ||||||
|                 [](Empty) -> RefPtr<StyleValue const> { VERIFY_NOT_REACHED(); }, |                 [&](Animations::KeyframeEffect::KeyFrameSet::UseInitial) -> RefPtr<StyleValue const> { | ||||||
|                 [&](AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial) { |                     return style_properties.maybe_null_property(it.key); | ||||||
|                     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; |  | ||||||
|                 }, |                 }, | ||||||
|                 [&](RefPtr<StyleValue const> value) { return value; }); |                 [&](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)]; |         auto const& end_property = keyframe_end_values.resolved_properties.get(it.key); | ||||||
|         if (end_property.has<Empty>()) { |         if (!end_property.has_value()) { | ||||||
|             if (resolved_start_property) { |             if (resolved_start_property) { | ||||||
|                 style_properties.set_property(property_id, resolved_start_property.release_nonnull()); |                 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(property_id), resolved_start_property->to_string()); |                 dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "No end property for property {}, using {}", string_from_property_id(it.key), resolved_start_property->to_string()); | ||||||
|             } |             } | ||||||
|             continue; |             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) |         if (!resolved_start_property || !resolved_end_property) | ||||||
|             continue; |             continue; | ||||||
|  | @ -988,227 +930,13 @@ ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_prop | ||||||
|         auto end = resolved_end_property.release_nonnull(); |         auto end = resolved_end_property.release_nonnull(); | ||||||
| 
 | 
 | ||||||
|         auto next_value = TRY(interpolate_property(*start, *end, progress_in_keyframe)); |         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()); |         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(property_id, next_value); |         style_properties.set_property(it.key, next_value); | ||||||
|         if (active_state_if_fill_forward) |  | ||||||
|             active_state_if_fill_forward->state[to_underlying(property_id)] = next_value; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return {}; |     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
 | // 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 | 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; |         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)); |     TRY(cascade_custom_properties(element, pseudo_element, matching_rule_set.author_rules)); | ||||||
| 
 | 
 | ||||||
|     // Then we apply the declarations from the matched rules in cascade order:
 |     // Then we apply the declarations from the matched rules in cascade order:
 | ||||||
|  | @ -1261,176 +989,175 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM | ||||||
|     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No); |     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No); | ||||||
| 
 | 
 | ||||||
|     // Animation declarations [css-animations-2]
 |     // 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); |         auto animation_name = style.maybe_null_property(PropertyID::AnimationName); | ||||||
|         if (animation_name.is_null()) |         if (animation_name.is_null()) | ||||||
|             return OptionalNone {}; |             return OptionalNone {}; | ||||||
|         if (animation_name->is_string()) |         if (animation_name->is_string()) | ||||||
|             return animation_name->as_string().string_value(); |             return animation_name->as_string().string_value(); | ||||||
|         return animation_name->to_string(); |         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()) { |     if (animation_name.has_value()) { | ||||||
|                 // We've already finished going through this animation, so drop it from the active animations.
 |         if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName); source_declaration && source_declaration != element.cached_animation_name_source()) { | ||||||
|                 m_active_animations.remove(animation_key); |             // This animation name is new, so we need to create a new animation for it.
 | ||||||
|                 // If the animation's fill mode was set to forwards/both, we need to collect and use the final frame's styles.
 |             element.set_cached_animation_name_source(source_declaration); | ||||||
|                 if (*finished_state) { | 
 | ||||||
|                     auto& state = (*finished_state)->state; |             Optional<CSS::Time> duration; | ||||||
|                     for (size_t property_id_value = 0; property_id_value < state.size(); ++property_id_value) { |             if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) { | ||||||
|                         if (auto& property_value = state[property_id_value]) |                 if (duration_value->is_time()) { | ||||||
|                             style.set_property(static_cast<PropertyID>(property_id_value), *property_value); |                     duration = duration_value->as_time().time(); | ||||||
|                     } |                 } else if (duration_value->is_identifier() && duration_value->as_identifier().id() == ValueID::Auto) { | ||||||
|  |                     // We use empty optional to represent "auto".
 | ||||||
|  |                     duration = {}; | ||||||
|                 } |                 } | ||||||
|             } else if (!animation_name->is_empty()) { |  | ||||||
|                 auto active_animation = m_active_animations.get(animation_key); |  | ||||||
|                 if (!active_animation.has_value()) { |  | ||||||
|                     // New animation!
 |  | ||||||
|                     Optional<CSS::Time> duration; |  | ||||||
|                     if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) { |  | ||||||
|                         if (duration_value->is_time()) { |  | ||||||
|                             duration = duration_value->as_time().time(); |  | ||||||
|                         } else if (duration_value->is_identifier() && duration_value->as_identifier().id() == ValueID::Auto) { |  | ||||||
|                             // We use empty optional to represent "auto".
 |  | ||||||
|                             duration = {}; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     CSS::Time delay { 0, CSS::Time::Type::S }; |  | ||||||
|                     if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time()) |  | ||||||
|                         delay = delay_value->as_time().time(); |  | ||||||
| 
 |  | ||||||
|                     Optional<size_t> iteration_count = 1; |  | ||||||
|                     if (auto iteration_count_value = style.maybe_null_property(PropertyID::AnimationIterationCount); iteration_count_value) { |  | ||||||
|                         if (iteration_count_value->is_identifier() && iteration_count_value->to_identifier() == ValueID::Infinite) |  | ||||||
|                             iteration_count = {}; |  | ||||||
|                         else if (iteration_count_value->is_number()) |  | ||||||
|                             iteration_count = static_cast<size_t>(iteration_count_value->as_number().number()); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     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<size_t>(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)), |  | ||||||
|                                 .jump_at_start = jump_at_start, |  | ||||||
|                                 .jump_at_end = jump_at_end, |  | ||||||
|                             } }; |  | ||||||
|                             break; |  | ||||||
|                         } |  | ||||||
|                         case EasingFunction::StepEnd: |  | ||||||
|                             timing_function = 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>(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)); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 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()) |             CSS::Time delay { 0, CSS::Time::Type::S }; | ||||||
|             ensure_animation_timer(); |             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<size_t>(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)), | ||||||
|  |                         .jump_at_start = jump_at_start, | ||||||
|  |                         .jump_at_end = jump_at_end, | ||||||
|  |                     } }; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 case EasingFunction::StepEnd: | ||||||
|  |                     timing_function = Animations::TimingFunction { Animations::StepsTimingFunction { | ||||||
|  |                         .number_of_steps = 1, | ||||||
|  |                         .jump_at_start = false, | ||||||
|  |                         .jump_at_end = true, | ||||||
|  |                     } }; | ||||||
|  |                     break; | ||||||
|  |                 case EasingFunction::StepStart: | ||||||
|  |                     timing_function = Animations::TimingFunction { Animations::StepsTimingFunction { | ||||||
|  |                         .number_of_steps = 1, | ||||||
|  |                         .jump_at_start = true, | ||||||
|  |                         .jump_at_end = false, | ||||||
|  |                     } }; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             auto& realm = element.realm(); | ||||||
|  | 
 | ||||||
|  |             auto effect = Animations::KeyframeEffect::create(realm); | ||||||
|  |             auto iteration_duration = duration.has_value() | ||||||
|  |                 ? Variant<double, String> { duration.release_value().to_milliseconds() } | ||||||
|  |                 : "auto"_string; | ||||||
|  |             effect->set_iteration_duration(iteration_duration); | ||||||
|  |             effect->set_start_delay(delay.to_milliseconds()); | ||||||
|  |             effect->set_iteration_count(iteration_count); | ||||||
|  |             effect->set_timing_function(move(timing_function)); | ||||||
|  |             effect->set_fill_mode(Animations::css_fill_mode_to_bindings_fill_mode(fill_mode)); | ||||||
|  |             effect->set_playback_direction(Animations::css_animation_direction_to_bindings_playback_direction(direction)); | ||||||
|  | 
 | ||||||
|  |             auto animation = CSSAnimation::create(realm); | ||||||
|  |             animation->set_id(animation_name.release_value()); | ||||||
|  |             animation->set_timeline(m_document->timeline()); | ||||||
|  |             animation->set_owning_element(element); | ||||||
|  |             animation->set_effect(effect); | ||||||
|  | 
 | ||||||
|  |             auto const& rule_cache = rule_cache_for_cascade_origin(CascadeOrigin::Author); | ||||||
|  |             if (auto keyframe_set = rule_cache.rules_by_animation_keyframes.get(animation->id()); keyframe_set.has_value()) | ||||||
|  |                 effect->set_key_frame_set(keyframe_set.value()); | ||||||
|  | 
 | ||||||
|  |             element.associate_with_effect(effect); | ||||||
|  | 
 | ||||||
|  |             HTML::TemporaryExecutionContext context(m_document->relevant_settings_object()); | ||||||
|  |             animation->play().release_value_but_fixme_should_propagate_errors(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     auto animations = element.get_animations({ .subtree = false }); | ||||||
|  |     for (auto& animation : animations) { | ||||||
|  |         if (!animation->is_relevant()) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         if (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
 |     // Important author declarations
 | ||||||
|  | @ -2235,88 +1962,36 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca | ||||||
|             ++rule_index; |             ++rule_index; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         // Loosely based on https://drafts.csswg.org/css-animations-2/#keyframe-processing
 | ||||||
|         sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) { |         sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) { | ||||||
|             auto keyframe_set = make<AnimationKeyFrameSet>(); |             auto keyframe_set = adopt_ref(*new Animations::KeyframeEffect::KeyFrameSet); | ||||||
|             AnimationKeyFrameSet::ResolvedKeyFrame resolved_keyframe; |             HashTable<PropertyID> animated_properties; | ||||||
| 
 | 
 | ||||||
|             // Forwards pass, resolve all the user-specified keyframe properties.
 |             // Forwards pass, resolve all the user-specified keyframe properties.
 | ||||||
|             for (auto const& keyframe : rule.keyframes()) { |             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(); |                 auto keyframe_rule = keyframe->style(); | ||||||
| 
 | 
 | ||||||
|                 if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule)) |                 if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule)) | ||||||
|                     continue; |                     continue; | ||||||
| 
 | 
 | ||||||
|                 auto current_keyframe = resolved_keyframe; |                 auto const& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule); | ||||||
|                 auto& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule); |                 for (auto const& property : keyframe_style.properties()) { | ||||||
|                 for (auto& property : keyframe_style.properties()) |                     animated_properties.set(property.property_id); | ||||||
|                     current_keyframe.resolved_properties[to_underlying(property.property_id)] = property.value; |                     resolved_keyframe.resolved_properties.set(property.property_id, property.value); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 resolved_keyframe = move(current_keyframe); |  | ||||||
|                 keyframe_set->keyframes_by_key.insert(key, resolved_keyframe); |                 keyframe_set->keyframes_by_key.insert(key, resolved_keyframe); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // If there is no 'from' keyframe, make a synthetic one.
 |             Animations::KeyframeEffect::generate_initial_and_final_frames(keyframe_set, animated_properties); | ||||||
|             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; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if constexpr (LIBWEB_CSS_DEBUG) { |             if constexpr (LIBWEB_CSS_DEBUG) { | ||||||
|                 dbgln("Resolved keyframe set '{}' into {} keyframes:", rule.name(), keyframe_set->keyframes_by_key.size()); |                 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) { |                 for (auto it = keyframe_set->keyframes_by_key.begin(); it != keyframe_set->keyframes_by_key.end(); ++it) | ||||||
|                     size_t props = 0; |                     dbgln("    - keyframe {}: {} properties", it.key(), it->resolved_properties.size()); | ||||||
|                     for (auto& entry : it->resolved_properties) |  | ||||||
|                         props += !entry.has<Empty>(); |  | ||||||
|                     dbgln("    - keyframe {}: {} properties", it.key(), props); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set)); |             rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set)); | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| #include <AK/HashMap.h> | #include <AK/HashMap.h> | ||||||
| #include <AK/Optional.h> | #include <AK/Optional.h> | ||||||
| #include <AK/OwnPtr.h> | #include <AK/OwnPtr.h> | ||||||
| #include <AK/RedBlackTree.h> | #include <LibWeb/Animations/KeyframeEffect.h> | ||||||
| #include <LibWeb/CSS/CSSFontFaceRule.h> | #include <LibWeb/CSS/CSSFontFaceRule.h> | ||||||
| #include <LibWeb/CSS/CSSKeyframesRule.h> | #include <LibWeb/CSS/CSSKeyframesRule.h> | ||||||
| #include <LibWeb/CSS/CSSStyleDeclaration.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; |     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; } |     void set_viewport_rect(Badge<DOM::Document>, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; } | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  | @ -156,28 +120,20 @@ private: | ||||||
| 
 | 
 | ||||||
|     JS::NonnullGCPtr<DOM::Document> m_document; |     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 { |     struct RuleCache { | ||||||
|         HashMap<FlyString, Vector<MatchingRule>> rules_by_id; |         HashMap<FlyString, Vector<MatchingRule>> rules_by_id; | ||||||
|         HashMap<FlyString, Vector<MatchingRule>> rules_by_class; |         HashMap<FlyString, Vector<MatchingRule>> rules_by_class; | ||||||
|         HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name; |         HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name; | ||||||
|         Vector<MatchingRule> other_rules; |         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); |     NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin); | ||||||
| 
 | 
 | ||||||
|     RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const; |     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_author_rule_cache; | ||||||
|     OwnPtr<RuleCache> m_user_rule_cache; |     OwnPtr<RuleCache> m_user_rule_cache; | ||||||
|  | @ -190,71 +146,7 @@ private: | ||||||
|     Length::FontMetrics m_default_font_metrics; |     Length::FontMetrics m_default_font_metrics; | ||||||
|     Length::FontMetrics m_root_element_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; |     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/AnimationTimeline.h> | ||||||
| #include <LibWeb/Animations/DocumentTimeline.h> | #include <LibWeb/Animations/DocumentTimeline.h> | ||||||
| #include <LibWeb/Bindings/MainThreadVM.h> | #include <LibWeb/Bindings/MainThreadVM.h> | ||||||
|  | #include <LibWeb/CSS/AnimationEvent.h> | ||||||
|  | #include <LibWeb/CSS/CSSAnimation.h> | ||||||
| #include <LibWeb/CSS/MediaQueryList.h> | #include <LibWeb/CSS/MediaQueryList.h> | ||||||
| #include <LibWeb/CSS/MediaQueryListEvent.h> | #include <LibWeb/CSS/MediaQueryListEvent.h> | ||||||
| #include <LibWeb/CSS/StyleComputer.h> | #include <LibWeb/CSS/StyleComputer.h> | ||||||
|  | @ -1889,6 +1891,19 @@ Element* Document::find_a_potential_indicated_element(FlyString const& fragment) | ||||||
|     return nullptr; |     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
 | // https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier
 | ||||||
| void Document::scroll_to_the_fragment() | 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
 | // https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
 | ||||||
| static bool is_potentially_named_element(DOM::Element const& element) | static bool is_potentially_named_element(DOM::Element const& element) | ||||||
| { | { | ||||||
|  |  | ||||||
|  | @ -561,6 +561,7 @@ public: | ||||||
|     void append_pending_animation_event(PendingAnimationEvent const&); |     void append_pending_animation_event(PendingAnimationEvent const&); | ||||||
|     void update_animations_and_send_events(Optional<double> const& timestamp); |     void update_animations_and_send_events(Optional<double> const& timestamp); | ||||||
|     void remove_replaced_animations(); |     void remove_replaced_animations(); | ||||||
|  |     void ensure_animation_timer(); | ||||||
| 
 | 
 | ||||||
|     bool ready_to_run_scripts() const { return m_ready_to_run_scripts; } |     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; |     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; |     JS::NonnullGCPtr<Page> m_page; | ||||||
|     OwnPtr<CSS::StyleComputer> m_style_computer; |     OwnPtr<CSS::StyleComputer> m_style_computer; | ||||||
|     JS::GCPtr<CSS::StyleSheetList> m_style_sheets; |     JS::GCPtr<CSS::StyleSheetList> m_style_sheets; | ||||||
|  | @ -815,6 +818,7 @@ private: | ||||||
| 
 | 
 | ||||||
|     // https://www.w3.org/TR/web-animations-1/#pending-animation-event-queue
 |     // https://www.w3.org/TR/web-animations-1/#pending-animation-event-queue
 | ||||||
|     Vector<PendingAnimationEvent> m_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 }; |     bool m_needs_to_call_page_did_load { false }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -195,7 +195,8 @@ void EventLoop::process() | ||||||
|         document.evaluate_media_queries_and_report_changes(); |         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]
 |     // 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