Open MartinSStewart opened 5 years ago
@MartinSStewart Please repeat your overlay with Ease.bezier 1 0 0 1
. If that lines up we can change the definition of Ease.inOutExpo
to that.
The green line represents Ease.bezier 1 0 0 1
. Unless I'm misunderstanding something it seems to also be incorrect.
I downloaded the repo and ran Test.elm and got the same behavior for Ease.bezier 1 0 0 1
.
Edit: Here's another version that doesn't match Ease.bezier 1 0 0 1
so I believe this is another bug.
Alright, it's possible there's a bug in Ease.bezier
.
I dug through the de Casteljau's algorithm as presented here and I cannot find a meaningful difference from our implementation (other than that we assume a cubic with (0,0) first and (1,1) last).
I have implemented my own version now. I can make a pull request for it though it's not an elegant or efficient solution. I have a bezier curve function that returns a 2d point for a given t. Then I do a binary search to find which t value gives the desired x coordinate and then return the y coordinate for that value.
Casteljau's is a standard algorithm and I'd rather figure out what's going on with it than take something that's "not an elegant or efficient solution". But I appreciate the initiative and maybe you can dig into the existing implementation to find the bug that I missed? Maybe you can examine the curves at different t
values and confirm numerically, not just visually, that there is a bug?
I don't think I'll have time to do a more in depth search as to what is going wrong. I looked at Casteljau's algorithm but right now I don't understand it. I did find this post which gives code for solving cubic bezier curves https://stackoverflow.com/a/51883347 if that's any help
hello from the future!
I stumbled my toe against this bug yesterday. The problem with Ease.bezier
is that it seems to ignore x1
and x2
params, as you can see here https://ellie-app.com/fTtTC88sFzva1
Having nothing better to do that afternoon, I dived into understanding how bezier curves are drawn and searching for ready cubic-bezier algorithms I could reuse. Eventually I implemented a very simple cubic-bezier algo and surprise! I got the exact same bug! And that helped me understand the root cause.
The problem seems to be that we both assumed calculating a point on a bezier curve (let's call this function bezierPoint
) for some time t
returns a (t, y)
point, but in reality bezierPoint t
gives you an (x, y)
point where x
/= t
.
The general solution seems to be calling bezierPoint
several times until x
is close enough to t
. I thought of two approaches:
t
bezierPoint
N times with different times using the bisection method, assuming that x
grows monotonically with t
(I think it is a safe assumption for cubic bezier curves like (0, 0), a, b, (1, 1)
and ax
, bx
are in <0, 1> range)I implemented them both and the binary search seems to be much more accurate even at low N values. Here is an example with steps=4
(black is Ease.bezier, red is lookup table, green is binary search)
Here is binary search at steps=8
EDIT: it turns out that I can get a more accurate AND faster approximation by defining the binary search exit condition as abs (x - t) < epsilon
, instead of a fixed number of steps. Here is a fragment of the previous curve, super up close
fixed steps (red): 18423 bezierPoint
calls
epsilon (black): 15140 bezierPoint
calls
EDIT 2: after refactoring the fixed steps algo, it got faster than the epsilon algo :slightly_smiling_face:
I wrote a benchmark for both binary search approaches, as well as different bezierPoint
implementations: https://megapctr.github.io/elm-cubic-bezier/
It seems the current bezierPoint
implementation in Easing.bezier
is very inefficient when compared with my simple approach, even after doing some optimizations
bezierPointSimple
93,744 runs / second
bezierPointAdvancedOriginal
6,472 runs / second
bezierPointAdvancedOptimized
8,681 runs / second
The benchmark and the implementations are at https://github.com/megapctr/elm-cubic-bezier
Ease.inOutExpo doesn't match the example provided here https://easings.net/en#easeInOutExpo .
Here I have overlaid Ease.inOutExpo (in red) with easings.net's version (in blue). Notice how their version is much steeper at the center.
I believe the actual InOutExpo is defined as a cubic bezier curve with control points at [(0, 0), (1, 0), (0, 1), (1, 1)] rather than InExpo mirrored with itself.
Edit: In my example I have y-axis flipped, ignore that.