ladybird/Libraries/LibWeb/CSS/CSSAnimation.cpp
Andreas Kling 42bf301acd LibWeb: Apply animation-timing-function per keyframe interval
Per the CSS Animations spec, the animation-timing-function property
describes how the animation progresses between each pair of keyframes,
not as an overall effect-level timing function.

Previously we set it as the effect-level timing function on the
AnimationEffect, which caused easing to be applied to the global
animation progress. This made animations with multiple keyframes
"pause" at the start and end of the full animation cycle instead of
easing smoothly between each pair of keyframes.

Now we:
- Store per-keyframe easing in ResolvedKeyFrame from @keyframes rules
- Store the default easing on CSSAnimation instead of on the effect
- Apply per-keyframe easing to the interval progress during
  interpolation, falling back to the CSS animation's default easing
- Also store per-keyframe easing from JS-created KeyframeEffects to
  avoid incorrectly applying CSS default easing to replaced effects
2026-03-21 23:16:32 -05:00

153 lines
7.2 KiB
C++

/*
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Animations/KeyframeEffect.h>
#include <LibWeb/Animations/ScrollTimeline.h>
#include <LibWeb/Bindings/CSSAnimationPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSAnimation.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(CSSAnimation);
GC::Ref<CSSAnimation> CSSAnimation::create(JS::Realm& realm)
{
return realm.create<CSSAnimation>(realm);
}
// https://www.w3.org/TR/css-animations-2/#animation-composite-order
int CSSAnimation::class_specific_composite_order(GC::Ref<Animations::Animation> other_animation) const
{
auto other = GC::Ref { as<CSSAnimation>(*other_animation) };
// The existence of an owning element determines the animation class, so both animations should have their owning
// element in the same state
VERIFY(owning_element().has_value() == other->owning_element().has_value());
// Within the set of CSS Animations with an owning element, two animations A and B are sorted in composite order
// (first to last) as follows:
if (owning_element().has_value()) {
// 1. If the owning element of A and B differs, sort A and B by tree order of their corresponding owning elements.
// With regard to pseudo-elements, the sort order is as follows:
// - element
// - ::marker
// - ::before
// - any other pseudo-elements not mentioned specifically in this list, sorted in ascending order by the Unicode
// codepoints that make up each selector
// - ::after
// - element children
if (owning_element() != other->owning_element()) {
// FIXME: Sort by tree order
return 0;
}
// 2. Otherwise, sort A and B based on their position in the computed value of the animation-name property of the
// (common) owning element.
// FIXME: Do this when animation-name supports multiple values
return 0;
}
// The composite order of CSS Animations without an owning element is based on their position in the global animation list.
return global_animation_list_order() - other->global_animation_list_order();
}
Animations::AnimationClass CSSAnimation::animation_class() const
{
if (owning_element().has_value())
return Animations::AnimationClass::CSSAnimationWithOwningElement;
return Animations::AnimationClass::CSSAnimationWithoutOwningElement;
}
// NB: Unrelated style changes shouldn't cause us to recreate anonymous timelines, to achieve this we drop updates
// between two equivalent anonymous timelines.
static bool should_update_timeline(GC::Ptr<Animations::AnimationTimeline> old_timeline, GC::Ptr<Animations::AnimationTimeline> new_timeline)
{
if (!old_timeline || !new_timeline)
return true;
if (is<Animations::ScrollTimeline>(*old_timeline) && is<Animations::ScrollTimeline>(*new_timeline)) {
auto const& old_scroll_timeline = as<Animations::ScrollTimeline>(*old_timeline);
auto const& new_scroll_timeline = as<Animations::ScrollTimeline>(*new_timeline);
if (!old_scroll_timeline.source_internal().has<Animations::ScrollTimeline::AnonymousSource>() || !new_scroll_timeline.source_internal().has<Animations::ScrollTimeline::AnonymousSource>())
return true;
return old_scroll_timeline.source_internal().get<Animations::ScrollTimeline::AnonymousSource>() != new_scroll_timeline.source_internal().get<Animations::ScrollTimeline::AnonymousSource>();
}
return true;
}
void CSSAnimation::apply_css_properties(ComputedProperties::AnimationProperties const& animation_properties)
{
// FIXME: Don't apply overriden properties as defined here: https://drafts.csswg.org/css-animations-2/#animations
VERIFY(effect());
auto& effect = as<Animations::KeyframeEffect>(*this->effect());
if (!m_ignored_css_properties.contains(PropertyID::AnimationTimeline) && should_update_timeline(timeline(), animation_properties.timeline)) {
HTML::TemporaryExecutionContext context(realm());
set_timeline(animation_properties.timeline);
}
effect.set_specified_iteration_duration(animation_properties.duration);
effect.set_specified_start_delay(animation_properties.delay);
effect.set_iteration_count(animation_properties.iteration_count);
// https://drafts.csswg.org/web-animations-2/#updating-animationeffect-timing
// Timing properties may also be updated due to a style change. Any change to a CSS animation property that affects
// timing requires rerunning the procedure to normalize specified timing.
effect.normalize_specified_timing();
// NB: animation-timing-function is applied per-keyframe, not as the effect-level timing function.
// The effect-level timing function remains linear.
m_default_easing = animation_properties.timing_function;
effect.set_fill_mode(Animations::css_fill_mode_to_bindings_fill_mode(animation_properties.fill_mode));
effect.set_playback_direction(Animations::css_animation_direction_to_bindings_playback_direction(animation_properties.direction));
effect.set_composite(Animations::css_animation_composition_to_bindings_composite_operation(animation_properties.composition));
if (animation_properties.play_state != last_css_animation_play_state()) {
if (animation_properties.play_state == CSS::AnimationPlayState::Running && play_state() != Bindings::AnimationPlayState::Running) {
HTML::TemporaryExecutionContext context(realm());
play().release_value_but_fixme_should_propagate_errors();
} else if (animation_properties.play_state == CSS::AnimationPlayState::Paused && play_state() != Bindings::AnimationPlayState::Paused) {
HTML::TemporaryExecutionContext context(realm());
pause().release_value_but_fixme_should_propagate_errors();
}
set_last_css_animation_play_state(animation_properties.play_state);
}
}
void CSSAnimation::set_timeline_for_bindings(GC::Ptr<Animations::AnimationTimeline> timeline)
{
// AD-HOC: When the timeline of a CSS animation is modified by the author from JS we should no longer apply changes
// to the `animation-timeline` property. See https://github.com/w3c/csswg-drafts/issues/13472
m_ignored_css_properties.set(PropertyID::AnimationTimeline);
set_timeline(timeline);
}
CSSAnimation::CSSAnimation(JS::Realm& realm)
: Animations::Animation(realm)
{
// FIXME:
// CSS Animations generated using the markup defined in this specification are not added to the global animation
// list when they are created. Instead, these animations are appended to the global animation list at the first
// moment when they transition out of the idle play state after being disassociated from their owning element. CSS
// Animations that have been disassociated from their owning element but are still idle do not have a defined
// composite order.
}
void CSSAnimation::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSAnimation);
Base::initialize(realm);
}
}