Saturday, May 28, 2011

Calculating Cubic Bezier Function

If you ever played with the CSS3 native css-transitions, you know that they use cubic-bezier notation to define the transition formula. Cubic Bezier is a parametric function that allows you to define all sorts of smooth curvatures that looks kinda like that



The reason why it's used instead of sinuses and exponents, is that it allows you to define all sorts of shapes with just a few more or less intuitively understandable parameters. Plus, unlike other mathematical functions it can be computed very fast, because it's a simple parametric function, no sequences no nothing.

In RightJS we support native css3 transitions, but it also has a pure JavaScript fx-engine to handle old browsers. And the thing is that native CSS3 transitions use the cubic-bezier functions, while the pure JavaScript engine uses a bit of mumbo-jumbo with Math.sin() and Math.log(), and what I wanted is to allow the developers to use cubic-bezier notation with the old engine.

The problem is that it's not that simple. The cubic-bezier function itself is pretty much straightforward and you can calculate a set of X and Y coordinates in no time, but it won't make you any good because both x-s and y-s will be related to the third t parameter and therefore unevenly distributed agains the t parameter. What you really need is a straight Y = B(x) function.

And here it gets tricky. That much tricky. But, luckily for us, there is always a better way to do it. All you need is to recall a bit of kindergarden math.

Originally, the cubic bezier function is used in a parametric form, but it also can be converted into a polynomial form, in which case it will look like that (you can google it and find say this document)

a = 3 P0
b = 3 P1
c = 3 P2
c0 = P0
c1 = b - a
c2 = a - 2b + c
c3 = P3 - P0 + b - c

B(t) = c0 + t(c1 + t(c2 + tc3))

In case of CSS3 cubic-bezier definitions, P0 is always 0 and P3 is always 1, in which case the formula gets even simpler

c3 = 3 * P1
c2 = 3 * (P2 - P1) - c3
c1 = 1 - c3 - c2

B(t) = t * (c1 + t * (c2 + t * c3))

Which, in case of of CSS3 x1,y1,x2,y2 parameters, can be written in javascript with two functions, for example this way

var Cx = 3 * x1;
var Bx = 3 * (x2 - x1) - Cx;
var Ax = 1 - Cx - Bx;

var Cy = 3 * y1;
var By = 3 * (y2 - y1) - Cy;
var Ay = 1 - Cy - By;

function bezier_x(t) {
return t * (Cx + t * (Bx + t * Ax));
}

function bezier_y(t) {
return t * (Cy + t * (By + t * Ay));
}

There are two reasons why we use the polynomial form of the equation. Firstly, it has less operations and therefore will work faster. Secondly, we are going to use Newton's method to approximate the values and will need a first derivative of one of the functions, which in case of polynomial functions is pretty simple.

Now, to the fun part. We converted the function from one form to another, but the question remains the same, how are we going to get the y = B(x) from x = B(t) and y = B(t)? Quite simple my friends, we will use the kindergarden math, or more exactly the Newton's method to approximate a parametric x value for every t value so that we could get an evenly distributed set of x values and then get a set of y for them using the same bezier_y(t) function.

Newton's method might look a bit scary for those who are not familiar with it, but it is actually very simple. Check this video the guy explains it nicely.

All you need to make it work is to have a derivative to your function, which in our case will look like that

function bezier_x_der(x) {
return Cx + t * (2*Bx + t * 3*Ax);
}

And after that, we just define a little function that will make several iterations trying to find an x value which came from the current t value. We don't have to be very precise in our case, we just make 5 iterations tops and limit the precision by the 3 digits after the pointer.

function find_x_for(t) {
var x = t, i = 0, z;

while (i < 5) {
z = bezier_x(x) - t;
if (Math.abs(z) < 1e-3) break;

x = x - z / bezier_x_der(x);
i++;
}

return x;
}

Now you can create a loop and calculate a set of evenly distributed values, just like that:

var result = [];
for (var i=0; i < 1.001; i+=0.1) {
result.push(bezier_y(find_x_for(i)));
}
console.log(result);



That's basically all of it. You can find the full version of this script in this gist.

Enjoy!

No comments: