mirror of
https://github.com/zebrajr/ladybird.git
synced 2025-12-06 00:19:53 +01:00
LibWeb: Separate use time easing functions from EasingStyleValue
In the future there will be different methods of creating these use-time easing functions (e.g. from `KeywordStyleValue`s)
This commit is contained in:
parent
0e30de82cc
commit
95e26819d9
|
|
@ -12,7 +12,6 @@
|
|||
#include <LibWeb/Animations/DocumentTimeline.h>
|
||||
#include <LibWeb/Animations/PseudoElementParsing.h>
|
||||
#include <LibWeb/CSS/CSSTransition.h>
|
||||
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
|
|
@ -171,9 +170,8 @@ void Animatable::add_transitioned_properties(Optional<CSS::PseudoElement> pseudo
|
|||
duration = resolved_time.value().to_milliseconds();
|
||||
}
|
||||
}
|
||||
auto timing_function = timing_functions[i]->is_easing() ? timing_functions[i]->as_easing().function() : CSS::EasingStyleValue::CubicBezier::ease();
|
||||
auto timing_function = CSS::EasingFunction::from_style_value(timing_functions[i]);
|
||||
auto transition_behavior = CSS::keyword_to_transition_behavior(transition_behaviors[i]->to_keyword()).value_or(CSS::TransitionBehavior::Normal);
|
||||
VERIFY(timing_functions[i]->is_easing());
|
||||
transition.transition_attributes.empend(delay, duration, timing_function, transition_behavior);
|
||||
|
||||
for (auto const& property : properties[i])
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public:
|
|||
struct TransitionAttributes {
|
||||
double delay;
|
||||
double duration;
|
||||
CSS::EasingStyleValue::Function timing_function;
|
||||
CSS::EasingFunction timing_function;
|
||||
CSS::TransitionBehavior transition_behavior;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ EffectTiming AnimationEffect::get_timing() const
|
|||
.iterations = m_iteration_count,
|
||||
.duration = m_iteration_duration,
|
||||
.direction = m_playback_direction,
|
||||
.easing = m_timing_function.to_string(CSS::SerializationMode::Normal),
|
||||
.easing = m_timing_function.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ ComputedEffectTiming AnimationEffect::get_computed_timing() const
|
|||
.iterations = m_iteration_count,
|
||||
.duration = duration,
|
||||
.direction = m_playback_direction,
|
||||
.easing = m_timing_function.to_string(CSS::SerializationMode::Normal),
|
||||
.easing = m_timing_function.to_string(),
|
||||
},
|
||||
|
||||
end_time(),
|
||||
|
|
@ -158,12 +158,11 @@ WebIDL::ExceptionOr<void> AnimationEffect::update_timing(OptionalEffectTiming ti
|
|||
|
||||
// 4. If the easing member of input exists but cannot be parsed using the <easing-function> production
|
||||
// [CSS-EASING-1], throw a TypeError and abort this procedure.
|
||||
RefPtr<CSS::StyleValue const> easing_value;
|
||||
Optional<CSS::EasingFunction> easing_value;
|
||||
if (timing.easing.has_value()) {
|
||||
easing_value = parse_easing_string(timing.easing.value());
|
||||
if (!easing_value)
|
||||
if (!easing_value.has_value())
|
||||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid easing function"sv };
|
||||
VERIFY(easing_value->is_easing());
|
||||
}
|
||||
|
||||
// 5. Assign each member that exists in input to the corresponding timing property of effect as follows:
|
||||
|
|
@ -197,8 +196,8 @@ WebIDL::ExceptionOr<void> AnimationEffect::update_timing(OptionalEffectTiming ti
|
|||
m_playback_direction = timing.direction.value();
|
||||
|
||||
// - easing → timing function
|
||||
if (easing_value)
|
||||
m_timing_function = easing_value->as_easing().function();
|
||||
if (easing_value.has_value())
|
||||
m_timing_function = easing_value.value();
|
||||
|
||||
if (auto animation = m_associated_animation)
|
||||
animation->effect_timing_changed({});
|
||||
|
|
@ -604,11 +603,11 @@ Optional<double> AnimationEffect::transformed_progress() const
|
|||
return m_timing_function.evaluate_at(directed_progress.value(), before_flag);
|
||||
}
|
||||
|
||||
RefPtr<CSS::StyleValue const> AnimationEffect::parse_easing_string(StringView value)
|
||||
Optional<CSS::EasingFunction> AnimationEffect::parse_easing_string(StringView value)
|
||||
{
|
||||
if (auto style_value = parse_css_value(CSS::Parser::ParsingParams(), value, CSS::PropertyID::AnimationTimingFunction)) {
|
||||
if (style_value->is_easing())
|
||||
return style_value;
|
||||
return CSS::EasingFunction::from_style_value(*style_value);
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <AK/Variant.h>
|
||||
#include <LibWeb/Bindings/AnimationEffectPrototype.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/CSS/EasingFunction.h>
|
||||
#include <LibWeb/CSS/Enums.h>
|
||||
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ class AnimationEffect : public Bindings::PlatformObject {
|
|||
GC_DECLARE_ALLOCATOR(AnimationEffect);
|
||||
|
||||
public:
|
||||
static RefPtr<CSS::StyleValue const> parse_easing_string(StringView value);
|
||||
static Optional<CSS::EasingFunction> parse_easing_string(StringView value);
|
||||
|
||||
EffectTiming get_timing() const;
|
||||
ComputedEffectTiming get_computed_timing() const;
|
||||
|
|
@ -106,8 +107,8 @@ public:
|
|||
Bindings::PlaybackDirection playback_direction() const { return m_playback_direction; }
|
||||
void set_playback_direction(Bindings::PlaybackDirection playback_direction) { m_playback_direction = playback_direction; }
|
||||
|
||||
CSS::EasingStyleValue::Function const& timing_function() { return m_timing_function; }
|
||||
void set_timing_function(CSS::EasingStyleValue::Function value) { m_timing_function = move(value); }
|
||||
CSS::EasingFunction const& timing_function() { return m_timing_function; }
|
||||
void set_timing_function(CSS::EasingFunction value) { m_timing_function = move(value); }
|
||||
|
||||
GC::Ptr<Animation> associated_animation() const { return m_associated_animation; }
|
||||
void set_associated_animation(GC::Ptr<Animation> value);
|
||||
|
|
@ -193,7 +194,7 @@ protected:
|
|||
GC::Ptr<Animation> m_associated_animation {};
|
||||
|
||||
// https://www.w3.org/TR/web-animations-1/#time-transformations
|
||||
CSS::EasingStyleValue::Function m_timing_function { CSS::EasingStyleValue::Linear::identity() };
|
||||
CSS::EasingFunction m_timing_function { CSS::EasingFunction::from_style_value(CSS::EasingStyleValue::create(CSS::EasingStyleValue::Linear::identity())) };
|
||||
|
||||
// Used for calculating transitions in StyleComputer
|
||||
Phase m_previous_phase { Phase::Idle };
|
||||
|
|
|
|||
|
|
@ -560,10 +560,10 @@ static WebIDL::ExceptionOr<Vector<BaseKeyframe>> process_a_keyframes_argument(JS
|
|||
auto easing_string = keyframe.easing.get<String>();
|
||||
auto easing_value = AnimationEffect::parse_easing_string(easing_string);
|
||||
|
||||
if (!easing_value)
|
||||
if (!easing_value.has_value())
|
||||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid animation easing value: \"{}\"", easing_string)) };
|
||||
|
||||
keyframe.easing.set(NonnullRefPtr<CSS::StyleValue const> { *easing_value });
|
||||
keyframe.easing.set(easing_value.value());
|
||||
}
|
||||
|
||||
// 9. Parse each of the values in unused easings using the CSS syntax defined for easing member of the EffectTiming
|
||||
|
|
@ -571,7 +571,7 @@ static WebIDL::ExceptionOr<Vector<BaseKeyframe>> process_a_keyframes_argument(JS
|
|||
for (auto& unused_easing : unused_easings) {
|
||||
auto easing_string = unused_easing.get<String>();
|
||||
auto easing_value = AnimationEffect::parse_easing_string(easing_string);
|
||||
if (!easing_value)
|
||||
if (!easing_value.has_value())
|
||||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid animation easing value: \"{}\"", easing_string)) };
|
||||
}
|
||||
|
||||
|
|
@ -827,8 +827,8 @@ WebIDL::ExceptionOr<GC::RootVector<JS::Object*>> KeyframeEffect::get_keyframes()
|
|||
auto object = JS::Object::create(realm, realm.intrinsics().object_prototype());
|
||||
TRY(object->set(vm.names.offset, keyframe.offset.has_value() ? JS::Value(keyframe.offset.value()) : JS::js_null(), ShouldThrowExceptions::Yes));
|
||||
TRY(object->set(vm.names.computedOffset, JS::Value(keyframe.computed_offset.value()), ShouldThrowExceptions::Yes));
|
||||
auto easing_value = keyframe.easing.get<NonnullRefPtr<CSS::StyleValue const>>();
|
||||
TRY(object->set(vm.names.easing, JS::PrimitiveString::create(vm, easing_value->to_string(CSS::SerializationMode::Normal)), ShouldThrowExceptions::Yes));
|
||||
auto easing_value = keyframe.easing.get<CSS::EasingFunction>();
|
||||
TRY(object->set(vm.names.easing, JS::PrimitiveString::create(vm, easing_value.to_string()), ShouldThrowExceptions::Yes));
|
||||
|
||||
if (keyframe.composite == Bindings::CompositeOperationOrAuto::Replace) {
|
||||
TRY(object->set(vm.names.composite, JS::PrimitiveString::create(vm, "replace"sv), ShouldThrowExceptions::Yes));
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
namespace Web::Animations {
|
||||
|
||||
using EasingValue = Variant<String, NonnullRefPtr<CSS::StyleValue const>>;
|
||||
using EasingValue = Variant<String, CSS::EasingFunction>;
|
||||
|
||||
// https://www.w3.org/TR/web-animations-1/#the-keyframeeffectoptions-dictionary
|
||||
struct KeyframeEffectOptions : public EffectTiming {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ set(SOURCES
|
|||
CSS/CSSVariableReferenceValue.cpp
|
||||
CSS/Descriptor.cpp
|
||||
CSS/Display.cpp
|
||||
CSS/EasingFunction.cpp
|
||||
CSS/EdgeRect.cpp
|
||||
CSS/Fetch.cpp
|
||||
CSS/Flex.cpp
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
#include <LibWeb/CSS/Interpolation.h>
|
||||
#include <LibWeb/CSS/PropertyID.h>
|
||||
#include <LibWeb/CSS/PseudoElement.h>
|
||||
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/StyleValue.h>
|
||||
#include <LibWeb/CSS/Time.h>
|
||||
|
||||
|
|
|
|||
259
Libraries/LibWeb/CSS/EasingFunction.cpp
Normal file
259
Libraries/LibWeb/CSS/EasingFunction.cpp
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "EasingFunction.h"
|
||||
#include <AK/BinarySearch.h>
|
||||
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
// https://drafts.csswg.org/css-easing/#linear-easing-function-output
|
||||
double LinearEasingFunction::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
// To calculate linear easing output progress for a given linear easing function func,
|
||||
// an input progress value inputProgress, and an optional before flag (defaulting to false),
|
||||
// perform the following:
|
||||
|
||||
// 1. Let points be func’s control points.
|
||||
|
||||
// 2. If points holds only a single item, return the output progress value of that item.
|
||||
if (control_points.size() == 1)
|
||||
return control_points[0].output;
|
||||
|
||||
// 3. If inputProgress matches the input progress value of the first point in points,
|
||||
// and the before flag is true, return the first point’s output progress value.
|
||||
if (input_progress == control_points[0].input.value() && before_flag)
|
||||
return control_points[0].output;
|
||||
|
||||
// 4. If inputProgress matches the input progress value of at least one point in points,
|
||||
// return the output progress value of the last such point.
|
||||
auto maybe_match = control_points.last_matching([&](auto& stop) { return input_progress == stop.input; });
|
||||
if (maybe_match.has_value())
|
||||
return maybe_match->output;
|
||||
|
||||
// 5. Otherwise, find two control points in points, A and B, which will be used for interpolation:
|
||||
ControlPoint A;
|
||||
ControlPoint B;
|
||||
|
||||
if (input_progress < control_points[0].input.value()) {
|
||||
// 1. If inputProgress is smaller than any input progress value in points,
|
||||
// let A and B be the first two items in points.
|
||||
// If A and B have the same input progress value, return A’s output progress value.
|
||||
A = control_points[0];
|
||||
B = control_points[1];
|
||||
if (A.input == B.input.value())
|
||||
return A.output;
|
||||
} else if (input_progress > control_points.last().input.value()) {
|
||||
// 2. If inputProgress is larger than any input progress value in points,
|
||||
// let A and B be the last two items in points.
|
||||
// If A and B have the same input progress value, return B’s output progress value.
|
||||
A = control_points[control_points.size() - 2];
|
||||
B = control_points[control_points.size() - 1];
|
||||
if (A.input == B.input.value())
|
||||
return B.output;
|
||||
} else {
|
||||
// 3. Otherwise, let A be the last control point whose input progress value is smaller than inputProgress,
|
||||
// and let B be the first control point whose input progress value is larger than inputProgress.
|
||||
A = control_points.last_matching([&](ControlPoint const& stop) { return stop.input.value() < input_progress; }).value();
|
||||
B = control_points.first_matching([&](ControlPoint const& stop) { return stop.input.value() > input_progress; }).value();
|
||||
}
|
||||
|
||||
// 6. Linearly interpolate (or extrapolate) inputProgress along the line defined by A and B, and return the result.
|
||||
auto factor = (input_progress - A.input.value()) / (B.input.value() - A.input.value());
|
||||
return A.output + factor * (B.output - A.output);
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
|
||||
double CubicBezierEasingFunction::evaluate_at(double input_progress, bool) const
|
||||
{
|
||||
constexpr static auto 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);
|
||||
};
|
||||
|
||||
// For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
|
||||
// at the closest endpoint as follows:
|
||||
|
||||
// - For input progress values less than zero,
|
||||
if (input_progress < 0.0) {
|
||||
// 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
|
||||
// tangent.
|
||||
if (x1 > 0.0)
|
||||
return y1 / x1 * input_progress;
|
||||
|
||||
// 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
|
||||
// the tangent.
|
||||
if (x2 > 0.0)
|
||||
return y2 / x2 * input_progress;
|
||||
|
||||
// 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// - For input progress values greater than one,
|
||||
if (input_progress > 1.0) {
|
||||
// 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
|
||||
if (x2 < 1.0)
|
||||
return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
|
||||
|
||||
// 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
|
||||
// tangent.
|
||||
if (x1 < 1.0)
|
||||
return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
|
||||
|
||||
// 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
|
||||
// "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
|
||||
|
||||
auto x = input_progress;
|
||||
|
||||
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 >= NumericLimits<double>::epsilon())
|
||||
return 1;
|
||||
if (x - sample.x <= NumericLimits<double>::epsilon())
|
||||
return -1;
|
||||
return 0;
|
||||
}))
|
||||
return found->y;
|
||||
|
||||
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.last().t;
|
||||
auto last_x = m_cached_x_samples.last().x;
|
||||
while (last_x <= x && last_t < 1.0) {
|
||||
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 >= NumericLimits<double>::epsilon())
|
||||
return 1;
|
||||
if (x - sample.x <= NumericLimits<double>::epsilon())
|
||||
return -1;
|
||||
return 0;
|
||||
}))
|
||||
return found->y;
|
||||
}
|
||||
|
||||
// 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 sample1.y + factor * (sample2.y - sample1.y);
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/css-easing-1/#step-easing-algo
|
||||
double StepsEasingFunction::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
auto current_step = floor(input_progress * interval_count);
|
||||
|
||||
// 2. If the step position property is one of:
|
||||
// - jump-start,
|
||||
// - jump-both,
|
||||
// increment current step by one.
|
||||
if (position == StepPosition::JumpStart || position == StepPosition::Start || position == StepPosition::JumpBoth)
|
||||
current_step += 1;
|
||||
|
||||
// 3. If both of the following conditions are true:
|
||||
// - the before flag is set, and
|
||||
// - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
|
||||
// decrement current step by one.
|
||||
auto step_progress = input_progress * interval_count;
|
||||
if (before_flag && trunc(step_progress) == step_progress)
|
||||
current_step -= 1;
|
||||
|
||||
// 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
|
||||
if (input_progress >= 0.0 && current_step < 0.0)
|
||||
current_step = 0.0;
|
||||
|
||||
// 5. Calculate jumps based on the step position as follows:
|
||||
|
||||
// jump-start or jump-end -> steps
|
||||
// jump-none -> steps - 1
|
||||
// jump-both -> steps + 1
|
||||
auto jumps = interval_count;
|
||||
if (position == StepPosition::JumpNone) {
|
||||
jumps--;
|
||||
} else if (position == StepPosition::JumpBoth) {
|
||||
jumps++;
|
||||
}
|
||||
|
||||
// 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
|
||||
if (input_progress <= 1.0 && current_step > jumps)
|
||||
current_step = jumps;
|
||||
|
||||
// 7. The output progress value is current step / jumps.
|
||||
return current_step / jumps;
|
||||
}
|
||||
|
||||
EasingFunction EasingFunction::from_style_value(StyleValue const& style_value)
|
||||
{
|
||||
if (style_value.is_easing()) {
|
||||
return style_value.as_easing().function().visit(
|
||||
[](EasingStyleValue::Linear const& linear) -> EasingFunction {
|
||||
Vector<LinearEasingFunction::ControlPoint> resolved_control_points;
|
||||
|
||||
for (auto const& control_point : linear.stops)
|
||||
resolved_control_points.append({ control_point.input, control_point.output });
|
||||
|
||||
return LinearEasingFunction { resolved_control_points, linear.to_string(SerializationMode::ResolvedValue) };
|
||||
},
|
||||
[](EasingStyleValue::CubicBezier const& cubic_bezier) -> EasingFunction {
|
||||
auto resolved_x1 = clamp(cubic_bezier.x1.resolved({}).value_or(0.0), 0.0, 1.0);
|
||||
auto resolved_y1 = cubic_bezier.y1.resolved({}).value_or(0.0);
|
||||
auto resolved_x2 = clamp(cubic_bezier.x2.resolved({}).value_or(0.0), 0.0, 1.0);
|
||||
auto resolved_y2 = cubic_bezier.y2.resolved({}).value_or(0.0);
|
||||
|
||||
return CubicBezierEasingFunction { resolved_x1, resolved_y1, resolved_x2, resolved_y2, cubic_bezier.to_string(SerializationMode::Normal) };
|
||||
},
|
||||
[](EasingStyleValue::Steps const& steps) -> EasingFunction {
|
||||
auto resolved_interval_count = max(steps.number_of_intervals.resolved({}).value_or(1), steps.position == StepPosition::JumpNone ? 2 : 1);
|
||||
|
||||
return StepsEasingFunction { resolved_interval_count, steps.position, steps.to_string(SerializationMode::ResolvedValue) };
|
||||
});
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
double EasingFunction::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
return visit(
|
||||
[&](auto const& function) {
|
||||
return function.evaluate_at(input_progress, before_flag);
|
||||
});
|
||||
}
|
||||
|
||||
String EasingFunction::to_string() const
|
||||
{
|
||||
return visit(
|
||||
[](auto const& function) {
|
||||
return function.stringified;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
60
Libraries/LibWeb/CSS/EasingFunction.h
Normal file
60
Libraries/LibWeb/CSS/EasingFunction.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/CSS/Enums.h>
|
||||
#include <LibWeb/CSS/StyleValues/StyleValue.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
struct LinearEasingFunction {
|
||||
struct ControlPoint {
|
||||
Optional<double> input;
|
||||
double output;
|
||||
};
|
||||
|
||||
Vector<ControlPoint> control_points;
|
||||
String stringified;
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
};
|
||||
|
||||
struct CubicBezierEasingFunction {
|
||||
double x1;
|
||||
double y1;
|
||||
double x2;
|
||||
double y2;
|
||||
String stringified;
|
||||
|
||||
struct CachedSample {
|
||||
double x;
|
||||
double y;
|
||||
double t;
|
||||
};
|
||||
|
||||
mutable Vector<CachedSample> m_cached_x_samples {};
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
};
|
||||
|
||||
struct StepsEasingFunction {
|
||||
long interval_count;
|
||||
StepPosition position;
|
||||
String stringified;
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
};
|
||||
|
||||
struct EasingFunction : public Variant<LinearEasingFunction, CubicBezierEasingFunction, StepsEasingFunction> {
|
||||
using Variant::Variant;
|
||||
static EasingFunction from_style_value(StyleValue const&);
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
String to_string() const;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1277,9 +1277,9 @@ static void apply_animation_properties(DOM::Document& document, CascadedProperti
|
|||
play_state = *play_state_value;
|
||||
}
|
||||
|
||||
CSS::EasingStyleValue::Function timing_function { CSS::EasingStyleValue::CubicBezier::ease() };
|
||||
EasingFunction timing_function = EasingFunction::from_style_value(EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()));
|
||||
if (auto timing_property = cascaded_properties.property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing())
|
||||
timing_function = timing_property->as_easing().function();
|
||||
timing_function = EasingFunction::from_style_value(timing_property->as_easing());
|
||||
|
||||
Bindings::CompositeOperation composite_operation { Bindings::CompositeOperation::Replace };
|
||||
if (auto composite_property = cascaded_properties.property(PropertyID::AnimationComposition); composite_property) {
|
||||
|
|
|
|||
|
|
@ -114,61 +114,6 @@ EasingStyleValue::Linear::Linear(Vector<EasingStyleValue::Linear::Stop> stops)
|
|||
this->stops = move(stops);
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-easing/#linear-easing-function-output
|
||||
double EasingStyleValue::Linear::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
// To calculate linear easing output progress for a given linear easing function func,
|
||||
// an input progress value inputProgress, and an optional before flag (defaulting to false),
|
||||
// perform the following:
|
||||
|
||||
// 1. Let points be func’s control points.
|
||||
// 2. If points holds only a single item, return the output progress value of that item.
|
||||
if (stops.size() == 1)
|
||||
return stops[0].output;
|
||||
|
||||
// 3. If inputProgress matches the input progress value of the first point in points,
|
||||
// and the before flag is true, return the first point’s output progress value.
|
||||
if (input_progress == stops[0].input.value() && before_flag)
|
||||
return stops[0].output;
|
||||
|
||||
// 4. If inputProgress matches the input progress value of at least one point in points,
|
||||
// return the output progress value of the last such point.
|
||||
auto maybe_match = stops.last_matching([&](auto& stop) { return input_progress == stop.input.value(); });
|
||||
if (maybe_match.has_value())
|
||||
return maybe_match->output;
|
||||
|
||||
// 5. Otherwise, find two control points in points, A and B, which will be used for interpolation:
|
||||
Stop A;
|
||||
Stop B;
|
||||
|
||||
if (input_progress < stops[0].input.value()) {
|
||||
// 1. If inputProgress is smaller than any input progress value in points,
|
||||
// let A and B be the first two items in points.
|
||||
// If A and B have the same input progress value, return A’s output progress value.
|
||||
A = stops[0];
|
||||
B = stops[1];
|
||||
if (A.input == B.input)
|
||||
return A.output;
|
||||
} else if (input_progress > stops.last().input.value()) {
|
||||
// 2. If inputProgress is larger than any input progress value in points,
|
||||
// let A and B be the last two items in points.
|
||||
// If A and B have the same input progress value, return B’s output progress value.
|
||||
A = stops[stops.size() - 2];
|
||||
B = stops[stops.size() - 1];
|
||||
if (A.input == B.input)
|
||||
return B.output;
|
||||
} else {
|
||||
// 3. Otherwise, let A be the last control point whose input progress value is smaller than inputProgress,
|
||||
// and let B be the first control point whose input progress value is larger than inputProgress.
|
||||
A = stops.last_matching([&](auto& stop) { return stop.input.value() < input_progress; }).value();
|
||||
B = stops.first_matching([&](auto& stop) { return stop.input.value() > input_progress; }).value();
|
||||
}
|
||||
|
||||
// 6. Linearly interpolate (or extrapolate) inputProgress along the line defined by A and B, and return the result.
|
||||
auto factor = (input_progress - A.input.value()) / (B.input.value() - A.input.value());
|
||||
return A.output + factor * (B.output - A.output);
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
|
||||
String EasingStyleValue::Linear::to_string(SerializationMode) const
|
||||
{
|
||||
|
|
@ -211,111 +156,6 @@ String EasingStyleValue::Linear::to_string(SerializationMode) const
|
|||
return MUST(builder.to_string());
|
||||
}
|
||||
|
||||
double EasingStyleValue::CubicBezier::evaluate_at(double input_progress, bool) const
|
||||
{
|
||||
constexpr static auto 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);
|
||||
};
|
||||
|
||||
// https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
|
||||
auto resolved_x1 = clamp(x1.resolved({}).value_or(0.0), 0.0, 1.0);
|
||||
auto resolved_y1 = y1.resolved({}).value_or(0.0);
|
||||
auto resolved_x2 = clamp(x2.resolved({}).value_or(0.0), 0.0, 1.0);
|
||||
auto resolved_y2 = y2.resolved({}).value_or(0.0);
|
||||
|
||||
// For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
|
||||
// at the closest endpoint as follows:
|
||||
|
||||
// - For input progress values less than zero,
|
||||
if (input_progress < 0.0) {
|
||||
// 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
|
||||
// tangent.
|
||||
if (resolved_x1 > 0.0)
|
||||
return resolved_y1 / resolved_x1 * input_progress;
|
||||
|
||||
// 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
|
||||
// the tangent.
|
||||
if (resolved_x2 > 0.0)
|
||||
return resolved_y2 / resolved_x2 * input_progress;
|
||||
|
||||
// 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// - For input progress values greater than one,
|
||||
if (input_progress > 1.0) {
|
||||
// 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
|
||||
if (resolved_x2 < 1.0)
|
||||
return (1.0 - resolved_y2) / (1.0 - resolved_x2) * (input_progress - 1.0) + 1.0;
|
||||
|
||||
// 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
|
||||
// tangent.
|
||||
if (resolved_x1 < 1.0)
|
||||
return (1.0 - resolved_y1) / (1.0 - resolved_x1) * (input_progress - 1.0) + 1.0;
|
||||
|
||||
// 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
|
||||
// "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
|
||||
|
||||
auto x = input_progress;
|
||||
|
||||
auto solve = [&](auto t) {
|
||||
auto x = cubic_bezier_at(resolved_x1, resolved_x2, t);
|
||||
auto y = cubic_bezier_at(resolved_y1, resolved_y2, t);
|
||||
return CubicBezier::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 >= NumericLimits<double>::epsilon())
|
||||
return 1;
|
||||
if (x - sample.x <= NumericLimits<double>::epsilon())
|
||||
return -1;
|
||||
return 0;
|
||||
}))
|
||||
return found->y;
|
||||
|
||||
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.last().t;
|
||||
auto last_x = m_cached_x_samples.last().x;
|
||||
while (last_x <= x && last_t < 1.0) {
|
||||
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 >= NumericLimits<double>::epsilon())
|
||||
return 1;
|
||||
if (x - sample.x <= NumericLimits<double>::epsilon())
|
||||
return -1;
|
||||
return 0;
|
||||
}))
|
||||
return found->y;
|
||||
}
|
||||
|
||||
// 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 sample1.y + factor * (sample2.y - sample1.y);
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-easing/#bezier-serialization
|
||||
String EasingStyleValue::CubicBezier::to_string(SerializationMode mode) const
|
||||
{
|
||||
|
|
@ -345,54 +185,6 @@ String EasingStyleValue::CubicBezier::to_string(SerializationMode mode) const
|
|||
return MUST(builder.to_string());
|
||||
}
|
||||
|
||||
double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
// https://www.w3.org/TR/css-easing-1/#step-easing-algo
|
||||
// 1. Calculate the current step as floor(input progress value × steps).
|
||||
auto resolved_number_of_intervals = number_of_intervals.resolved({}).value_or(1);
|
||||
resolved_number_of_intervals = max(resolved_number_of_intervals, position == StepPosition::JumpNone ? 2 : 1);
|
||||
|
||||
auto current_step = floor(input_progress * resolved_number_of_intervals);
|
||||
|
||||
// 2. If the step position property is one of:
|
||||
// - jump-start,
|
||||
// - jump-both,
|
||||
// increment current step by one.
|
||||
if (position == StepPosition::JumpStart || position == StepPosition::Start || position == StepPosition::JumpBoth)
|
||||
current_step += 1;
|
||||
|
||||
// 3. If both of the following conditions are true:
|
||||
// - the before flag is set, and
|
||||
// - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
|
||||
// decrement current step by one.
|
||||
auto step_progress = input_progress * resolved_number_of_intervals;
|
||||
if (before_flag && trunc(step_progress) == step_progress)
|
||||
current_step -= 1;
|
||||
|
||||
// 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
|
||||
if (input_progress >= 0.0 && current_step < 0.0)
|
||||
current_step = 0.0;
|
||||
|
||||
// 5. Calculate jumps based on the step position as follows:
|
||||
|
||||
// jump-start or jump-end -> steps
|
||||
// jump-none -> steps - 1
|
||||
// jump-both -> steps + 1
|
||||
auto jumps = resolved_number_of_intervals;
|
||||
if (position == StepPosition::JumpNone) {
|
||||
jumps--;
|
||||
} else if (position == StepPosition::JumpBoth) {
|
||||
jumps++;
|
||||
}
|
||||
|
||||
// 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
|
||||
if (input_progress <= 1.0 && current_step > jumps)
|
||||
current_step = jumps;
|
||||
|
||||
// 7. The output progress value is current step / jumps.
|
||||
return current_step / jumps;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-easing/#steps-serialization
|
||||
String EasingStyleValue::Steps::to_string(SerializationMode mode) const
|
||||
{
|
||||
|
|
@ -423,14 +215,6 @@ String EasingStyleValue::Steps::to_string(SerializationMode mode) const
|
|||
return MUST(builder.to_string());
|
||||
}
|
||||
|
||||
double EasingStyleValue::Function::evaluate_at(double input_progress, bool before_flag) const
|
||||
{
|
||||
return visit(
|
||||
[&](auto const& curve) {
|
||||
return curve.evaluate_at(input_progress, before_flag);
|
||||
});
|
||||
}
|
||||
|
||||
String EasingStyleValue::Function::to_string(SerializationMode mode) const
|
||||
{
|
||||
return visit(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ public:
|
|||
|
||||
bool operator==(Linear const&) const = default;
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
String to_string(SerializationMode) const;
|
||||
|
||||
Linear(Vector<Stop> stops);
|
||||
|
|
@ -66,7 +65,6 @@ public:
|
|||
return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
|
||||
}
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
String to_string(SerializationMode) const;
|
||||
};
|
||||
|
||||
|
|
@ -79,14 +77,12 @@ public:
|
|||
|
||||
bool operator==(Steps const&) const = default;
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
String to_string(SerializationMode) const;
|
||||
};
|
||||
|
||||
struct WEB_API Function : public Variant<Linear, CubicBezier, Steps> {
|
||||
using Variant::Variant;
|
||||
|
||||
double evaluate_at(double input_progress, bool before_flag) const;
|
||||
String to_string(SerializationMode) const;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user