elm-community / easing-functions

Easing (animation and timing) library for Elm.
http://package.elm-lang.org/packages/elm-community/easing-functions/latest
BSD 3-Clause "New" or "Revised" License
11 stars 3 forks source link

InOutExpo doesn't match easings.net's version #4

Open MartinSStewart opened 5 years ago

MartinSStewart commented 5 years ago

Ease.inOutExpo doesn't match the example provided here https://easings.net/en#easeInOutExpo .

graph

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.

mgold commented 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.

MartinSStewart commented 5 years ago

The green line represents Ease.bezier 1 0 0 1. Unless I'm misunderstanding something it seems to also be incorrect.

Screen Shot 2019-09-09 at 5 19 38 PM
MartinSStewart commented 5 years ago

I downloaded the repo and ran Test.elm and got the same behavior for Ease.bezier 1 0 0 1.

Screen Shot 2019-09-09 at 5 28 59 PM

Edit: Here's another version that doesn't match Ease.bezier 1 0 0 1 so I believe this is another bug.

mgold commented 5 years ago

Alright, it's possible there's a bug in Ease.bezier.

mgold commented 5 years ago

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).

MartinSStewart commented 5 years ago

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.

mgold commented 5 years ago

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?

MartinSStewart commented 5 years ago

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

ffigiel commented 2 years ago

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:

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 image 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:

ffigiel commented 2 years ago

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