LibWeb: Store animation time values in abstract type

In level 2 of the web animations spec, times are no longer always
measures in milliseconds, they can also be percents when dealing with
progress-based (i.e. scroll-based) timelines.

We don't actually support percent times yet but this change will make it
easier to implement when we do.
This commit is contained in:
Callum Law 2025-11-30 20:31:57 +13:00 committed by Sam Atkins
parent f9df1c4eea
commit 69f05bd45d
14 changed files with 206 additions and 107 deletions

View file

@ -134,15 +134,16 @@ void Animation::set_timeline(GC::Ptr<AnimationTimeline> new_timeline)
// https://www.w3.org/TR/web-animations-1/#dom-animation-starttime
// https://www.w3.org/TR/web-animations-1/#set-the-start-time
void Animation::set_start_time(Optional<double> const& new_start_time)
void Animation::set_start_time_for_bindings(Optional<double> const& raw_new_start_time)
{
// Setting this attribute updates the start time using the procedure to set the start time of this object to the new
// value.
auto new_start_time = raw_new_start_time.map([](auto const& value) { return TimeValue { TimeValue::Type::Milliseconds, value }; });
// 1. Let timeline time be the current time value of the timeline that animation is associated with. If there is no
// timeline associated with animation or the associated timeline is inactive, let the timeline time be
// unresolved.
auto timeline_time = m_timeline && !m_timeline->is_inactive() ? m_timeline->current_time() : Optional<double> {};
auto timeline_time = m_timeline && !m_timeline->is_inactive() ? m_timeline->current_time() : Optional<TimeValue> {};
// 2. If timeline time is unresolved and new start time is resolved, make animations hold time unresolved.
if (!timeline_time.has_value() && new_start_time.has_value())
@ -185,7 +186,7 @@ void Animation::set_start_time(Optional<double> const& new_start_time)
}
// https://www.w3.org/TR/web-animations-1/#animation-current-time
Optional<double> Animation::current_time() const
Optional<TimeValue> Animation::current_time() const
{
// The current time is calculated from the first matching condition from below:
@ -212,8 +213,10 @@ Optional<double> Animation::current_time() const
}
// https://www.w3.org/TR/web-animations-1/#animation-set-the-current-time
WebIDL::ExceptionOr<void> Animation::set_current_time(Optional<double> const& seek_time)
WebIDL::ExceptionOr<void> Animation::set_current_time_for_bindings(Optional<double> const& raw_seek_time)
{
auto seek_time = raw_seek_time.map([](auto const& value) { return TimeValue { TimeValue::Type::Milliseconds, value }; });
// 1. Run the steps to silently set the current time of animation to seek time.
TRY(silently_set_current_time(seek_time));
@ -266,13 +269,13 @@ WebIDL::ExceptionOr<void> Animation::set_playback_rate(double new_playback_rate)
// -> If animation is associated with a monotonically increasing timeline and the previous time is resolved,
if (m_timeline && m_timeline->is_monotonically_increasing() && previous_time.has_value()) {
// set the current time of animation to previous time.
TRY(set_current_time(previous_time));
TRY(set_current_time_for_bindings(previous_time->as_milliseconds()));
}
// -> If animation is associated with a non-null timeline that is not monotonically increasing, the start time of
// animation is resolved, associated effect end is not infinity, and either:
// - the previous playback rate < 0 and the new playback rate ≥ 0, or
// - the previous playback rate ≥ 0 and the new playback rate < 0,
else if (m_timeline && !m_timeline->is_monotonically_increasing() && m_start_time.has_value() && !isinf(associated_effect_end()) && ((previous_playback_rate < 0.0 && new_playback_rate >= 0.0) || (previous_playback_rate >= 0 && new_playback_rate < 0))) {
else if (m_timeline && !m_timeline->is_monotonically_increasing() && m_start_time.has_value() && !isinf(associated_effect_end().value) && ((previous_playback_rate < 0.0 && new_playback_rate >= 0.0) || (previous_playback_rate >= 0 && new_playback_rate < 0))) {
// Set animations start time to the result of evaluating associated effect end - start time for animation.
m_start_time = associated_effect_end() - m_start_time.value();
}
@ -316,7 +319,7 @@ Bindings::AnimationPlayState Animation::play_state() const
// - animations effective playback rate > 0 and current time ≥ associated effect end; or
// - animations effective playback rate < 0 and current time ≤ 0,
auto effective_playback_rate = this->effective_playback_rate();
if (current_time.has_value() && ((effective_playback_rate > 0.0 && current_time.value() >= associated_effect_end()) || (effective_playback_rate < 0.0 && current_time.value() <= 0.0))) {
if (current_time.has_value() && ((effective_playback_rate > 0.0 && current_time.value() >= associated_effect_end()) || (effective_playback_rate < 0.0 && current_time->value <= 0))) {
// → finished
return Bindings::AnimationPlayState::Finished;
}
@ -463,7 +466,7 @@ void Animation::cancel(ShouldInvalidate should_invalidate)
// not associated with an active timeline, let timeline time be an unresolved time value.
// 9. Set cancelEvents timelineTime to timeline time. If timeline time is unresolved, set it to null.
AnimationPlaybackEventInit init;
init.timeline_time = m_timeline && !m_timeline->is_inactive() ? m_timeline->current_time() : Optional<double> {};
init.timeline_time = m_timeline && !m_timeline->is_inactive() ? m_timeline->current_time().map([](auto const& value) { return value.as_milliseconds(); }) : Optional<double> {};
auto cancel_event = AnimationPlaybackEvent::create(realm, HTML::EventNames::cancel, init);
// 10. If animation has a document for timing, then append cancelEvent to its document for timing's pending
@ -507,7 +510,7 @@ WebIDL::ExceptionOr<void> Animation::finish()
auto effective_playback_rate = this->effective_playback_rate();
if (effective_playback_rate == 0.0)
return WebIDL::InvalidStateError::create(realm(), "Animation with a playback rate of 0 cannot be finished"_utf16);
if (effective_playback_rate > 0.0 && isinf(associated_effect_end()))
if (effective_playback_rate > 0.0 && isinf(associated_effect_end().value))
return WebIDL::InvalidStateError::create(realm(), "Animation with no end cannot be finished"_utf16);
// 2. Apply any pending playback rate to animation.
@ -519,7 +522,7 @@ WebIDL::ExceptionOr<void> Animation::finish()
// -> Otherwise,
// Let limit be zero.
auto playback_rate = this->playback_rate();
auto limit = playback_rate > 0.0 ? associated_effect_end() : 0.0;
auto limit = playback_rate > 0.0 ? associated_effect_end() : TimeValue::create_zero(m_timeline);
// 4. Silently set the current time to limit.
TRY(silently_set_current_time(limit));
@ -582,7 +585,7 @@ WebIDL::ExceptionOr<void> Animation::play_an_animation(AutoRewind auto_rewind)
auto has_pending_ready_promise = false;
// 3. Let seek time be a time value that is initially unresolved.
Optional<double> seek_time;
Optional<TimeValue> seek_time;
// 4. If the auto-rewind flag is true, perform the steps corresponding to the first matching condition from the
// following, if any:
@ -595,17 +598,17 @@ WebIDL::ExceptionOr<void> Animation::play_an_animation(AutoRewind auto_rewind)
// - unresolved, or
// - less than zero, or
// - greater than or equal to associated effect end,
if (playback_rate >= 0.0 && (!current_time.has_value() || current_time.value() < 0.0 || current_time.value() >= associated_effect_end)) {
if (playback_rate >= 0.0 && (!current_time.has_value() || current_time->value < 0 || current_time.value() >= associated_effect_end)) {
// Set seek time to zero.
seek_time = 0.0;
seek_time = TimeValue::create_zero(m_timeline);
}
// -> If animations effective playback rate < 0, and animations current time is either:
// - unresolved, or
// - less than or equal to zero, or
// - greater than associated effect end,
else if (playback_rate < 0.0 && (!current_time.has_value() || current_time.value() <= 0.0 || current_time.value() > associated_effect_end)) {
else if (playback_rate < 0.0 && (!current_time.has_value() || current_time->value <= 0 || current_time.value() > associated_effect_end)) {
// -> If associated effect end is positive infinity,
if (isinf(associated_effect_end) && associated_effect_end > 0.0) {
if (isinf(associated_effect_end.value) && associated_effect_end.value > 0) {
// throw an "InvalidStateError" DOMException and abort these steps.
return WebIDL::InvalidStateError::create(realm(), "Cannot rewind an animation with an infinite effect end"_utf16);
}
@ -621,7 +624,7 @@ WebIDL::ExceptionOr<void> Animation::play_an_animation(AutoRewind auto_rewind)
// - animations current time is unresolved,
if (!seek_time.has_value() && !m_start_time.has_value() && !current_time().has_value()) {
// set seek time to zero.
seek_time = 0.0;
seek_time = TimeValue::create_zero(m_timeline);
}
// 6. Let has finite timeline be true if animation has an associated timeline that is not monotonically increasing.
@ -709,7 +712,7 @@ WebIDL::ExceptionOr<void> Animation::pause()
return {};
// 3. Let seek time be a time value that is initially unresolved.
Optional<double> seek_time;
Optional<TimeValue> seek_time;
// 4. Let has finite timeline be true if animation has an associated timeline that is not monotonically increasing.
auto has_finite_timeline = m_timeline && !m_timeline->is_monotonically_increasing();
@ -720,13 +723,13 @@ WebIDL::ExceptionOr<void> Animation::pause()
// -> If animations playback rate is ≥ 0,
if (playback_rate() >= 0.0) {
// Set seek time to zero.
seek_time = 0.0;
seek_time = TimeValue::create_zero(m_timeline);
}
// -> Otherwise
else {
// If associated effect end for animation is positive infinity,
auto associated_effect_end = this->associated_effect_end();
if (isinf(associated_effect_end) && associated_effect_end > 0.0) {
if (isinf(associated_effect_end.value) && associated_effect_end.value > 0) {
// throw an "InvalidStateError" DOMException and abort these steps.
return WebIDL::InvalidStateError::create(realm(), "Cannot pause an animation with an infinite effect end"_utf16);
}
@ -814,7 +817,7 @@ WebIDL::ExceptionOr<void> Animation::update_playback_rate(double new_playback_ra
else if (previous_play_state == Bindings::AnimationPlayState::Finished) {
// 1. Let the unconstrained current time be the result of calculating the current time of animation
// substituting an unresolved time value for the hold time.
Optional<double> unconstrained_current_time;
Optional<TimeValue> unconstrained_current_time;
{
TemporaryChange change(m_hold_time, {});
unconstrained_current_time = current_time();
@ -883,14 +886,14 @@ void Animation::persist()
}
// https://www.w3.org/TR/web-animations-1/#animation-time-to-timeline-time
Optional<double> Animation::convert_an_animation_time_to_timeline_time(Optional<double> time) const
Optional<TimeValue> Animation::convert_an_animation_time_to_timeline_time(Optional<TimeValue> time) const
{
// 1. If time is unresolved, return time.
if (!time.has_value())
return time;
// 2. If time is infinity, return an unresolved time value.
if (isinf(time.value()))
if (isinf(time->value))
return {};
// 3. If animations playback rate is zero, return an unresolved time value.
@ -907,14 +910,14 @@ Optional<double> Animation::convert_an_animation_time_to_timeline_time(Optional<
}
// https://www.w3.org/TR/web-animations-1/#animation-time-to-origin-relative-time
Optional<double> Animation::convert_a_timeline_time_to_an_origin_relative_time(Optional<double> time) const
Optional<double> Animation::convert_a_timeline_time_to_an_origin_relative_time(Optional<TimeValue> time) const
{
// 1. Let timeline time be the result of converting time from an animation time to a timeline time.
auto timeline_time = convert_an_animation_time_to_timeline_time(time);
// 2. If timeline time is unresolved, return time.
if (!timeline_time.has_value())
return time;
return {};
// 3. If animation is not associated with a timeline, return an unresolved time value.
if (!m_timeline)
@ -969,11 +972,11 @@ void Animation::effect_timing_changed(Badge<AnimationEffect>)
}
// https://www.w3.org/TR/web-animations-1/#associated-effect-end
double Animation::associated_effect_end() const
TimeValue Animation::associated_effect_end() const
{
// The associated effect end of an animation is equal to the end time of the animations associated effect. If the
// animation has no associated effect, the associated effect end is zero.
return m_effect ? m_effect->end_time() : 0.0;
return m_effect ? m_effect->end_time() : TimeValue::create_zero(m_timeline);
}
// https://www.w3.org/TR/web-animations-1/#effective-playback-rate
@ -999,7 +1002,7 @@ void Animation::apply_any_pending_playback_rate()
}
// https://www.w3.org/TR/web-animations-1/#animation-silently-set-the-current-time
WebIDL::ExceptionOr<void> Animation::silently_set_current_time(Optional<double> seek_time)
WebIDL::ExceptionOr<void> Animation::silently_set_current_time(Optional<TimeValue> seek_time)
{
// 1. If seek time is an unresolved time value, then perform the following steps.
if (!seek_time.has_value()) {
@ -1055,7 +1058,7 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
//
// Note: This is required to accommodate timelines that may change direction. Without this definition, a once-
// finished animation would remain finished even when its timeline progresses in the opposite direction.
Optional<double> unconstrained_current_time;
Optional<TimeValue> unconstrained_current_time;
if (did_seek == DidSeek::No) {
TemporaryChange change(m_hold_time, {});
unconstrained_current_time = current_time();
@ -1086,7 +1089,7 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
}
}
// -> If playback rate < 0 and unconstrained current time is less than or equal to 0,
else if (m_playback_rate < 0.0 && unconstrained_current_time.value() <= 0.0) {
else if (m_playback_rate < 0.0 && unconstrained_current_time->value <= 0) {
// If did seek is true, let the hold time be the value of unconstrained current time.
if (did_seek == DidSeek::Yes) {
m_hold_time = unconstrained_current_time;
@ -1094,9 +1097,9 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
// If did seek is false, let the hold time be the minimum value of previous current time and zero. If the
// previous current time is unresolved, let the hold time be zero.
else if (m_previous_current_time.has_value()) {
m_hold_time = min(m_previous_current_time.value(), 0.0);
m_hold_time = min(m_previous_current_time.value(), TimeValue::create_zero(m_timeline));
} else {
m_hold_time = 0.0;
m_hold_time = TimeValue::create_zero(m_timeline);
}
}
// -> If playback rate ≠ 0, and animation is associated with an active timeline,
@ -1138,14 +1141,14 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
// 4. Set finishEvents type attribute to finish.
// 5. Set finishEvents currentTime attribute to the current time of animation.
AnimationPlaybackEventInit init;
init.current_time = current_time();
init.current_time = current_time()->as_milliseconds();
auto finish_event = AnimationPlaybackEvent::create(realm, HTML::EventNames::finish, init);
// 6. Set finishEvents timelineTime attribute to the current time of the timeline with which animation is
// associated. If animation is not associated with a timeline, or the timeline is inactive, let
// timelineTime be null.
if (m_timeline && !m_timeline->is_inactive())
finish_event->set_timeline_time(m_timeline->current_time());
finish_event->set_timeline_time(m_timeline->current_time()->as_milliseconds());
else
finish_event->set_timeline_time({});

View file

@ -7,6 +7,7 @@
#pragma once
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibWeb/Animations/TimeValue.h>
#include <LibWeb/Bindings/AnimationPrototype.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/EventTarget.h>
@ -40,11 +41,21 @@ public:
GC::Ptr<AnimationTimeline> timeline() const { return m_timeline; }
void set_timeline(GC::Ptr<AnimationTimeline>);
Optional<double> const& start_time() const { return m_start_time; }
void set_start_time(Optional<double> const&);
// https://drafts.csswg.org/web-animations-2/#dom-animation-starttime
Optional<double> start_time_for_bindings() const
{
return start_time().map([](auto const& start_time) { return start_time.as_milliseconds(); });
}
Optional<TimeValue> start_time() const { return m_start_time; }
void set_start_time_for_bindings(Optional<double> const&);
Optional<double> current_time() const;
WebIDL::ExceptionOr<void> set_current_time(Optional<double> const&);
// https://drafts.csswg.org/web-animations-2/#dom-animation-currenttime
Optional<double> current_time_for_bindings() const
{
return current_time().map([](auto const& current_time) { return current_time.as_milliseconds(); });
}
Optional<TimeValue> current_time() const;
WebIDL::ExceptionOr<void> set_current_time_for_bindings(Optional<double> const&);
double playback_rate() const { return m_playback_rate; }
WebIDL::ExceptionOr<void> set_playback_rate(double value);
@ -94,8 +105,8 @@ public:
WebIDL::ExceptionOr<void> reverse();
void persist();
Optional<double> convert_an_animation_time_to_timeline_time(Optional<double>) const;
Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<double>) const;
Optional<TimeValue> convert_an_animation_time_to_timeline_time(Optional<TimeValue>) const;
Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<TimeValue>) const;
GC::Ptr<DOM::Document> document_for_timing() const;
void notify_timeline_time_did_change();
@ -115,7 +126,8 @@ public:
auto release_saved_cancel_time() { return move(m_saved_cancel_time); }
double associated_effect_end() const;
TimeValue associated_effect_end() const;
Optional<CSS::AnimationPlayState> last_css_animation_play_state() const { return m_last_css_animation_play_state; }
void set_last_css_animation_play_state(CSS::AnimationPlayState state) { m_last_css_animation_play_state = state; }
@ -144,7 +156,7 @@ private:
double effective_playback_rate() const;
void apply_any_pending_playback_rate();
WebIDL::ExceptionOr<void> silently_set_current_time(Optional<double>);
WebIDL::ExceptionOr<void> silently_set_current_time(Optional<TimeValue>);
void update_finished_state(DidSeek, SynchronouslyNotify);
void reset_an_animations_pending_tasks();
@ -169,13 +181,13 @@ private:
GC::Ptr<AnimationTimeline> m_timeline;
// https://www.w3.org/TR/web-animations-1/#animation-start-time
Optional<double> m_start_time {};
Optional<TimeValue> m_start_time {};
// https://www.w3.org/TR/web-animations-1/#animation-hold-time
Optional<double> m_hold_time {};
Optional<TimeValue> m_hold_time {};
// https://www.w3.org/TR/web-animations-1/#previous-current-time
Optional<double> m_previous_current_time {};
Optional<TimeValue> m_previous_current_time {};
// https://www.w3.org/TR/web-animations-1/#playback-rate
double m_playback_rate { 1.0 };
@ -205,9 +217,10 @@ private:
Optional<HTML::TaskID> m_pending_finish_microtask_id;
Optional<double> m_saved_play_time;
Optional<double> m_saved_pause_time;
Optional<double> m_saved_cancel_time;
Optional<TimeValue> m_saved_play_time;
Optional<TimeValue> m_saved_pause_time;
Optional<TimeValue> m_saved_cancel_time;
Optional<CSS::AnimationPlayState> m_last_css_animation_play_state;
};

View file

@ -11,8 +11,8 @@ interface Animation : EventTarget {
attribute DOMString id;
attribute AnimationEffect? effect;
attribute AnimationTimeline? timeline;
attribute double? startTime;
attribute double? currentTime;
[ImplementedAs=start_time_for_bindings] attribute double? startTime;
[ImplementedAs=current_time_for_bindings] attribute double? currentTime;
attribute double playbackRate;
[ImplementedAs=play_state_for_bindings] readonly attribute AnimationPlayState playState;
readonly attribute AnimationReplaceState replaceState;

View file

@ -125,9 +125,9 @@ ComputedEffectTiming AnimationEffect::get_computed_timing() const
.easing = m_timing_function.to_string(),
},
end_time(),
active_duration(),
local_time(),
end_time().as_milliseconds(),
active_duration().as_milliseconds(),
local_time().map([](auto const& time) { return time.as_milliseconds(); }),
transformed_progress(),
current_iteration(),
};
@ -314,15 +314,15 @@ AnimationDirection AnimationEffect::animation_direction() const
}
// https://www.w3.org/TR/web-animations-1/#end-time
double AnimationEffect::end_time() const
TimeValue AnimationEffect::end_time() const
{
// 1. The end time of an animation effect is the result of evaluating
// max(start delay + active duration + end delay, 0).
return max(m_start_delay.as_milliseconds() + active_duration() + m_end_delay.as_milliseconds(), 0.0);
return max(m_start_delay + active_duration() + m_end_delay, TimeValue::create_zero(associated_timeline()));
}
// https://www.w3.org/TR/web-animations-1/#local-time
Optional<double> AnimationEffect::local_time() const
Optional<TimeValue> AnimationEffect::local_time() const
{
// The local time of an animation effect at a given moment is based on the first matching condition from the
// following:
@ -339,25 +339,25 @@ Optional<double> AnimationEffect::local_time() const
}
// https://www.w3.org/TR/web-animations-1/#active-duration
double AnimationEffect::active_duration() const
TimeValue AnimationEffect::active_duration() const
{
// The active duration is calculated as follows:
// active duration = iteration duration × iteration count
// If either the iteration duration or iteration count are zero, the active duration is zero. This clarification is
// needed since the result of infinity multiplied by zero is undefined according to IEEE 754-2008.
if (m_iteration_duration.value == 0 || m_iteration_count == 0.0)
return 0.0;
return TimeValue::create_zero(associated_timeline());
return m_iteration_duration.as_milliseconds() * m_iteration_count;
return m_iteration_duration * m_iteration_count;
}
Optional<double> AnimationEffect::active_time() const
Optional<TimeValue> AnimationEffect::active_time() const
{
return active_time_using_fill(m_fill_mode);
}
// https://www.w3.org/TR/web-animations-1/#calculating-the-active-time
Optional<double> AnimationEffect::active_time_using_fill(Bindings::FillMode fill_mode) const
Optional<TimeValue> AnimationEffect::active_time_using_fill(Bindings::FillMode fill_mode) const
{
// The active time is based on the local time and start delay. However, it is only defined when the animation effect
// should produce an output and hence depends on its fill mode and phase as follows,
@ -369,7 +369,7 @@ Optional<double> AnimationEffect::active_time_using_fill(Bindings::FillMode fill
// -> If the fill mode is backwards or both,
if (fill_mode == Bindings::FillMode::Backwards || fill_mode == Bindings::FillMode::Both) {
// Return the result of evaluating max(local time - start delay, 0).
return max(local_time().value() - m_start_delay.as_milliseconds(), 0.0);
return max(local_time().value() - m_start_delay, TimeValue::create_zero(associated_timeline()));
}
// -> Otherwise,
@ -380,7 +380,7 @@ Optional<double> AnimationEffect::active_time_using_fill(Bindings::FillMode fill
// -> If the animation effect is in the active phase,
if (is_in_the_active_phase()) {
// Return the result of evaluating local time - start delay.
return local_time().value() - m_start_delay.as_milliseconds();
return local_time().value() - m_start_delay;
}
// -> If the animation effect is in the after phase,
@ -390,7 +390,7 @@ Optional<double> AnimationEffect::active_time_using_fill(Bindings::FillMode fill
// -> If the fill mode is forwards or both,
if (fill_mode == Bindings::FillMode::Forwards || fill_mode == Bindings::FillMode::Both) {
// Return the result of evaluating max(min(local time - start delay, active duration), 0).
return max(min(local_time().value() - m_start_delay.as_milliseconds(), active_duration()), 0.0);
return max(min(local_time().value() - m_start_delay, active_duration()), TimeValue::create_zero(associated_timeline()));
}
// -> Otherwise,
@ -452,17 +452,17 @@ bool AnimationEffect::is_in_effect() const
}
// https://www.w3.org/TR/web-animations-1/#before-active-boundary-time
double AnimationEffect::before_active_boundary_time() const
TimeValue AnimationEffect::before_active_boundary_time() const
{
// max(min(start delay, end time), 0)
return max(min(m_start_delay.as_milliseconds(), end_time()), 0.0);
return max(min(m_start_delay, end_time()), TimeValue::create_zero(associated_timeline()));
}
// https://www.w3.org/TR/web-animations-1/#active-after-boundary-time
double AnimationEffect::after_active_boundary_time() const
TimeValue AnimationEffect::after_active_boundary_time() const
{
// max(min(start delay + active duration, end time), 0)
return max(min(m_start_delay.as_milliseconds() + active_duration(), end_time()), 0.0);
return max(min(m_start_delay + active_duration(), end_time()), TimeValue::create_zero(associated_timeline()));
}
// https://www.w3.org/TR/web-animations-1/#animation-effect-before-phase
@ -565,7 +565,7 @@ Optional<double> AnimationEffect::overall_progress() const
// Otherwise,
else {
// Let overall progress be the result of calculating active time / iteration duration.
overall_progress = active_time.value() / m_iteration_duration.as_milliseconds();
overall_progress = active_time.value() / m_iteration_duration;
}
// 3. Return the result of calculating overall progress + iteration start.

View file

@ -116,18 +116,18 @@ public:
AnimationDirection animation_direction() const;
double end_time() const;
Optional<double> local_time() const;
double active_duration() const;
Optional<double> active_time() const;
Optional<double> active_time_using_fill(Bindings::FillMode) const;
TimeValue end_time() const;
Optional<TimeValue> local_time() const;
TimeValue active_duration() const;
Optional<TimeValue> active_time() const;
Optional<TimeValue> active_time_using_fill(Bindings::FillMode) const;
bool is_in_play() const;
bool is_current() const;
bool is_in_effect() const;
double before_active_boundary_time() const;
double after_active_boundary_time() const;
TimeValue before_active_boundary_time() const;
TimeValue after_active_boundary_time() const;
bool is_in_the_before_phase() const;
bool is_in_the_after_phase() const;

View file

@ -14,7 +14,7 @@ namespace Web::Animations {
GC_DEFINE_ALLOCATOR(AnimationTimeline);
// https://drafts.csswg.org/web-animations-1/#dom-animationtimeline-currenttime
Optional<double> AnimationTimeline::current_time() const
Optional<TimeValue> AnimationTimeline::current_time() const
{
// Returns the current time for this timeline or null if this timeline is inactive.
if (is_inactive())
@ -22,7 +22,7 @@ Optional<double> AnimationTimeline::current_time() const
return m_current_time;
}
void AnimationTimeline::set_current_time(Optional<double> value)
void AnimationTimeline::set_current_time(Optional<TimeValue> value)
{
if (value == m_current_time)
return;

View file

@ -17,7 +17,11 @@ class AnimationTimeline : public Bindings::PlatformObject {
GC_DECLARE_ALLOCATOR(AnimationTimeline);
public:
Optional<double> current_time() const;
Optional<double> current_time_for_bindings() const
{
return current_time().map([](auto const& current_time) { return current_time.as_milliseconds(); });
}
Optional<TimeValue> current_time() const;
virtual void update_current_time(double timestamp) = 0;
@ -31,7 +35,7 @@ public:
bool is_monotonically_increasing() const { return m_is_monotonically_increasing; }
// https://www.w3.org/TR/web-animations-1/#timeline-time-to-origin-relative-time
virtual Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<double>) { VERIFY_NOT_REACHED(); }
virtual Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<TimeValue>) { VERIFY_NOT_REACHED(); }
virtual bool can_convert_a_timeline_time_to_an_origin_relative_time() const { return false; }
void associate_with_animation(GC::Ref<Animation> value) { m_associated_animations.set(value); }
@ -45,10 +49,10 @@ protected:
virtual void visit_edges(Cell::Visitor&) override;
virtual void finalize() override;
void set_current_time(Optional<double> value);
void set_current_time(Optional<TimeValue> value);
// https://www.w3.org/TR/web-animations-1/#dom-animationtimeline-currenttime
Optional<double> m_current_time {};
Optional<TimeValue> m_current_time {};
// https://drafts.csswg.org/web-animations-1/#monotonically-increasing-timeline
bool m_is_monotonically_increasing { false };

View file

@ -3,7 +3,7 @@
// https://drafts.csswg.org/web-animations-2/#the-animationtimeline-interface
[Exposed=Window]
interface AnimationTimeline {
readonly attribute double? currentTime;
[ImplementedAs=current_time_for_bindings] readonly attribute double? currentTime;
[ImplementedAs=duration_for_bindings] readonly attribute CSSNumberish? duration;
[FIXME] Animation play (optional AnimationEffect? effect = null);
};

View file

@ -41,14 +41,18 @@ WebIDL::ExceptionOr<GC::Ref<DocumentTimeline>> DocumentTimeline::construct_impl(
}
// https://www.w3.org/TR/web-animations-1/#ref-for-timeline-time-to-origin-relative-time
Optional<double> DocumentTimeline::convert_a_timeline_time_to_an_origin_relative_time(Optional<double> timeline_time)
Optional<double> DocumentTimeline::convert_a_timeline_time_to_an_origin_relative_time(Optional<TimeValue> timeline_time)
{
// To convert a timeline time, timeline time, to an origin-relative time for a document timeline, timeline, return
// the sum of the timeline time and timelines origin time. If timeline is inactive, return an unresolved time
// value.
if (is_inactive() || !timeline_time.has_value())
return {};
return timeline_time.value() + m_origin_time;
// NB: We know the timeline time of a DocumentTimeline is always in milliseconds because it's only ever set from
// update_current_time()
VERIFY(timeline_time->type == TimeValue::Type::Milliseconds);
return timeline_time->value + m_origin_time;
}
// https://drafts.csswg.org/web-animations-1/#document-timeline
@ -58,7 +62,7 @@ void DocumentTimeline::update_current_time(double timestamp)
// as a fixed offset from the now timestamp provided each time the update animations and send events procedure is
// run. This fixed offset is equal to the current time of the default document timeline when this timelines current
// time was zero, and is thus referred to as the document timelines origin time.
set_current_time(timestamp - m_origin_time);
set_current_time(TimeValue { TimeValue::Type::Milliseconds, timestamp - m_origin_time });
// https://drafts.csswg.org/web-animations-1/#ref-for-active-timeline
// After a document timeline becomes active, it is monotonically increasing.

View file

@ -31,7 +31,7 @@ public:
virtual void update_current_time(double timestamp) override;
virtual bool is_inactive() const override;
virtual Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<double>) override;
virtual Optional<double> convert_a_timeline_time_to_an_origin_relative_time(Optional<TimeValue>) override;
virtual bool can_convert_a_timeline_time_to_an_origin_relative_time() const override { return true; }
private:

View file

@ -27,6 +27,55 @@ struct TimeValue {
Type type;
double value;
TimeValue operator-() const
{
return { type, -value };
}
TimeValue operator*(double other) const
{
return { type, value * other };
}
TimeValue operator-(TimeValue const& other) const
{
VERIFY(type == other.type);
return { type, value - other.value };
}
TimeValue operator+(TimeValue const& other) const
{
VERIFY(type == other.type);
return { type, value + other.value };
}
TimeValue operator/(double divisor) const
{
return { type, value / divisor };
}
double operator/(TimeValue const& other) const
{
VERIFY(type == other.type);
return value / other.value;
}
int operator<=>(TimeValue const& other) const
{
VERIFY(type == other.type);
if (value < other.value)
return -1;
if (value > other.value)
return 1;
return 0;
}
bool operator==(TimeValue const& other) const
{
return type == other.type && value == other.value;
}
// FIXME: This method is temporary as we migrate all CSS timing to use TimeValue.
double as_milliseconds() const
{
@ -61,3 +110,15 @@ struct NullableCSSNumberish : FlattenVariant<Variant<Empty>, CSS::CSSNumberish>
};
}
template<>
struct AK::Formatter<Web::Animations::TimeValue> : Formatter<FormatString> {
ErrorOr<void> format(FormatBuilder& builder, Web::Animations::TimeValue const& time)
{
switch (time.type) {
case Web::Animations::TimeValue::Type::Milliseconds:
return Formatter<FormatString>::format(builder, "{}ms"sv, time.value);
}
return {};
}
};

View file

@ -1074,7 +1074,9 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
};
// For each element and property, the implementation must act as follows:
auto style_change_event_time = m_document->timeline()->current_time().value();
// NB: We know that a DocumentTimeline's current time is always in milliseconds
VERIFY(m_document->timeline()->current_time()->type == Animations::TimeValue::Type::Milliseconds);
auto style_change_event_time = m_document->timeline()->current_time()->value;
// FIXME: Add some transition helpers to AbstractElement.
auto& element = abstract_element.element();

View file

@ -32,6 +32,7 @@
#include <LibWeb/Animations/AnimationPlaybackEvent.h>
#include <LibWeb/Animations/AnimationTimeline.h>
#include <LibWeb/Animations/DocumentTimeline.h>
#include <LibWeb/Animations/TimeValue.h>
#include <LibWeb/Bindings/DocumentPrototype.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
@ -2646,7 +2647,7 @@ void Document::dispatch_events_for_transition(GC::Ref<CSS::CSSTransition> transi
if (!transition->current_time().has_value()) {
// If the transition has an unresolved current time,
// The transition phase is idle.
} else if (transition->current_time().value() < 0.0) {
} else if (transition->current_time()->value < 0) {
// If the transition has a current time < 0,
// The transition phase is before.
transition_phase = Phase::Before;
@ -2680,19 +2681,27 @@ void Document::dispatch_events_for_transition(GC::Ref<CSS::CSSTransition> transi
auto effect = transition->effect();
double elapsed_time = [&]() {
Animations::TimeValue elapsed_time = [&]() {
if (interval == Interval::Start)
return max(min(-effect->start_delay().as_milliseconds(), effect->active_duration()), 0) / 1000;
return max(min(-effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(transition->timeline()));
if (interval == Interval::End)
return max(min(transition->associated_effect_end() - effect->start_delay().as_milliseconds(), effect->active_duration()), 0) / 1000;
return max(min(transition->associated_effect_end() - effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(transition->timeline()));
if (interval == Interval::ActiveTime) {
// The active time of the animation at the moment it was canceled calculated using a fill mode of both.
// FIXME: Compute this properly.
return 0.0;
return Animations::TimeValue::create_zero(transition->timeline());
}
VERIFY_NOT_REACHED();
}();
double elapsed_time_output;
switch (elapsed_time.type) {
case Animations::TimeValue::Type::Milliseconds:
elapsed_time_output = elapsed_time.value / 1000;
break;
}
append_pending_animation_event({
.event = CSS::TransitionEvent::create(
transition->owning_element()->element().realm(),
@ -2700,7 +2709,7 @@ void Document::dispatch_events_for_transition(GC::Ref<CSS::CSSTransition> transi
CSS::TransitionEventInit {
{ .bubbles = true },
MUST(String::from_utf8(transition->transition_property())),
elapsed_time,
elapsed_time_output,
transition->owning_element()->pseudo_element().map([](auto it) {
return MUST(String::formatted("::{}", CSS::pseudo_element_name(it)));
})
@ -2785,8 +2794,13 @@ void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::An
auto owning_element = css_animation.owning_element();
auto dispatch_event = [&](FlyString const& name, double elapsed_time_ms) {
auto elapsed_time_seconds = elapsed_time_ms / 1000;
auto dispatch_event = [&](FlyString const& name, Animations::TimeValue elapsed_time) {
double elapsed_time_output;
switch (elapsed_time.type) {
case Animations::TimeValue::Type::Milliseconds:
elapsed_time_output = elapsed_time.value / 1000;
break;
}
append_pending_animation_event({
.event = CSS::AnimationEvent::create(
@ -2795,7 +2809,7 @@ void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::An
{
{ .bubbles = true },
css_animation.animation_name(),
elapsed_time_seconds,
elapsed_time_output,
owning_element->pseudo_element().map([](auto it) {
return MUST(String::formatted("::{}", CSS::pseudo_element_name(it)));
})
@ -2810,10 +2824,10 @@ void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::An
// For calculating the elapsedTime of each event, the following definitions are used:
// - interval start = max(min(-start delay, active duration), 0)
auto interval_start = max(min(-effect->start_delay().as_milliseconds(), effect->active_duration()), 0.0);
auto interval_start = max(min(-effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(animation->timeline()));
// - interval end = max(min(associated effect end - start delay, active duration), 0)
auto interval_end = max(min(effect->end_time() - effect->start_delay().as_milliseconds(), effect->active_duration()), 0.0);
auto interval_end = max(min(effect->end_time() - effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(animation->timeline()));
switch (previous_phase) {
case Animations::AnimationEffect::Phase::Before:
@ -2841,9 +2855,7 @@ void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::An
auto iteration_boundary = previous_current_iteration > current_iteration ? current_iteration + 1 : current_iteration;
// 3. The elapsed time is the result of evaluating (iteration boundary - iteration start) × iteration duration).
auto iteration_duration_variant = effect->iteration_duration();
auto iteration_duration = iteration_duration_variant.as_milliseconds();
auto elapsed_time = (iteration_boundary - effect->iteration_start()) * iteration_duration;
auto elapsed_time = effect->iteration_duration() * (iteration_boundary - effect->iteration_start());
dispatch_event(HTML::EventNames::animationiteration, elapsed_time);
}
@ -2863,7 +2875,7 @@ void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::An
if (current_phase == Animations::AnimationEffect::Phase::Idle && previous_phase != Animations::AnimationEffect::Phase::Idle && previous_phase != Animations::AnimationEffect::Phase::After) {
// FIXME: Calculate a non-zero time when the animation is cancelled by means other than calling cancel()
auto cancel_time = animation->release_saved_cancel_time().value_or(0.0);
auto cancel_time = animation->release_saved_cancel_time().value_or(Animations::TimeValue::create_zero(animation->timeline()));
dispatch_event(HTML::EventNames::animationcancel, cancel_time);
}
@ -5448,8 +5460,8 @@ void Document::remove_replaced_animations()
// - Set removeEvents timelineTime attribute to the current time of the timeline with which animation is
// associated.
Animations::AnimationPlaybackEventInit init;
init.current_time = animation->current_time();
init.timeline_time = animation->timeline()->current_time();
init.current_time = animation->current_time()->as_milliseconds();
init.timeline_time = animation->timeline()->current_time()->as_milliseconds();
auto remove_event = Animations::AnimationPlaybackEvent::create(realm(), HTML::EventNames::remove, init);
// - If animation has a document for timing, then append removeEvent to its document for timing's pending
@ -5461,7 +5473,7 @@ void Document::remove_replaced_animations()
.event = remove_event,
.animation = animation,
.target = animation,
.scheduled_event_time = animation->timeline()->convert_a_timeline_time_to_an_origin_relative_time(init.timeline_time),
.scheduled_event_time = animation->timeline()->convert_a_timeline_time_to_an_origin_relative_time(animation->timeline()->current_time()),
};
document->append_pending_animation_event(pending_animation_event);
}

View file

@ -21,13 +21,13 @@ void InternalAnimationTimeline::update_current_time(double)
void InternalAnimationTimeline::set_time(Optional<double> time)
{
set_current_time(time);
set_current_time(time.map([](double value) -> Animations::TimeValue { return { Animations::TimeValue::Type::Milliseconds, value }; }));
}
InternalAnimationTimeline::InternalAnimationTimeline(JS::Realm& realm)
: AnimationTimeline(realm)
{
m_current_time = 0.0;
m_current_time = { Animations::TimeValue::Type::Milliseconds, 0.0 };
m_is_monotonically_increasing = true;
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();