DEV Community

Cover image for After Effects: Text Animation And The Expression Selector
Kat
Kat

Posted on • Edited on

After Effects: Text Animation And The Expression Selector


Introduction

In the first article of this series, After Effects Basics, I briefly mentioned the text animator. It is a tool in After Effects, which allows for animation of independent characters, words, or lines, as opposed to the layer controls, which affect the entire text layer as a whole.

Screenshot of the text animator options

However it isn't the most intuitive to use. Therefore I wanted to go over it again in depth, starting with basic concepts, all the way to advanced techniques.

Let's get started.


Using The Range Selector

From the properties panel, click the +Add Animator button. Select the property you want to animate. For this example, I will be using position.

When you select a property, the layer will have new options open up in the timeline, like so:

Screenshot after adding a position text animator

You'll see the new property, Animator 1 added. Inside of it, there is Range Selector 1, and a new position stopwatch. This is the basic setup for the text animator.

Change the y coordinate of your position to 100. You'll notice the text shift down 100 pixels. Next, open up the Range Selector property. You'll see start, end and offset parameters (as well as some advanced options). To demonstrate how the range selector works, make a keyframe at the beginning of your timeline for the start parameter, setting it to 0. Then, at 1 second, add another keyframe setting it to 100. As you play this back, you'll see the letters of your text layer animate, one by one, raising back to their default position.

Text Animator start Position GIF

As the name suggests, the start and end parameters of the range selector control how much of the text is affected by all the properties listed inside Animator 1 (in this case, position). With the start set to 0, and the end set to 100, the entire range is selected, so all characters will be affected. However, as we change the start from 0 to 100 over time, it stops affecting the text gradually as the range gets smaller, resulting in none of the text being affected once it reaches 100.

You can also create a similar animation using the offset parameter, instead of the start or end. This allows you to keep your range fixed, but offsets the values from -100 to 100. This allows for a bit more flexibility, and to easily reverse the animation.

Text Animator offset Position GIF

Finally, if you explore the advanced options of the range selector, you can find some options to help fine tune the animation. You can change whether or not the range selects the text by character, word or line. Perfect if you have a long sentence or heading. You can also affect the easing of the animation, randomise the order characters are affected, and change the shape of the animation. This will alter how the text transitions, and is worth playing with to achieve different results.

screen shot of the based on parameter

You can also apply multiple Animators to the same text layer with slightly offset keyframes, to achieve some fun effects.

To try this, remove all keyframes currently on your timeline. Set start to 0, end to 100, and animate offset over 1 second, from -100 to 0. Then, duplicate Animator 1. This will create an Animator 2 property. Open this up, and set the position inside Animator 2 to -50. Select the keyframes for Animator 2's range selector, and move them forward 4 frames. You'll notice this creates a small bounce back for your text.

Text Animator 2 Animators GIF

If you want to add additional properties to the same Animator, you can do so by clicking on the add button next to the Animator in the timeline, selecting property, then whatever property you would like to add.

add button

And that covers the range selector. For some projects, this is enough. Stop here for a bit and have a play with this selector. Try using different properties within the same Animator, or create multiple Animators with different effects. Once you're happy you understand how this control works, you can move onto the next step: working with the expression selector.


Using The Expression Selector

The range selector is not perfect. It does not allow us to control the delay between each letter as it animates, but instead limits us to changing the shape of the animation, the offset, and ease. This means that animations can sometimes be hard to time, and often, does not allow for a smooth transition of letters. What if you want to start the animation of the second letter before the first has completely finished?

If you need to be more precise, you will need to use the expression selector.

It took me some time to work out how to use this selector. But now that I have, I much prefer it to the range selector.

First, create a new text layer, add an Animator with a position property like before, and set the y coordinate to 200. This time however, delete the range selector. Then from your timeline, click the add button next to Animator 1. Select Selector/Expression. Opening the property up, you will see it has a lot less options than the range selector. The Based On option allows you to change whether the selector affects characters, words, or lines. Other than this, there is simply an Amount property, which changes how much the Animator applies to the text. By default, there is an expression written in the amount parameter: selectorValue * textIndex/textTotal, and your text layer will look something like this:

Screenshot Expression selector 01

This immediately throws 3 variables at us without explanation. Allow me to go through these now, and why they are affecting the text in this way.

selectorValue is referencing the Amount value of this property. By default, it is set to 100, 100, 100.

textIndex assigns an index number to each character, word or line in your text layer (depending on what your based on property is set to), so that each may be affected individually.

textTotal is the total number of characters, words, or lines in your text layer (depending on what your based on property is set to).

With my based on set to Characters, we can now make sense of the expression written in the amount property.

textIndex/textTotal will produce a number between 0 and 1. This in turn is multiplied against the selectorValue. Therefore, the first character in the text layer with an index of 1, will be affected by the Animator only a small fraction. However, as textIndex increases in value, the amount the Animator affects the text also increases. This goes on until we reach the last character of the text layer, which is 100% affected by the effects of the Animator.

Another way to understand it, is to see each letter affected in sequence.

Let's replace the default expression with this one:

if (textIndex == Math.floor(time)) 100
    else 0
Enter fullscreen mode Exit fullscreen mode

Create an if statement. If the textIndex is the same as Math.floor(time), the Animator will be applied to it 100% (Math.floor is applied to ensure the statement only returns integers). Else, it will be applied at a value of 0 (meaning it will have no effect whatsoever).

As you scrub through the timeline, you will see one by one the letters of your text move.

Screenshot expression selector 02

This is how we select our range in the expression selector. This may be a bit confusing at first. But hopefully, I can help demystify the process of writing expressions for dynamic text movement a little bit.

So, we now know textIndex is one of the values we need to consider when writing the expression. Let's now consider the other values we will need.

Go ahead and change the y position property within Animator 1 to -200. This will help us write the expression to come.

Delete the expression from your expression selector, and create the following variables:

var frames = 1 / thisComp.frameDuration;    //comp frame rate
var delay = 1;  //frame delay between each character, in frames
var dur = 6;    //duration of animation per character, in frames
Enter fullscreen mode Exit fullscreen mode

frames is simply the framerate of the current composition. We will need this in order to convert our other values into frames rather than seconds, since working in seconds can be confusing when needing such small increments. This is easily set by dividing 1 by thisComp.frameDuration.

delay is the time between each character in our animation. The lower the number, the less of a delay, therefore the quicker the animation. Setting it to 1 will result in a 1 frame delay between each character - meaning the second letter will start to animate 1 frame after the first letter starts to move, and so on.

dur is the duration of the animation, per character. I've set mine to 6 for a medium speed animation.

Now we have established our variables, we can create an ease function to animate the text. This will provide a gradual change in value to our characters, vs our previous if statement.

Let's take a look at this:

var frames = 1 / thisComp.frameDuration;
var delay = 1;
var dur = 6;

var indexDelay = textIndex*(delay/frames);
var frameDur = dur/frames;

ease(time, indexDelay, indexDelay + frameDur, 100, 0);
Enter fullscreen mode Exit fullscreen mode

As you can see, we start off again with our variables frames, delay, and dur. But there are two more variables we need to create in order to connect them with the textIndex value, and keep the ease function easy to read.

First is indexDelay. This simply multiplies the textIndex with our delay variable (divided by frames, to convert the delay into the correct time increments). This will dictate when each letter begins its animation.

The second is frameDur. This simply divides dur by frames, to convert the animation duration into the correct time increment.

Finally, the ease function. If you are unfamiliar with the ease function, I go into it in depth in this article. I highly recommend making yourself familiar with how this function works before continuing.

I set the first argument to time, so the selector value is remapped to time and will change as the seconds tick by. Next, I set my in and out points to indexDelay, and indexDelay + frameDur. This means each character will animate in turn from its own index specific start point, for as long as our dur value, in frames. Finally, the last 2 arguments will set what values the selector property animates between. These are set to 100 and 0 respectively, meaning the Animator will go from 100% affecting the text, to 0% affecting it, returning the text to its default position.

The result of this expression is this:

Apricot GIF 01

Already, we can see a big difference in the timings here vs the range selector. Here, we are able to play with the timings in a much more precise way. Feel free to adjust the numbers of the delay and dur variables, to see what can be achieved.

Like the range selector, you can duplicate Animator 1 to create an Animator 2. By simply adjusting time in Animator 2's ease function, you can create a similar bounce effect as we did for the range selector before:

Animator 2 expression


var frames = 1 / thisComp.frameDuration;
var delay = 1;
var dur = 6;

var indexDelay = textIndex*(delay/frames);
var frameDur = dur/frames;
var inTime = (thisComp.frameDuration * 4);

ease(time - inTime, indexDelay, indexDelay + frameDur, 100, 0);
Enter fullscreen mode Exit fullscreen mode

Result with 2 expression selector animators:
Apricot GIF 02

I created the variable inTime to store my 4 frame calculation, to keep the ease function tidy.

For the cherry on top, try adding an opacity property to Animator 1, so the text also fades in as it drops down (remember: you can do this by clicking the add button next to Animator 1, selecting Property/Opacity, and setting it to 0):

Apricot GIF 03

Finally, if you want the text to animate on and off, you need only use an if statement to toggle between 2 different ease functions on both your Animators:

Animator 1 expression

var frames = 1 / thisComp.frameDuration;
var delay = 1;
var dur = 6;

var indexDelay = textIndex*(delay/frames);
var frameDur = dur/frames;

var timeOut = linear(time, 2, 4, 0, 2);

var aniIn = ease(time, indexDelay, indexDelay + frameDur, 100, 0);
var aniOut = ease(time - 2, indexDelay, indexDelay + frameDur, 0, 100);

if (time < 2) aniIn
    else aniOut
Enter fullscreen mode Exit fullscreen mode

Animator 2 expression

var frames = 1 / thisComp.frameDuration;
var delay = 1;
var dur = 6;

var indexDelay = textIndex*(delay/frames);
var frameDur = dur/frames;
var inTime = (thisComp.frameDuration * 4);

var aniIn = ease(time - inTime, indexDelay, indexDelay + frameDur, 100, 0);
var aniOut = ease(time - inTime - 2, indexDelay, indexDelay + frameDur, 0, 100);

if (time < 2 - (thisComp.frameDuration * 4)) aniIn
    else aniOut
Enter fullscreen mode Exit fullscreen mode

Result:

Apricot GIF 04

I advise you to stop and have a play with this now, to see what you can make before moving onto the advanced techniques.


Advanced Techniques

So, the range selector lets us fine tune our movements, but not control the delay between our letters. Whereas using the expression selector, we can control the delay, but the default linear and ease functions leave us with only two types of interpolation for our animation.

Or, does it?

What if we want more dynamic animation?

Then we need to create our own custom easing functions. Or, we can use functions others have already created, such as Penner's Easing functions.

I have gone into depth about these functions in this article before. In short, they are a collection of equations which, in Penner's own terms, add "flavour to motion." And, thanks to these functions being open source, we can use them in our After Effects templates, or make our own .jsx files to store them in free of charge (click on the previous article for a full rundown of how to do this). This saves us massive amounts of time - so thank you very much once again, Robert Penner.

Importing my .jsx file to my project, I make a new text layer and add an expression selector to it like before. Then, I start by referencing my .jsx file:

const penner = footage("Penner Easing Functions.jsx").sourceData.getFunctions();
Enter fullscreen mode Exit fullscreen mode

If you are not looking to make a .jsx file, but instead are adding custom functions manually to your project each time, I will post the details of the functions I will be using here. My first example will be using the easeInOutSine function, which you can create by adding...

 function easeInOutSine (t, b, c, d) {
            return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
        };
Enter fullscreen mode Exit fullscreen mode

...to the start of your expression, instead of calling a .jsx file.

By using the easeInOutSine function, we can make a constantly moving text layer, bobbing up and down following a sine curve.

After calling our .jsx file, or establishing the function, we set up our variables, the same as we did before:

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 12;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
Enter fullscreen mode Exit fullscreen mode

Then, once we are happy, we can write our final expression. Since the Penner Easing functions work slightly differently to the default ease and linear expressions, our variables will be inputted in a slightly different order:

easeInOutSine(time - indexDelay, 100, -100, frameDur)
Enter fullscreen mode Exit fullscreen mode

Using easeInOutSine, the indexDelay is subtracted from time, our first argument, since this function does not establish an in and out point for value remapping. The second argument is the starting value of our selector, which is set to 100. The third argument is the change in value, so this is set to -100, to bring the selector value to 0. The last argument is the duration of the animation. This is simply set to frameDur.

The result:
Orange GIF 01

Wow - what a cool trick! Even though we specified a duration of frameDur, the text will move continuously, as we have not set any boundaries for the custom function to stop its calculations (i'll be getting to this shortly).

By adjusting the delay, we can control the space between each letter like so:
Delay of 3
Orange GIF 02

Delay of 4
Orange GIF 03

Delay of 5
Orange GIF 04

Already, the effect can be varied substantially, just by changing the delay value. But the variations don't have to stop there. What if instead of subtracting the textIndex from time, we added it to our dur value? This would create an animation, where every letter of our text moved at a slightly different speed, depending on its textIndex value.

var frames = 1 / thisComp.frameDuration;
var dur = 12;

var durAlt = (textIndex + dur) / frames;

//Return
easeInOutSine(time, 100, -100, durAlt)
Enter fullscreen mode Exit fullscreen mode

Since we are no longer referencing delay, we can remove this variable and marvel at the results.

Orange GIF 06

This animation was actually the result of a mistake I made testing these expressions. But I really like it! I think it's a great way to add randomness to your work, as it is forever evolving. It is also a testament to the importance of experimentation. I never would have found this possibility if I didn't give myself room to explore and make mistakes.

So, to recap the full advanced expression we've worked with so far:

easeInOutSine full expression with .jsx file

const penner = footage("Penner Easing Functions.jsx").sourceData.getFunctions();

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 12;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var durAlt = (textIndex + dur) / frames;

//Return
penner.easeInOutSine(time - indexDelay, 100, -100, frameDur) //Evenly timed expression

//or

penner.easeInOutSine(time, 100, -100, durAlt) //Each character animating at different speeds
Enter fullscreen mode Exit fullscreen mode

easeInOutSine full expression without .jsx file

 function easeInOutSine (t, b, c, d) {
            return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
        };

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 12;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var durAlt = (textIndex + dur) / frames;

//Return
easeInOutSine(time - indexDelay, 100, -100, frameDur) //Evenly timed expression

//or

easeInOutSine(time, 100, -100, durAlt) //Each character animating at different speeds
Enter fullscreen mode Exit fullscreen mode

This is all well and good for animating with a sine, but having values go out of bounds may not be as useful for other custom functions. Let's take the easeInOutBack function for example:

function easeInOutBack (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
            return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
        };
Enter fullscreen mode Exit fullscreen mode

If we use the previously devised expression for this function:

easeInOutBack(time - indexDelay, 100, -100, frameDur)
Enter fullscreen mode Exit fullscreen mode

We get this:
Cherry GIF 01

Here, we have a problem. The animation is only supposed to drop down once. But, because the function is remapping to time, it continues moving after the specified duration and drops down twice - moving the selector value and going out of bounds. This is because we haven't set any boundaries on our time. Even after our specified duration, time continues to tick, giving the function space to continue its calculations.

In order to fix this, we need to clamp the time parameter. Let's make another variable to store this new value in:

var inTime = clamp(time - indexDelay, 0, frameDur);
Enter fullscreen mode Exit fullscreen mode

Here, we clamp time, preventing it from counting higher than the frameDur variable. However, because we are subtracting the indexDelay from time first, each character will hit this limitation in turn, allowing each letter to complete its animation before coming to a halt.

easeInOutBack full expression with .jsx file

const penner = footage("Penner Easing Functions.jsx").sourceData.getFunctions();

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 12;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var inTime = clamp(time - indexDelay, 0, frameDur);

//Return
penner.easeInOutBack(inTime, 100, -100, frameDur)
Enter fullscreen mode Exit fullscreen mode

easeInOutBack full expression without .jsx file

function easeInOutBack (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
            return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
        };

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 12;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var inTime = clamp(time - indexDelay, 0, frameDur);

//Return
easeInOutBack(inTime, 100, -100, frameDur)
Enter fullscreen mode Exit fullscreen mode

Result:
Cherry GIF 02

Nice! Using the easeInOutBack function, we can make a text animation with a bounce, using only one 1 Animator. Much more simple than creating 2 and offsetting the time like before!

You can also control how "bouncy" this expression is by adding the 5th argument to this custom function: s. According to the expression, while undefined it is 1.70158. So if we change it to a much larger value (and increase our dur value to account for the extra animation time), we can make the movement back more pronounced:

easeInOutBack(inTime, 100, -100, frameDur, 5)
Enter fullscreen mode Exit fullscreen mode

Result:
Cherry GIF 03

There's one last thing to fix with the easeInOutBack function before we continue. The expression is supposed to ease back at the start and the end of the movement (hence ease in and out) - however currently it only moves back at the end. This is because at the start, our selector is already at max value: 100. If we want to see the move back at the start, we need to leave room for the selector to move back to. Therefore, I should set my starting value to 90 instead.

easeInOutBack(inTime, 90, -90, frameDur)
Enter fullscreen mode Exit fullscreen mode

Result:
Cherry GIF 04

Perfect! Now that I have fixed everything for the function to run correctly, I can use it to animate in and out by creating a new aniOut variable, and an if statement like before:

easeInOutBack full expression with .jsx file

const penner = footage("Penner Easing Functions.jsx").sourceData.getFunctions();

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 10;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var inTime = clamp(time - indexDelay, 0, frameDur);
var outTime = clamp(time - 1.5 - indexDelay, 0, frameDur);

//Functions
var aniIn = penner.easeInOutBack(inTime, 90, -90, frameDur)
var aniOut = penner.easeInOutBack(outTime, 0, 90, frameDur)

if (time < 1.5) aniIn
    else aniOut
Enter fullscreen mode Exit fullscreen mode

easeInOutBack full expression without .jsx file

function easeInOutBack (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
            return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
        };

var frames = 1 / thisComp.frameDuration;
var delay = 2;
var dur = 10;

var indexDelay = textIndex * (delay/frames);
var frameDur = dur/frames;
var inTime = clamp(time - indexDelay, 0, frameDur);
var outTime = clamp(time - 1.5 - indexDelay, 0, frameDur);

//Functions
var aniIn = easeInOutBack(inTime, 90, -90, frameDur)
var aniOut = easeInOutBack(outTime, 0, 90, frameDur)

if (time < 1.5) aniIn
    else aniOut
Enter fullscreen mode Exit fullscreen mode

Result:
Cherry GIF 05

And with that, we have a good foundation to begin experimenting with Penner's other custom easing functions.

Try using other functions, other properties, and seeing how the delay and dur values can alter the effects. Get creative - and if you've made it this far, congratulations! You are now a master of the After Effects text animator.


Conclusion

All examples covered here in this tutorial are achieved using predominantly 1 property: position (with a little helping hand from opacity), only 2 of Penner's Easing functions, and the range or expression selectors. There is still a lot of room for experimentation - for instance, combining the use of range selectors and expression selectors.

As I said before, the text animator tool is powerful. It can create so many unique and incredible animations, and the best part about using expression selectors is that they leave the text layers keyframe-less, and editable - perfect for creating dynamic templates.

I hope this article has been inspiring, and given you the tools to play with your own animations.

As always, do you have any questions? Perhaps you know of a better way to achieve this? Let me know in the comments. I'd also love to see what you create!

Image description

Top comments (0)