Sfoglia il codice sorgente

LibWeb: Add a TimingFunction struct implementing the Web Easing spec

This is mostly a copy-paste of the algorithms already in StyleComputer
with spec links and comments. The only thing that really changed is the
handling of values outside of the range [0, 1] in the cubic bezier
function.

The implementation in StyleComputer will be removed in a later commit.
Matthew Olsson 1 anno fa
parent
commit
66bd75f2b9

+ 172 - 0
Userland/Libraries/LibWeb/Animations/TimingFunction.cpp

@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/BinarySearch.h>
+#include <LibWeb/Animations/TimingFunction.h>
+#include <math.h>
+
+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); });
+}
+
+}

+ 60 - 0
Userland/Libraries/LibWeb/Animations/TimingFunction.h

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/Vector.h>
+
+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<CachedSample, 64> 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<LinearTimingFunction, CubicBezierTimingFunction, StepsTimingFunction> function;
+
+    double operator()(double input_progress, bool before_flag) const;
+};
+
+static TimingFunction linear_timing_function { LinearTimingFunction {} };
+// NOTE: Magic values from <https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease>
+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 } };
+
+}

+ 1 - 0
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