It’s great to be able to animate a stack of layers using familiar Beziér handles. This is great for flexible spines, caterpillars, snakes, or in the example I’m working on: swimming fish.
It creates an easy to use rig, that allows you to easily create fluid motion with minimal controls.
I’ve done this a couple of times, and each time I’ve had to consult Wikipedia for the formula and then turn that into useable expressions. So to prevent me having to reinvent it another time, here’s how it’s done…
First I made my controls
There are four controls: the beginning and end points, and two handles to define the curve. They’re all nulls, but you could use anything. In the example above the beginning is yellow, the handles are ‘sea foam’ and green and the end is blue.
To make editing easier I parented the handles to the end points—if you’re used to Beziér controls in most design apps this is the way they normally work, with the point having handles that are attached to it. It also allows me to rotate the end points to twist the spline, meaning I can mostly just move and rotate my end points to control the whole thing.
Next I made my layer stack
These are the layers that are going to be animated. In this case it is a bunch of solids with circular masks. I named them—and this is important—as c1, c2 c3 etc. They’re above the controls in the timeline—this too is important for the way I’ve implemented the expression. They don’t have to be the same, in this example they all have different scale, and they’re based on two different coloured solids.
Time to get mathsy
Don’t worry, you can skip this bit if you’re allergic to maths.
A Beziér curve is defined by this equation:
B(t) = (1 – t)3P0 + 3(1 – t)2tP1 + 3(1 – t)t2P2 + t3P3 ,
0 ≤ t ≤ 1
- t is the distance along the curve, expressed as a value from 0 to 1;
- P0 is the start,
- P1 and P2 are the handles and
- P3 is the end point.
P0 P1, P2 and P3 are all vectors, aka two- or three-dimensional arrays or matrices. Now to turn that into a useable expression.
First we define the points
P0 and P3 e easy; they’re the position of these points
Next, the handles.
Because I parented the handles to the end points we need to get their world position, which we do with a layer space transform, in this case toWorld:
toWorld() method returns the position of a given point on that layer with respect to the the world. Here we’re getting each handle’s anchor point in world terms.
Next we define t
<layer>.name returns the name of the layer, which is a string. We get the last character using the JS string method
split(-1). This will be an integer, so we can treat it as such. If you have more than 10 elements in your bezier spine, you’ll need to use an appropriate number of padding zeros and use
n is the number of digits.
Then we divide the integer by the number of layers in the stack, which we find by getting the index of the end point (that’s why the layer stack has to be above the control points). This makes sure that 0<t<1 or in other words it normalises it to a value between 0 and 1.
Now for the real hoo-hah.
Math.pow() palaver, but what can you do?
Math.pow((1 - t), 3)* p0 + 3 * Math.pow(1 - t, 2) * t * p1 + 3 * (1 - t) * Math.pow(t, 2) * p2 + Math.pow(t, 3) * p3;
You’ll notice that we don’t have to split up the x and y components of any of the points. Remember how p0, p1 etc. were vectors in the Beziér equation? Well you can do vector maths in expressions, as long as you only multiply vectors by scalars (normal, single component numbers like integers and decimals), and you only add vectors to other vectors. One of the advantages of this is that the expression will work just as well on a 2D or 3D layer. Magic.
if your eyes are glazing over, here’s where you get to copy and paste.
TODO: a script to apply this all automagically. For now, set up your layers as described, and apply this to the animated layer’s position property:
p0=thisComp.layer("begin").transform.position; p1=thisComp.layer("H1").toComp(thisComp.layer("H1").transform.anchorPoint); p2=thisComp.layer("H2").toComp(thisComp.layer("H2").transform.anchorPoint); p3=thisComp.layer("end").transform.position; t=(name)/(thisComp.layer("end").index-2); Math.pow((1 - t), 3)* p0 + 3 * Math.pow(1 - t, 2) * t * p1 + 3 * (1 - t) * Math.pow(t, 2) * p2 + Math.pow(t, 3) * p3;
Here’s the script in action: