diff --git a/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp b/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp new file mode 100644 index 0000000000..8be42450fa --- /dev/null +++ b/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023, Ali Mohammad Pur + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::Animations { + +// https://www.w3.org/TR/css-easing-1/#linear-easing-function +double LinearTimingFunction::operator()(double input_progress, bool) const +{ + return input_progress; +} + +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); +} + +// https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo +double CubicBezierTimingFunction::operator()(double input_progress, bool) const +{ + // 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) + return 1; + if (x < sample.x) + 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.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->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 clamp(sample1.y + factor * (sample2.y - sample1.y), 0, 1); +} + +// https://www.w3.org/TR/css-easing-1/#step-easing-algo +double StepsTimingFunction::operator()(double input_progress, bool before_flag) const +{ + // 1. Calculate the current step as floor(input progress value × steps). + auto current_step = floor(input_progress * number_of_steps); + + // 2. If the step position property is one of: + // - jump-start, + // - jump-both, + // increment current step by one. + if (jump_at_start) + 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 * number_of_steps; + 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 + double jumps; + if (jump_at_start ^ jump_at_end) + jumps = number_of_steps; + else if (jump_at_start && jump_at_end) + jumps = number_of_steps + 1; + else + jumps = number_of_steps - 1; + + // 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; +} + +double TimingFunction::operator()(double input_progress, bool before_flag) const +{ + return function.visit([&](auto const& f) { return f(input_progress, before_flag); }); +} + +} diff --git a/Userland/Libraries/LibWeb/Animations/TimingFunction.h b/Userland/Libraries/LibWeb/Animations/TimingFunction.h new file mode 100644 index 0000000000..87a6a9e004 --- /dev/null +++ b/Userland/Libraries/LibWeb/Animations/TimingFunction.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023, Ali Mohammad Pur + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::Animations { + +// https://www.w3.org/TR/css-easing-1/#the-linear-easing-function +struct LinearTimingFunction { + double operator()(double t, bool) const; +}; + +// https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions +struct CubicBezierTimingFunction { + double x1; + double y1; + double x2; + double y2; + + struct CachedSample { + double x; + double y; + double t; + }; + + mutable Vector m_cached_x_samples = {}; + + double operator()(double input_progress, bool) const; +}; + +// https://www.w3.org/TR/css-easing-1/#step-easing-functions +struct StepsTimingFunction { + size_t number_of_steps; + bool jump_at_start; + bool jump_at_end; + + double operator()(double input_progress, bool before_flag) const; +}; + +struct TimingFunction { + Variant function; + + double operator()(double input_progress, bool before_flag) const; +}; + +static TimingFunction linear_timing_function { LinearTimingFunction {} }; +// NOTE: Magic values from +static TimingFunction ease_timing_function { CubicBezierTimingFunction { 0.25, 0.1, 0.25, 1.0 } }; +static TimingFunction ease_in_timing_function { CubicBezierTimingFunction { 0.42, 0.0, 1.0, 1.0 } }; +static TimingFunction ease_out_timing_function { CubicBezierTimingFunction { 0.0, 0.0, 0.58, 1.0 } }; +static TimingFunction ease_in_out_timing_function { CubicBezierTimingFunction { 0.42, 0.0, 0.58, 1.0 } }; + +} diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 18bc55b29c..c91e01bb61 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES Animations/AnimationEffect.cpp Animations/AnimationTimeline.cpp Animations/DocumentTimeline.cpp + Animations/TimingFunction.cpp ARIA/AriaData.cpp ARIA/ARIAMixin.cpp ARIA/Roles.cpp