Physical Simulations
Some Practical Examples
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.
Let's Add Some Decay
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)
That's the Fact Jack
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.
Make it Bounce
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.
Time for a Stretch
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.
Squash and Stretch
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.
Bouncing Ball Revisited
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
}
Momentum Balls
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.