-
Notifications
You must be signed in to change notification settings - Fork 9
Add cubic bezier easing #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| /* | ||
| * Copyright 2024 Matthieu Bouron <[email protected]> | ||
| * Copyright 2019 The Android Open Source Project | ||
| * Copyright 2016-2022 GoPro Inc. | ||
| * | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
|
|
@@ -24,6 +26,7 @@ | |
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <float.h> | ||
|
|
||
| #include "internal.h" | ||
| #include "log.h" | ||
|
|
@@ -64,6 +67,7 @@ static const struct param_choices easing_choices = { | |
| {"back_out", EASING_BACK_OUT, .desc=NGLI_DOCSTRING("overstep target value and smoothly converge back to it")}, | ||
| {"back_in_out", EASING_BACK_IN_OUT, .desc=NGLI_DOCSTRING("combination of `back_in` then `back_out`")}, | ||
| {"back_out_in", EASING_BACK_OUT_IN, .desc=NGLI_DOCSTRING("combination of `back_out` then `back_in`")}, | ||
| {"bezier_cubic", EASING_BEZIER_CUBIC, .desc=NGLI_DOCSTRING("cubic bezier curve")}, | ||
| {NULL} | ||
| } | ||
| }; | ||
|
|
@@ -319,6 +323,175 @@ static easing_type back_derivative(easing_type t, easing_type s) | |
| DECLARE_EASINGS(back, , back_func(x, PARAM(0, 1.70158)), TRANSFORM) | ||
| DECLARE_EASINGS(back, _derivative, back_derivative(x, PARAM(0, 1.70158)), DERIVATIVE) | ||
|
|
||
| /* Cubic Bezier */ | ||
|
|
||
| static int double_close_to_zero(double value) | ||
| { | ||
| return fabs(value) < FLT_EPSILON; | ||
| } | ||
|
|
||
| static float clamp_root(float r) | ||
| { | ||
| const float s = NGLI_CLAMP(r, 0.f, 1.f); | ||
| return fabsf(s - r) > FLT_EPSILON ? NAN : s; | ||
| } | ||
|
|
||
| /* | ||
| * Fast cbrt adapted from the Android Compose library. | ||
| * | ||
| * See: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt;l=160 | ||
| */ | ||
| static float fast_cbrt(float x) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the native |
||
| { | ||
| union { float f32; uint32_t u32; } v = { .f32 = x }; | ||
| v.u32 = v.u32 & 0x1ffffffffL; | ||
| v.u32 = 0x2a510554 + (v.u32 / 3); | ||
|
|
||
| float estimate = v.f32; | ||
| estimate -= (estimate - x / (estimate * estimate)) * (1.0f / 3.0f); | ||
| estimate -= (estimate - x / (estimate * estimate)) * (1.0f / 3.0f); | ||
| return estimate; | ||
| } | ||
|
|
||
| /* | ||
| * Find the first cubic root using Cardano's algorithm. | ||
| * | ||
| * See: https://pomax.github.io/bezierinfo/#yforx | ||
| */ | ||
| static float find_first_cubic_root(float p0, float p1, float p2, float p3) | ||
| { | ||
| double a = 3.0 * (p0 - 2.0 * p1 + p2); | ||
| double b = 3.0 * (p1 - p0); | ||
| double c = p0; | ||
| double d = -p0 + 3.0 * (p1 - p2) + p3; | ||
|
Comment on lines
+363
to
+366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I'm assuming this is the cubic points being converted to polynomial coefficients, but the value are weirdly shuffled. You made your polynomial
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I implemented it so it matches the ref mentioned in the comment, see: https://pomax.github.io/bezierinfo/#yforx |
||
|
|
||
| if (double_close_to_zero(d)) { | ||
| if (double_close_to_zero(a)) { | ||
| if (double_close_to_zero(b)) { | ||
| return NAN; | ||
| } | ||
| return clamp_root((float) (-c / b)); | ||
| } else { | ||
| double q = sqrt(b * b - 4.0 * a * c); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is missing a check if the |
||
| double a2 = 2.0 * a; | ||
|
|
||
| float root = clamp_root(((float) ((q - b) / a2))); | ||
| if (!isnan(root)) | ||
| return root; | ||
|
|
||
| return clamp_root(((float) ((-b - q) / a2))); | ||
|
Comment on lines
+375
to
+382
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: there is a simpler quadratic formula which we're using in distmap where you find a middle point Since there are less subtractions, I also believe it leads to a lower risk of catastrophic cancellation (to be verified though). |
||
| } | ||
| } | ||
|
|
||
| a /= d; | ||
| b /= d; | ||
| c /= d; | ||
|
|
||
| double o3 = (3.0 * b - a * a) / 9.0; | ||
| double q2 = (2.0 * a * a * a - 9.0 * a * b + 27.0 * c) / 54.0; | ||
| double discriminant = q2 * q2 + o3 * o3 * o3; | ||
| double a3 = a / 3.0; | ||
|
|
||
| // Three possible real roots | ||
| if (discriminant < 0.0) { | ||
| double mp33 = -(o3 * o3 * o3); | ||
| double r = sqrt(mp33); | ||
| double t = -q2 / r; | ||
| double cos_phi = NGLI_CLAMP(t, -1.0, 1.0); | ||
| double phi = acos(cos_phi); | ||
| double t1 = 2.0 * fast_cbrt((float)r); | ||
|
|
||
| float root = clamp_root((float)(t1 * cos(phi / 3.0) - a3)); | ||
| if (!isnan(root)) | ||
| return root; | ||
|
|
||
| root = clamp_root((float)(t1 * cos((phi + TAU_F64) / 3.0) - a3)); | ||
| if (!isnan(root)) | ||
| return root; | ||
|
|
||
| return clamp_root((float)(t1 * cos((phi + 2.0 * TAU_F64) / 3.0) - a3)); | ||
| } | ||
|
|
||
| // Three real roots, but two of them are equal | ||
| if (discriminant == 0.0) { | ||
| float u1 = -fast_cbrt((float)q2); | ||
|
|
||
| float root = clamp_root((float)(2.0 * u1 - a3)); | ||
| if (!isnan(root)) | ||
| return root; | ||
|
|
||
| return clamp_root((float)(-u1 - a3)); | ||
| } | ||
|
|
||
| // One real root, two complex roots | ||
| double sd = sqrt(discriminant); | ||
| double u1 = fast_cbrt((float)(-q2 + sd)); | ||
| double v1 = fast_cbrt((float)(q2 + sd)); | ||
|
|
||
| return clamp_root((float)(u1 - v1 - a3)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that in my experience the analytic cubic is filled with numerical instability and I wouldn't recommend it... I ended up dropping it from the distmap for a more stable iterative algorithm. |
||
| } | ||
|
|
||
| static double evaluate_cubic(double p1, double p2, double t) | ||
| { | ||
| double a = 1.0 / 3.0 + (p1 - p2); | ||
| double b = (p2 - 2.0 * p1); | ||
| double c = p1; | ||
| return 3.0 * ((a * t + b) * t + c) * t; | ||
|
Comment on lines
+436
to
+439
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would recommend computing and caching the polynomial for faster evaluation. |
||
| } | ||
|
|
||
| static easing_type bezier_cubic(easing_type t, size_t args_nb, const easing_type *args) | ||
| { | ||
| if (t < 0.f || t > 1.f) | ||
| return t; | ||
|
|
||
| const easing_type a = PARAM(0, 0.0); | ||
| const easing_type b = PARAM(1, 0.0); | ||
| const easing_type c = PARAM(2, 1.0); | ||
| const easing_type d = PARAM(3, 1.0); | ||
|
Comment on lines
+447
to
+450
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So here is something: with easing, the starting and ending points are always respectively Next, you can't have the curve going backward, which means All these constraints may also make the solver and friends way more convenient and may avoid a bunch of tests. You also won't need to extend the number of parameter from 2 to 4. |
||
|
|
||
| const easing_type v = NGLI_MAX(t, FLT_EPSILON); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is |
||
| const float p0 = (float)(0.0 - v); | ||
| const float p1 = (float)( a - v); | ||
| const float p2 = (float)( c - v); | ||
| const float p3 = (float)(1.0 - v); | ||
|
|
||
| easing_type r = find_first_cubic_root(p0, p1, p2, p3); | ||
| if (isnan(r)) { | ||
| /* No root found */ | ||
| return 0.0; | ||
| } | ||
|
|
||
| return evaluate_cubic(b, d, r); | ||
| } | ||
|
|
||
| static easing_type bezier_cubic_derivative(easing_type t, size_t args_nb, const easing_type *args) | ||
| { | ||
| if (t < 0.f || t > 1.f) | ||
| return 1.0; | ||
|
|
||
| const easing_type a = PARAM(0, 0.0); | ||
| const easing_type b = PARAM(1, 0.0); | ||
| const easing_type c = PARAM(2, 1.0); | ||
| const easing_type d = PARAM(3, 1.0); | ||
|
|
||
| const double da = 3.0 * (b - a); | ||
| const double db = 3.0 * (c - b); | ||
| const double dc = 3.0 * (d - c); | ||
|
|
||
| const easing_type v = NGLI_MAX(t, FLT_EPSILON); | ||
| const float dp0 = (float)(0.0 - v); | ||
| const float dp1 = (float)( da - v); | ||
| const float dp2 = (float)( dc - v); | ||
|
|
||
| easing_type r = find_first_cubic_root(dp0, dp1, dp2, 0.f); | ||
| if (isnan(r)) { | ||
| /* No root found */ | ||
| return 1.0; | ||
| } | ||
|
|
||
| return evaluate_cubic(db, 0.0, r); | ||
| } | ||
|
|
||
| static const struct { | ||
| easing_function function; | ||
| easing_function derivative; | ||
|
|
@@ -365,6 +538,7 @@ static const struct { | |
| [EASING_BACK_OUT] = {back_out, back_out_derivative, NULL}, | ||
| [EASING_BACK_IN_OUT] = {back_in_out, back_in_out_derivative, NULL}, | ||
| [EASING_BACK_OUT_IN] = {back_out_in, back_out_in_derivative, NULL}, | ||
| [EASING_BEZIER_CUBIC] = {bezier_cubic, bezier_cubic_derivative, NULL}, | ||
| }; | ||
|
|
||
| static int check_offsets(double x0, double x1) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| # | ||
| # Copyright 2024 Matthieu Bouron <[email protected]> | ||
| # Copyright 2020-2022 GoPro Inc. | ||
| # | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
|
|
@@ -39,19 +40,21 @@ def _easing_join(easing, args): | |
|
|
||
| _easing_specs = ( | ||
| # fmt: off | ||
| ("linear", 0), | ||
| ("quadratic", 3), | ||
| ("cubic", 3), | ||
| ("quartic", 3), | ||
| ("quintic", 3), | ||
| ("power:7.3", 3), | ||
| ("sinus", 3), | ||
| ("exp", 3), | ||
| ("circular", 3), | ||
| ("bounce", 1), | ||
| ("elastic", 1), | ||
| ("elastic:1.5:1.2", 1), | ||
| ("back", 3), | ||
| ("linear", 0), | ||
| ("quadratic", 3), | ||
| ("cubic", 3), | ||
| ("quartic", 3), | ||
| ("quintic", 3), | ||
| ("power:7.3", 3), | ||
| ("sinus", 3), | ||
| ("exp", 3), | ||
| ("circular", 3), | ||
| ("bounce", 1), | ||
| ("elastic", 1), | ||
| ("elastic:1.5:1.2", 1), | ||
| ("back", 3), | ||
| ("bezier_cubic", 0), | ||
| ("bezier_cubic:0.45:0:0.58:1", 0), | ||
| # fmt: on | ||
| ) | ||
|
|
||
|
|
@@ -111,8 +114,13 @@ def _test_derivative_approximations(nb_points=20): | |
| times = [i * scale for i in range(nb_points + 1)] | ||
| max_err = 0.0005 | ||
|
|
||
| # Unsupported easings (due to lack of precision) | ||
| unsupported_easings = ["bezier_cubic"] | ||
|
|
||
| for easing in _easing_list: | ||
| easing_name, easing_args = _easing_split(easing) | ||
| if easing_name in unsupported_easings: | ||
| continue | ||
| for offsets in _offsets: | ||
| out_vals = [ngl.easing_derivate(easing_name, t, easing_args, offsets) for t in times] | ||
| approx_vals = [_approx_derivative(easing_name, t, easing_args, offsets) for t in times] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| off0: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.844422 0.618369 0.618369 | ||
| off1: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.844422 0.618369 0.618369 | ||
| off2: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.844422 0.618369 0.618369 | ||
| off3: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.844422 0.618369 0.618369 | ||
| off0: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.618369 0.250506 0.844422 0.909746 0.909746 | ||
| off1: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.618369 0.250506 0.844422 0.909746 0.909746 | ||
| off2: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.618369 0.250506 0.844422 0.909746 0.909746 | ||
| off3: 0.757954 0.420572 0.258917 0.511275 0.404934 0.783799 0.303313 0.476597 0.583382 0.908113 0.504687 0.281838 0.755804 0.618369 0.250506 0.844422 0.909746 0.909746 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| off0: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.682349 0.612477 0.339850 0.209222 0.109058 0.551267 0.706561 0.547441 0.109058 0.551267 0.706561 0.547441 | ||
| off1: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.682349 0.612477 0.339850 0.209222 0.109058 0.551267 0.706561 0.547441 0.109058 0.551267 0.706561 0.547441 | ||
| off2: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.682349 0.612477 0.339850 0.209222 0.109058 0.551267 0.706561 0.547441 0.109058 0.551267 0.706561 0.547441 | ||
| off3: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.682349 0.612477 0.339850 0.209222 0.109058 0.551267 0.706561 0.547441 0.109058 0.551267 0.706561 0.547441 | ||
| off0: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.103294 0.522133 0.669220 0.518509 0.543210 0.360343 0.642833 0.402295 0.682349 0.612477 0.339850 0.209222 0.587617 0.444989 0.596287 0.384901 0.587617 0.444989 0.596287 0.384901 | ||
| off1: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.103294 0.522133 0.669220 0.518509 0.543210 0.360343 0.642833 0.402295 0.682349 0.612477 0.339850 0.209222 0.587617 0.444989 0.596287 0.384901 0.587617 0.444989 0.596287 0.384901 | ||
| off2: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.103294 0.522133 0.669220 0.518509 0.543210 0.360343 0.642833 0.402295 0.682349 0.612477 0.339850 0.209222 0.587617 0.444989 0.596287 0.384901 0.587617 0.444989 0.596287 0.384901 | ||
| off3: 0.480603 0.380642 0.736778 0.285117 0.371384 0.454595 0.707639 0.393273 0.269236 0.722009 0.590719 0.239305 0.503564 0.543992 0.448472 0.499368 0.224733 0.528837 0.651300 0.495616 0.529613 0.112959 0.487020 0.685246 0.551146 0.583499 0.287951 0.522351 0.260492 0.805028 0.548699 0.014042 0.535866 0.296950 0.614150 0.497483 0.001143 0.493578 0.867603 0.243911 0.294179 0.787425 0.172839 0.513368 0.175965 0.713504 0.592298 0.330351 0.072327 0.287755 0.456681 0.838695 0.103294 0.522133 0.669220 0.518509 0.543210 0.360343 0.642833 0.402295 0.682349 0.612477 0.339850 0.209222 0.587617 0.444989 0.596287 0.384901 0.587617 0.444989 0.596287 0.384901 |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FLT_EPSILONis, as far as I can tell, not a very good threshold choice as it's probably going to filter out all sorts of cases where the root is close to 0 or 1: it's a very restrictive value, and at the same time not 100%. A more arbitrary1e-6(that can be understood as a "precision" rounding) will likely do a better job.