Creating Trails



"Mouse Trails"

Sometimes there is a need to have a series of layers follow a "lead" layer with a time delay. You've probably seen those web pages where the cursor leaves a "mouse trail" - a string of copies of the cursor image that follow behind, each with less opacity than the copy before.

There are a couple of basic approaches to solving this problem. The first solution uses After Effects' "valueAtTime" method to retrieve the position value of the leader layer at a previous time. Let's take a look at the code:

follow the leader


delay = 5; //number of frames to delay

d = delay*thisComp.frameDuration*(index - 1);
thisComp.layer(1).position.valueAtTime(time - d)

Let's take a look at what's going on here. The first line just sets the number of frames that you want each layer to be delayed from the previous layer. The second line calculates the time delay of each layer based on its position in the layer stack. In our example, the delay for Layer 1 will be zero (because that layer is the "leader"), the delay for Layer 2 will be 5 frames, for Layer 3 it will be 10 frames, etc. The last line retrieves the position value of the lead layer at the current time minus the layer's delay.

Now let's look at how we could have done this the wrong way. It might be tempting to use an expression like this:


delay = 5; //number of frames to delay

d = delay*thisComp.frameDuration;
thisComp.layer(index - 1).position.valueAtTime(time - d)

So what's wrong with this expression? It certainly gets the job done. The problem is that it's a "cascading" expression, where the expression in one layer refers to the previous layer which refers to the previous layer, etc. This can enormously increase the amount of work that After Effects has to do at each frame. You may not notice the effect if you have a small number of layers but as you increase the number of layers, rendering can grind to a halt.

The difference is subtle but significant. The second expression uses a fixed delay relative to the layer directly above in the layer stack. The first expression always references the first layer in the stack and calculates its delay based on its own position in the layer stack. Much better.

Adding a Fade

We can add interest to our animation by adding an expression that will exponentially fade the layer's opacity depending on how far down it is in the layer stack. Here's the opacity expression:

follow the leader with fade


opacityFactor = .75;

Math.pow(opacityFactor,index - 1)*100

An Interesting Variation

This "follow the leader" technique will of course work for other properties besides position. It's pretty simple. For example, if you wanted a series of layers to have a time-delayed rotation you would just edit our position expression to look like this to turn it into a rotation expression:

follow the leader's rotation


delay = 5; //number of frames to delay

d = delay*thisComp.frameDuration*(index - 1);
thisComp.layer(1).rotation.valueAtTime(time - d)

The disadvantage to this method is that if you have animated a lot of the leader's properties, you have to customize and apply the expression for each property. There is a time-saving trick that is helpful in some circumstances. In the examples above, the follower layers have been duplicates of the leader layer. If I were to set up a similar animation where I animated the leader's position, scale, rotation and opacity and I wanted the time-delayed followers to have all the leader's attributes, I could just pre-compose the leader layer, enable time remapping, apply the following time remapping expression, and duplicate the leader as many times as necessary. The rest is automatic.

follow all leader's animation


delay = 3; //number of frames to delay

d = delay*thisComp.frameDuration*(index - 1);
time - d

You can see that the followers all have a time-delayed copy of the leader's animations.

A Completely Different Approach

The method described above works well, but sometimes it's desirable to have the layers maintain a fixed distance instead of a fixed time delay. In the method previously described for following a leader's position, the layers all start out at the same location and if the leader stops, all the followers eventually catch up and end up in the same location as the leader.

As an alternate design goal, suppose that we want the layers to be more like the cars of a train. That is, if the leader slows down, stops, or even backs up, we want the cars of our train to maintain a fixed separation on the "track" (which is the path defined by the leader).

Follow the Null

The solution to this is not exactly straight forward, but it's a useful concept that's worth investing some time to understand. Here's how we do it. The key is that we use keyframes to animate the position of a 3D null to define a "template" for the path that our other layers will follow. The duration of the null's animation isn't critical - I generally use something like four seconds and name the null "template". You then add two sliders to the null - the first one (name it "offset") controls the separation between layers and the other one (name it "travel") defines the percentage of travel along the path.

You then apply this expression to as many 3D layers as you want in your "train":

follow null template


strt = 0; //start time of template motion
end = 4.0; //end time of template motion

t = thisComp.layer("template");
offset = (t.effect("offset")("Slider")/100)*(index -1);
travel = linear(t.effect("travel")("Slider")/100,strt,end);
t.position.valueAtTime(travel - offset)

Adjust the offset slider to spread your layers out along the path and adjust the travel slider so that the last layer just moves off the start position. You then set a keyframe for the travel slider to set the initial position. It's important to note that the travel slider does not start at zero (unless you want the layers to start out all bunched up). You then animate the travel slider between its initial value and 100 (which will give you full travel along the path). This animation can include slowing down and backing up and the layers will retain their relative spacing. Note that the animation time of the travel slider doesn't need to be the same as the time of your original animation of the template null. It's all based on percentages of travel so your final animation can be any length that you like. You might want to turn off the visibility of the null because it can be a little distracting to see it traveling at a different speed along the same path as the layers.

Adding Auto Orientation

If you want the layers to auto-orient to the path, you can add the following expression to the orientation property of the layers forming your "train":

adding auto orient


strt = 0; //start time of template motion
end = 4.0; //end time of template motion

t = thisComp.layer("template");
offset = (t.effect("offset")("Slider")/100)*(index -1);
travel = linear(t.effect("travel")("Slider")/100,strt,end);
if (travel <= offset){
  vect = t.position.velocityAtTime(0);
}else{
  vect = t.position.velocityAtTime(travel - offset);
}
lookAt(position, position + vect)

Following the Null's Orientation

OK - one last variation and we'll wrap this up. What if you wanted to have the layers follow not only the null template's path, but also its rotation? In this example, I have turned on the null's auto-orient-to-path, and also keyframed an additional Z rotation. I then changed the orientation expression of the follower layers to make them follow the null's orientation:

following null's orientation


strt = 0; //start time of template motion
end = 4.0; //end time of template motion

t = thisComp.layer("template");
offset = (t.effect("offset")("Slider")/100)*(index -1);
travel = linear(t.effect("travel")("Slider")/100,strt,end);

u = t.toWorldVec([1,0,0],travel - offset);
v = t.toWorldVec([0,1,0],travel - offset);
w = t.toWorldVec([0,0,1],travel - offset);

sinb = clamp(w[0],-1,1);
b = Math.asin(sinb/thisComp.pixelAspect);
cosb = Math.cos(b);
if (Math.abs(cosb) > .0005){
  c = -Math.atan2(v[0],u[0]);
  a = -Math.atan2(w[1],w[2]);
}else{
  a = Math.atan2(u[1],v[1]);
  c = 0;
}
[radiansToDegrees(a),radiansToDegrees(b),radiansToDegrees(c)]

This expression gets into some pretty hairy 3D space transforms. If you like that kind of thing - enjoy. Otherwise just know that it works and file it away for future reference.