mirror of
https://github.com/RGBCube/serenity
synced 2025-05-22 22:05:07 +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,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);
|
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)) {
|
if (animation_name.has_value()) {
|
||||||
AnimationKey animation_key {
|
if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName); source_declaration && source_declaration != element.cached_animation_name_source()) {
|
||||||
.source_declaration = source_declaration,
|
// This animation name is new, so we need to create a new animation for it.
|
||||||
.element = &element,
|
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;
|
Optional<CSS::Time> duration;
|
||||||
if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) {
|
if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value) {
|
||||||
if (duration_value->is_time()) {
|
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())
|
if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time())
|
||||||
delay = delay_value->as_time().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 (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)
|
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())
|
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 };
|
CSS::AnimationFillMode fill_mode { CSS::AnimationFillMode::None };
|
||||||
|
@ -1325,29 +1037,29 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
|
||||||
direction = *direction_value;
|
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()) {
|
if (auto timing_property = style.maybe_null_property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) {
|
||||||
auto& easing_value = timing_property->as_easing();
|
auto& easing_value = timing_property->as_easing();
|
||||||
switch (easing_value.easing_function()) {
|
switch (easing_value.easing_function()) {
|
||||||
case EasingFunction::Linear:
|
case EasingFunction::Linear:
|
||||||
timing_function = AnimationTiming { AnimationTiming::Linear {} };
|
timing_function = Animations::linear_timing_function;
|
||||||
break;
|
break;
|
||||||
case EasingFunction::Ease:
|
case EasingFunction::Ease:
|
||||||
timing_function = AnimationTiming { ease_timing_function };
|
timing_function = Animations::ease_timing_function;
|
||||||
break;
|
break;
|
||||||
case EasingFunction::EaseIn:
|
case EasingFunction::EaseIn:
|
||||||
timing_function = AnimationTiming { ease_in_timing_function };
|
timing_function = Animations::ease_in_timing_function;
|
||||||
break;
|
break;
|
||||||
case EasingFunction::EaseOut:
|
case EasingFunction::EaseOut:
|
||||||
timing_function = AnimationTiming { ease_out_timing_function };
|
timing_function = Animations::ease_out_timing_function;
|
||||||
break;
|
break;
|
||||||
case EasingFunction::EaseInOut:
|
case EasingFunction::EaseInOut:
|
||||||
timing_function = AnimationTiming { ease_in_out_timing_function };
|
timing_function = Animations::ease_in_out_timing_function;
|
||||||
break;
|
break;
|
||||||
case EasingFunction::CubicBezier: {
|
case EasingFunction::CubicBezier: {
|
||||||
auto values = easing_value.values();
|
auto values = easing_value.values();
|
||||||
timing_function = AnimationTiming {
|
timing_function = {
|
||||||
AnimationTiming::CubicBezier {
|
Animations::CubicBezierTimingFunction {
|
||||||
values[0]->as_number().number(),
|
values[0]->as_number().number(),
|
||||||
values[1]->as_number().number(),
|
values[1]->as_number().number(),
|
||||||
values[2]->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)),
|
.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_start = jump_at_start,
|
||||||
.jump_at_end = jump_at_end,
|
.jump_at_end = jump_at_end,
|
||||||
|
@ -1391,14 +1103,14 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EasingFunction::StepEnd:
|
case EasingFunction::StepEnd:
|
||||||
timing_function = AnimationTiming { AnimationTiming::Steps {
|
timing_function = Animations::TimingFunction { Animations::StepsTimingFunction {
|
||||||
.number_of_steps = 1,
|
.number_of_steps = 1,
|
||||||
.jump_at_start = false,
|
.jump_at_start = false,
|
||||||
.jump_at_end = true,
|
.jump_at_end = true,
|
||||||
} };
|
} };
|
||||||
break;
|
break;
|
||||||
case EasingFunction::StepStart:
|
case EasingFunction::StepStart:
|
||||||
timing_function = AnimationTiming { AnimationTiming::Steps {
|
timing_function = Animations::TimingFunction { Animations::StepsTimingFunction {
|
||||||
.number_of_steps = 1,
|
.number_of_steps = 1,
|
||||||
.jump_at_start = true,
|
.jump_at_start = true,
|
||||||
.jump_at_end = false,
|
.jump_at_end = false,
|
||||||
|
@ -1407,30 +1119,45 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto animation = make<Animation>(Animation {
|
auto& realm = element.realm();
|
||||||
.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)));
|
auto effect = Animations::KeyframeEffect::create(realm);
|
||||||
} else {
|
auto iteration_duration = duration.has_value()
|
||||||
m_active_animations.remove(animation_key);
|
? 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())
|
auto animations = element.get_animations({ .subtree = false });
|
||||||
ensure_animation_timer();
|
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