Files
ladybird/Libraries/LibWeb/Animations/KeyframeEffect.h
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

143 lines
6.1 KiB
C++

/*
* Copyright (c) 2023-2024, Matthew Olsson <mattco@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/RedBlackTree.h>
#include <LibWeb/Animations/AnimationEffect.h>
#include <LibWeb/Bindings/KeyframeEffectPrototype.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::Animations {
using EasingValue = Variant<String, CSS::EasingFunction>;
// https://www.w3.org/TR/web-animations-1/#the-keyframeeffectoptions-dictionary
struct KeyframeEffectOptions : public EffectTiming {
Bindings::CompositeOperation composite { Bindings::CompositeOperation::Replace };
Optional<String> pseudo_element {};
};
Bindings::CompositeOperation css_animation_composition_to_bindings_composite_operation(CSS::AnimationComposition composition);
Bindings::CompositeOperationOrAuto css_animation_composition_to_bindings_composite_operation_or_auto(CSS::AnimationComposition composition);
// https://www.w3.org/TR/web-animations-1/#dictdef-basepropertyindexedkeyframe
// Note: This is an intermediate structure used only when parsing Keyframes provided by the caller in a slightly
// different format. It is converted to BaseKeyframe, which is why it doesn't need to store the parsed properties
struct BasePropertyIndexedKeyframe {
Variant<Optional<double>, Vector<Optional<double>>> offset { Vector<Optional<double>> {} };
Variant<EasingValue, Vector<EasingValue>> easing { Vector<EasingValue> {} };
Variant<Bindings::CompositeOperationOrAuto, Vector<Bindings::CompositeOperationOrAuto>> composite { Vector<Bindings::CompositeOperationOrAuto> {} };
HashMap<String, Vector<String>> properties {};
};
// https://www.w3.org/TR/web-animations-1/#dictdef-basekeyframe
struct BaseKeyframe {
using UnparsedProperties = HashMap<String, String>;
using ParsedProperties = HashMap<CSS::PropertyID, NonnullRefPtr<CSS::StyleValue const>>;
Optional<double> offset {};
EasingValue easing { "linear"_string };
Bindings::CompositeOperationOrAuto composite { Bindings::CompositeOperationOrAuto::Auto };
Optional<double> computed_offset {};
Variant<UnparsedProperties, ParsedProperties> properties { UnparsedProperties {} };
UnparsedProperties& unparsed_properties() { return properties.get<UnparsedProperties>(); }
ParsedProperties& parsed_properties() { return properties.get<ParsedProperties>(); }
};
// https://www.w3.org/TR/web-animations-1/#the-keyframeeffect-interface
class KeyframeEffect final : public AnimationEffect {
WEB_PLATFORM_OBJECT(KeyframeEffect, AnimationEffect);
GC_DECLARE_ALLOCATOR(KeyframeEffect);
public:
constexpr static double AnimationKeyFrameKeyScaleFactor = 1000.0; // 0..100000
struct KeyFrameSet : public RefCounted<KeyFrameSet> {
struct UseInitial { };
struct ResolvedKeyFrame {
// These StyleValue properties can be unresolved, as they may be generated from a @keyframes rule, well
// before they are applied to an element
HashMap<CSS::PropertyID, Variant<UseInitial, NonnullRefPtr<CSS::StyleValue const>>> properties {};
Bindings::CompositeOperationOrAuto composite { Bindings::CompositeOperationOrAuto::Auto };
Optional<CSS::EasingFunction> easing {};
};
RedBlackTree<u64, ResolvedKeyFrame> keyframes_by_key;
};
static void generate_initial_and_final_frames(RefPtr<KeyFrameSet>, HashTable<CSS::PropertyID> const& animated_properties);
static int composite_order(GC::Ref<KeyframeEffect>, GC::Ref<KeyframeEffect>);
static GC::Ref<KeyframeEffect> create(JS::Realm&);
static WebIDL::ExceptionOr<GC::Ref<KeyframeEffect>> construct_impl(
JS::Realm&,
GC::Root<DOM::Element> const& target,
Optional<GC::Root<JS::Object>> const& keyframes,
Variant<double, KeyframeEffectOptions> options = KeyframeEffectOptions {});
static WebIDL::ExceptionOr<GC::Ref<KeyframeEffect>> construct_impl(JS::Realm&, GC::Ref<KeyframeEffect> source);
DOM::Element* target() const override { return m_target_element; }
void set_target(DOM::Element* target);
// JS bindings
Optional<String> pseudo_element() const;
WebIDL::ExceptionOr<void> set_pseudo_element(Optional<String>);
Optional<DOM::AbstractElement> target_abstract_element() const;
void set_target(DOM::AbstractElement);
Optional<CSS::PseudoElement> pseudo_element_type() const;
void set_pseudo_element(Optional<CSS::Selector::PseudoElementSelector> pseudo_element) { m_target_pseudo_selector = pseudo_element; }
Bindings::CompositeOperation composite() const { return m_composite; }
void set_composite(Bindings::CompositeOperation value) { m_composite = value; }
WebIDL::ExceptionOr<GC::RootVector<JS::Object*>> get_keyframes();
WebIDL::ExceptionOr<void> set_keyframes(Optional<GC::Root<JS::Object>> const&);
KeyFrameSet const* key_frame_set() { return m_key_frame_set; }
void set_key_frame_set(RefPtr<KeyFrameSet const> key_frame_set) { m_key_frame_set = key_frame_set; }
virtual bool is_keyframe_effect() const override { return true; }
virtual void update_computed_properties(AnimationUpdateContext&) override;
private:
KeyframeEffect(JS::Realm&);
virtual ~KeyframeEffect() override = default;
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
// https://www.w3.org/TR/web-animations-1/#effect-target-target-element
GC::Ptr<DOM::Element> m_target_element {};
// https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-pseudoelement
Optional<CSS::Selector::PseudoElementSelector> m_target_pseudo_selector {};
// https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-composite
Bindings::CompositeOperation m_composite { Bindings::CompositeOperation::Replace };
// https://www.w3.org/TR/web-animations-1/#keyframe
Vector<BaseKeyframe> m_keyframes {};
// A cached version of m_keyframes suitable for returning from get_keyframes()
Vector<GC::Ref<JS::Object>> m_keyframe_objects {};
RefPtr<KeyFrameSet const> m_key_frame_set {};
};
}