Physical simulations are among some of the most interesting (and challenging) applications of expressions in After Effects. Unfortunately, they involve a little math and physics, but I'll try to make it as painless as possible. Let's start out by looking at a couple of the math functions that we'll be using a lot
One of those math functions that we'll be using a lot is the sine wave, implemented in JavaScript as Math.sin(). A sine wave provides a nice periodic oscillating value that is very handy for many simulations. To see what it looks like, we'll use the built-in graphing capabilities of After Effects. If you apply a sine function to a Slider Control and then click the "graph overlay" icon, After Effects will display the value of the function versus time. Here's what we get when we apply the function Math.sin(time) to the Slider Control and open the graph overlay and set the scale of the graph to go from -1 to +1.:
From the graph you can see that at time 0 we get a value that starts at zero moves smoothly to +1, then back through zero to -1 and back to zero to complete the cycle. Then it repeats continuously. If you look at the graph, you can see that one complete cycle takes slightly longer than 6 seconds.
Now let's take a look at sine's fraternal twin, cosine, implemented in JavaScript as Math.cos(). A cosine wave has the same shape as a sine wave, it's just out of phase by 90 degrees. I've included a graph of the function Math.cos(time) as an example.
It starts at time 0 with a value of +1, moves through 0 to -1, back through 0, then back to +1 and it starts again. Again, you'll notice that one complete cycle takes slightly longer than 6 seconds. Whether you use sine or cosine depends entirely on the application. Sometimes it doesn't matter, but if you need the value to start at zero you would use sine. If, however, you need the value to start at full-scale, you would use cosine. Now let's jump right in to a practical application. Say we want an oscillating motion that increases as our object moves across the screen. Here's an expression that will increase the amplitude of a sine wave oscillation linearly with time:
veloc = 40; //horizontal velocity (pixels per second) amp = 12; //sine wave amplitude (pixels) freq = 2.4; //oscillations per second x = time*veloc; y = amp*time*Math.sin(freq*time*2*Math.PI) + thisComp.height/2; [x,y]
This code demonstrates a couple of concepts that we need to take a look at. First, the expression generates the left-to-right motion as well as the up-and-down oscillation. We could have just as easily keyframed the left-to-right motion and just used the expression for the sine wave component. To make that work, we would have to add in the layer's Position and we wouldn't need to offset the y value with half the comp height. Here's what that code would look like:
amp =12; //sine wave amplitude (pixels) freq = 2.4; //oscillations per second y = amp*time*Math.sin(freq*time*2*Math.PI); position + [0,y]
Another thing I should mention here is radians. All the JavaScript trig functions expect their parameter in units called radians, not degrees. It turns out that there are 2 times pi radians in one complete cycle of an oscillation (you'll remember from geometry class that pi is the constant 3.14... - conveniently made available in JavaScript as Math.PI). So if you want to express your frequency variable in terms of oscillations per second (which is a handy way to do it and the way I did it in the examples above) you have to multiply that value times 2*pi to make it come out right. So that's why you'll see "2*Math.PI" in a lot of our sine and cosine examples. OK - what if we wanted to have our layer bounce along, increasing linearly, but only in the upward direction? That would only take a slight modification to the code from the previous example:
veloc = 40; //horizontal velocity (pixels per second) amp =12; //sine wave amplitude (pixels) freq = 2.4; //oscillations per second x = time*veloc; y = amp*time*(Math.sin(freq*time*2*Math.PI) - 1)/2 + thisComp.height/2; [x,y]
here are a couple of things to notice about this expression. The first is that we are subtracting one from the value of our Math.sin() function. So instead of a range of -1 to +1, we will now have a range of -2 to 0. Then we divide by two to give us a range of -1 to 0. Why did we do that if we want the oscillation to be upward? Because in After Effects, upward is in the negative y direction. Make sense? OK - let's move on. Now let's look at simply having a sine wave oscillation that moves upward as it moves from left to right without increasing in amplitude. This requires only a fairly simple modification to our previous examples. Take a look at the code:
veloc = 40; //horizontal velocity (pixels per second) amp =12; //sine wave amplitude (pixels) freq = 2.4; //oscillations per second rise = 20; //upward ramp rate (pixels per second) x = time*veloc; y = amp*Math.sin(freq*time*2*Math.PI) - rise*time + .8*thisComp.height; [x,y]
Let's look at the changes we've made to get the new behavior. First we've added a new parameter called "rise" that defines how fast our sine wave will move upward. This value is multiplied by the time and used as a negative (upward) offset in the calculation of "y". The Math.sin() function is no longer multiplied by time so the sine wave itself does not increase in value as in the previous examples. Finally, our initial y offset has been changed to .8*thisComp.height so that movement will fit on the screen. Let's look at how we would increase the frequency of our sine wave as our layer moves from left to right. First let's look at the code:
veloc = 40; //horizontal velocity (pixels per second) amp = 16; //sine wave amplitude (pixels) freq = 0.5; //oscillations per second x = time*veloc; y = amp*Math.sin(freq*time*time*2*Math.PI) + thisComp.height/2; [x,y]
Let's examine what's different about this expression. The main difference is that we have "time" inside the Math.sin() function twice. This is what causes the frequency to increase with time. Let's look at one more useful variation of sine waves before we move on to something else. What happens if you multiply two sine waves together? You get amplitude modulation. If the frequency of one wave is quite a bit smaller than the other wave, you get a sine wave at the faster frequency that increases and decreases in amplitude at the slower frequency. Here's an example:
veloc = 40; //horizontal velocity (pixels per second) amp = 16; //sine wave amplitude (pixels) freq1 = 0.25; //oscillations per second freq2 = 3.0; x = time*veloc; wave1 = Math.sin(freq1*time*2*Math.PI); wave2 = Math.sin(freq2*time*2*Math.PI) y = amp*wave1*wave2 + thisComp.height/2; [x,y]
You'll notice that we now have two frequencies - "freq1" and "freq2". Our y value is now obtained by multiplying the two sine waves together. OK - that wraps up our initial look at sine and cosine waves. They'll be back with a vengeance shortly though, when we team them up with exponential curves. It'll be fun. Really.
Now let's look at the exponential curve, implemented in JavaScript as Math.exp(). This function takes whatever parameter you pass it and raises the constant "e" (2.718...) to that power. The wave that you get from this function really depends on what parameter you pass it. If we were to use time as the parameter, for example, our function would give us the value 1 where time is zero and begin increasing very rapidly (exponentially in fact) as time becomes greater than zero. Take a look at the graph of Math.exp(time).
Notice that by around 7 seconds the value is already around 1000 and increasing rapidly.
The exponential curve certainly has its uses, but we will more often be interested in the flavor that decreases exponentially rather than increases. This will be very useful for simulating things such as bouncing balls, springs, etc. - or anything that gradually slows down. There are two ways to generate this decaying curve and they give identical results. The first way is just to make the parameter to Math.exp() negative. For example, here's the graph of Math.exp(-time).
You can see that at time zero the value is 1 and as time increases, the value gradually approaches zero. The other variation of the decaying exponential curve is obtained simply by supplying the Math.exp() function with a positive parameter and dividing 1 by the result. That correctly implies that 1/Math.exp(time) is the same as Math.exp(-time).
As you can see, the curves are identical.
Before we move on to some practical applications of these math functions we need to take a look at the extremely useful combination of Math.sin() and Math.exp(). When we combine the sine wave with the decaying exponential curve, we get a decaying oscillation, which is very useful for simulating springs, pendulums, etc. Here's the graph of Math.sin(6*time)/Math.exp(time). From our discussion above, we know that Math.sin(6*time)*Math.exp(-time) would give us the same result.
You'll notice that what we get is a sine wave that quickly decays towards 0. (In case you're wondering where the 6 came from, it just speeds up the frequency of the sine wave to better demonstrate the decaying effect (we'll cover this "speeding up" of the sine wave in more detail shortly.)
What we need to do now is set up some parameters to let us easily control the frequency and amplitude of the sine and cosine waves, and the rate of decay of the decaying exponential curve. The amplitude is easy - we'll just have a variable called "amplitude" that we multiply by the Math.sin() function. If we set "amplitude" equal to 100, for example, our sine wave will range from -100 to +100 instead of -1 to +1. Frequency is also easy. Using the information we discussed previously about Math.sin() and Math.cos() wanting their parameters in radians, we just create a variable called "freq" and set it to the number of oscillations per second that we want. Then all we have to do is use Math.sin(freq*time*2*Math.PI) to generate the oscillation. Finally, let's look at what it takes to make our exponential decay faster or slower. It turns out that all we need is a parameter that we'll call "decay" that we multiply the time by in the Math.exp() function. If "decay" is zero, there will be no decay - the sine wave will just keep going. If "decay" is 1, the decay rate will be like in the examples above. Between zero and 1, the decay will be slower. Greater than 1, faster.
OK - let's start putting this all together. First let's look at a decaying sine wave with "decay" set to zero to start with (so that it runs continuously.) As an example, we'll use a pendulum where we'll apply our decaying sine wave (not decaying in this case) expression to the rotation property. Here's the code:
freq = 1.0; //oscillations per second amplitude = 50; decay = 0; //no decay amplitude*Math.sin(freq*time*2*Math.PI)/Math.exp(decay*time)
Our "amplitude" parameter is set to 50, so the rotation of the pendulum varies from -50 degrees to +50 degrees and our "freq" parameter is set to 1. The result is a pendulum that swings nicely back and forth once each second.
Now let's make it a decaying oscillation by using a non-zero value for "decay". We'll set the value to 0.7, which should give us a nice, smooth deceleration. Here's our new expression:
freq = 1.0; //oscillations per second amplitude = 50; decay = 0.7; amplitude*Math.sin(freq*time*2*Math.PI)/Math.exp(decay*time)
OK - we've seen how we can apply this to the Rotation property. Now let's look at Position. Without too much modification, we should be able generate a nice "springy" motion. In this comp, we'll be simulating a jack-in-the-box. We'll apply our decaying sine wave expression to "Jack's" y position but in this case we're going to use the cosine wave because it starts at full value at time zero. Here's the code:
freq = 5; amplitude = 35; decay = 1.0; y = amplitude*Math.cos(freq*time*2*Math.PI)/Math.exp(decay*time); position + [0,y]
You'll notice that the code plugs the value of the decaying cosine wave formula into the variable "y", which is then used, along with the Position's original x value to set the Position, which results in motion in only the y direction.
Let's look at one more application of our decaying sine wave for Position to simulate a bouncing ball. You'll notice that we're still using the cosine wave (so that our animation starts at its peak value at time zero). I've keyframed the movement of the ball across the bottom of the comp. Here's the code that adds the bounce:
freq = 1.0; //oscillations per second amplitude = 90; decay = .5; posCos = Math.abs(Math.cos(freq*time*2*Math.PI)); y = amplitude*posCos/Math.exp(decay*time); position - [0,y]
You'll notice that we've added a call to the Math.abs() function to our code. This makes all the half cycles of the cosine wave positive, which is just what we need for a bouncing simulation. Note that you need to choose your frequency carefully to sell this effect. For example, at 30 frames per second, a frequency of 2.0 will cause the first bounce to occur half way between frames 3 and 4. So it will look like the ball never quite hits the "floor". To have it occur exactly at frame 3, you would need to change your frequency to 2.5, and to make it occur at frame 4, you'd change the frequency to 1.875.
We've looked at using our decaying sine wave for Rotation to generate a "swinging" motion and for Position to generate "springy" and "bouncy" motion. How about Scale? It seems logical that we should be able to use basically the same expression to generate a "jiggly" motion. Let's see if we can. We'll apply the following expression to a round solid and see what happens:
freq =3; amplitude = 35; decay = 1.0; s = amplitude*Math.sin(freq*time*2*Math.PI)/Math.exp(decay*time); scale + [s,s]
We have generated a nice pulsating effect. You'll notice that this expression is very similar to the previous ones for Position. We plug the one-dimensional value of our decaying sine wave into the variable "s", which we then use to generate the two-dimensional value [s,s] which we add to the original Scale value (which was 100%). The result is that the Scale begins oscillating between values 65% and 135% of full scale and settles out to 100% as the value of the decaying sine wave approaches zero.
Without too much further effort, we can change this Scale expression into something more realistic. In the real world, when something jiggles, it's volume more or less stays constant. So if it squashes in the vertical direction, it gets fatter in the horizontal direction at the same time. If we're dealing with a rectangle, the area is given by width times height. If we change the width, we can figure out the new height that keeps the area constant by using this formula: old-width * old-height = new-width * new-height or, since the new width is just the old width times the new x scale value, old-width * old-height = old-width * (new-x-scale/100) * old-height * (new-y-scale/100) which can be simplified to new-y-scale = (1/new-x-scale) * 10000 Let's see if we can use this to come up with a jiggle expression that will maintain a constant area of a rectangle and try it with a round solid. Here's the expression:
freq = 5; amplitude = 25; decay = 1.0; t = time - inPoint; x = scale[0] + amplitude*Math.sin(freq*t*2*Math.PI)/Math.exp(decay*t); y = (1/x)*10000; [x,y]
You'll notice some changes from the previous expression. In order to make the animation work, I had to split the layer where the jiggling starts. That means that it no longer starts at time zero. So a new time variable "t" is calculated to be the amount of time since the in point of the layer. Next, to be able to use our formula for constant area, we need to have the total value of the x Scale (not the amount of increase or decrease from 100% as in the previous example). The y Scale amount is then calculated from the x Scale amount per our nifty formula.
Let's take a look at our bouncing ball again, but this time we'll include the Scale expression for squash and stretch. Here's the Position expression again:
freq = 1.0; //oscillations per second amplitude = 90; decay = .5; posCos = Math.abs(Math.cos(freq*time*2*Math.PI)); y = amplitude*posCos/Math.exp(decay*time); position - [0,y]
and here's the expression for Scale:
freq = 1.0;
squashFreq = 4.0;
decay = 5.0;
masterDecay = 0.4;
amplitude = 25;
delay = 1/(freq*4);
if (time > delay){
bounce = Math.sin(squashFreq*time*2*Math.PI);
bounceDecay = Math.exp(decay*((time - delay)%(freq/2)));
overallDecay = Math.exp(masterDecay*(time - delay));
x = scale[0] + amplitude*bounce/bounceDecay/overallDecay;
y = scale[0]*scale[1]/x;
[x,y]
}else{
scale
}
Let's take a look at one more example while we're working with the decaying motion equation. Remember those "momentum balls" that people used to have on their desks? They are a pretty good demonstration of the transfer of momentum and it turns out not too tough to simulate with expressions. For this demo I set up a precomp with one ball and a string and moved the anchor point to where the string connects to the frame. Then I applied this expression for Rotation to the precomp:
freq = 1.0;
maxAngle = 60;
decay = 0.4;
numBalls = 5;
if (index == 1){
clamp(maxAngle*Math.cos(freq*time*2*Math.PI)/Math.exp(decay*time),0,maxAngle)
}else if (index == numBalls){
clamp(maxAngle*Math.cos(freq*time*2*Math.PI)/Math.exp(decay*time),-maxAngle,0)
}else{
0
}
Then I duplicated the precomp four times and lined up the copies to form the momentum chain. The expression makes use of the clamp() function, which limits the ball on the left to positive angles and the ball on the right to negative angles.
Here's an example of just how far we can push this stuff. If we combine the formula for a three-dimensional sine wave with our code for automatically distributing layers in a grid we can set up a pretty cool undulating grid. Please note that even though it appears that the layers are undulating in the y direction in the demo movie they are actually laid out in an x/y grid and are moving back and forth in the z direction. I moved the camera to give a better perspective view of the action. Here's the code for position:
numCol = 8; //number of columns gap = 20; // distance between cells (pixels) amp = 40; //amplitude in pixels wave = 100; //wavelength in pixels freq = 0.5; //cycles per second origin = [gap/2,gap/2,0]; //upper left hand corner of grid x = ((index-1)%numCol)*gap; y = Math.floor((index-1)/numCol)*gap; z = 25*Math.cos((x + y)/wave + freq*Math.PI*2*time); origin + [x,y,z]
Almost all of this code is just concerned with the positioning a layer in the grid and is similar to code we've looked at previously. The undulations come from the next-to-the-last line which is the formula for a cosine wave that depends on x position, y position, and time to calculate the z position of the layer. You'll notice that the layers rock back and forth as they undulate up and down. This is accomplished by the following expression for orientation:
numCol = 8; //number of columns gap = 20; // distance between cells (pixels) wave = 100; //wavelength in pixels freq = 0.5; //cycles per second damp = .5; //damping factor x = ((index-1)%numCol)*gap; y = Math.floor((index-1)/numCol)*gap; xRot = Math.atan(Math.sin(x/wave + freq*Math.PI*2*time))*damp; yRot = Math.atan(Math.sin(y/wave + freq*Math.PI*2*time))*damp; [radiansToDegrees(xRot),radiansToDegrees(yRot),0]
Most of the code here is just a duplication of what's in the Position expression except for the last three lines, which calculate the layer's Orientation based the current action of the "wave" at the layer's Position. If you really dig into to this, you get into a mathematical concept called derivatives, which we're not going to get into here, except to say that conveniently for us, the derivative of a cosine wave is a sine wave, which works out quite well in this case.