Quick Tip: Branchless Programming

Branchless programming, in a general sense, refers to the use of mathematical operations utilizing conditional bools (i.e. true or false) in place of if/else statements.

Here’s a good primer that speaks to how it’s used and why it matters. The short version is this: if/else statements take a little bit of excess processing power because both branches of the if/else (that is, what happens when your condition is met and what happens when it’s not) are temporarily committed to memory. The stuff that’s associated with the ‘wrong’ branch is just tossed out when the expression figures out if the condition is met. It’s like the After Effects expression engine is an eager personification of a calculator, shivering, sweaty with excitement, getting both answers ready before you’ve even asked it to do that.

Here’s a quick, simple After Effects example that illustrates the concept. Let’s say you want to flip the opacity of a layer completely off when the scale falls below 20%.

This is an if/else statement to accomplish that task:

if (scale[0] >= 20){ // x-scale greater than or equal to 20
value;
} else {
0;
}

There’s absolutely nothing wrong with this expression. It will serve you well. What I’d like to draw your attention to, though, is the fact that the condition statement in the if/else [in this case, (scale[0] >= 20)], for all intents and purposes, works out to a 0 when false and a 1 when true. With this in mind, take a gander at this execution of the operation above:

value * (scale[0] >= 20);

It accomplishes effectively the same thing as the if/else statement. When the scale (the x-dimension of the scale, that is) is less than 20, the condition works out to 0.

The above example is kind of a cherry-picked best-case-scenario for the savings on typing. Calculating a 0 for your final expression result with this method is very easy.

Here’s another example of an if/else that we can accomplish without branches. Let’s say we want to control the size of a Rectangle Shape layer based on the y-position relative to a null object’s y-position. Let’s say our rectangle size is keyframed, and we want it to be half the size when the layer drops below the null (which we’ll call “groundNull“):

nPos = thisComp.layer("groundNull").position;
pos = position;
if (pos[1] <= nPos[1]){
value;
} else {
value*.5;
}

Again. This expression is perfectly valid. It will accomplish what you need it to. Let’s take a look at the branchless version. We just need to keep in mind how to write the inverse of the condition.

nPos = thisComp.layer("groundNull").position;
pos = position;
value * (pos[1] <= nPos[1]) + (value*.5)*(pos[1] > nPos[1]);

What you’ll notice is that things seem to get a little convoluted and redundant. First, take note that we need to explicitly write the inverse of the original condition to catch the rest of the cases when the original condition is not true.

  • (pos[1] <= nPos[1]) [y-position is less than or equal to groundNull y-position]
  • (pos[1] > nPos[1]) [y-position is greater than groundNull y-position]

We’re using a simple addition operation to make sure both possibilities of the conditions make their way to the final result.

Now– the astute among you might holler at me…”But Steve, doesn’t this expression have to calculate both results just like an if/else statement does?! Is there really any time savings at all with this example?!” You’re right. At the scope of these simple example expressions, you’re likely not going to notice any change in how fast your frames are calculated. I think this branchless workflow is best used if you have nested if/else statements that can be accomplished using simple math. (The point of this post, really, is to get you comfortable with the fact that you can use the bool resulting from a condition as a 0 or a 1 in a math operation.) Let me rewrite the rectangle size expression from above to see if I can consolidate some of the logic and refactor the math.

nPos = thisComp.layer("groundNull").position;
pos = position;
value * (1 - .5*(pos[1] > nPos[1]));

You’ll notice that I’m using the inverse of my original condition in the example. The reason for doing that will make sense in a second. (Spoiler alert: it just makes the math look prettier.) It’s easiest to understand the example above by testing out what happens when the condition is replaced with a 0 or a 1.

In the event that the y-position of the layer is greater than the y-position of the groundNull:

nPos = thisComp.layer("groundNull").position;
pos = position;
value * (1 - .5*(1));

The stuff in the parentheses works out, in this case, to (1 - .5)… or, yes, you got it… (.5). The calculated result is value*(.5).

In the event that the y-position of the layer is less than or equal to the y-position of the groundNull:

nPos = thisComp.layer("groundNull").position;
pos = position;
value * (1 - .5*(0));

The parenthesized result works out to (1 - 0)… and yeah… I see you understanding this jam…. I can smell it…. it’s (1). The result of value * (1) is value. Wow.

I can accomplish the same thing, if I wanted to use the original condition (pos[1] <= nPos[1]) (less than or equal to), just by altering the math.

nPos = thisComp.layer("groundNull").position;
pos = position;
value * (.5 + .5*(pos[1] <= nPos[1]));

I’ll leave you to internalize how that one works on your own. Hint: the math is just a slightly different route to getting the result of a .5 or a 1 in the parentheses.

Wrapping up

Because I don’t have a thorough understanding of how the Javascript or Legacy expression engines in After Effects work under the hood, I can’t tell you for sure if the introduction of branchless programming into your expressions is going to be a life-changing improvement to your frame calculation times. You should see it as simply a tool for the tool-belt. I personally prefer expressions that are more concise as opposed to verbose. If/else statements make for nice expressions because are easy to read and understand. The logic is separated and put into compartments. The drawback, though, is that they have the potential to be redundant– you might have lots of the same code copy/pasted between the two conditions just to get something working.

I like to think that any step to simplify my expressions (reducing the number of operations the expression has to do, that is) is a step that will help my preview/frame-calculation/render times… however slight. It’s not out of the ordinary for me to work on a composition that has hundreds of layers with identical expressions pasted on them. I have to believe that across those hundreds of layers, in a comp that’s hundreds of frames long, I have some control over how fast a preview or render happens– just by the way I write my expressions.

Case Study: Convex Hull

Picture this, ya goober: a huge set of points in space.

3D points in space revolving around a null
Admittedly not the hugest set if points.

You want to find the minimum wrapping shape that contains the entire set of points– the convex hull. The goal is to revolve the entire system of points– and have that wrapping shape update dynamically. Sure, you can try to do it the manual way….

using the pen tool to trace the convex hull
Look how manual.

The problem is, though– not only will the set of points that define the edge change over the course of the revolve, the number of points along the hull is likely to change, too. Those roto keyframing skills you have– keeping keyframes attached to their respective landmarks (so the interpolation is predictable) — they’ll do you no good here… because those landmarks along the silhouette aren’t going to be landmarks for the whole sequence.

I think you know where we’re going with this. An entirely new shape has to be drawn every frame. Get that Pen tool ready and hunker down, friend. I hope your podcast queue has some generous padding.

Ha haha. Kidding. This problem is luckily a simple gift wrapping algorithm. We have the tools we need in expressions to calculate this shape dynamically.

rotating set of 3D points wrapped in a convex hull
It works. It really works.
function indexOfMin(arr) { // find index of point that has the lowest x-position.
    if (arr.length === 0) {
        return -1;
    }
    var min = arr[0][0];
    var minIndex = 0;
    for (var i = 1; i < arr.length; i++) {
        if (arr[i][0] < min) {
            minIndex = i;
            min = arr[i][0];
        }
    }
    return minIndex;
}
// create the necessary things
points = [];
t=[];
hull = [];
pInd = [];
hullInd = 0;
points[0] = fromCompToSurface(thisComp.layer("ant_0200").toComp([0,0,0]));
points[1] = fromCompToSurface(thisComp.layer("ant_0201").toComp([0,0,0]));
points[2] = fromCompToSurface(thisComp.layer("ant_0202").toComp([0,0,0]));
points[3] = fromCompToSurface(thisComp.layer("ant_0203").toComp([0,0,0]));
points[4] = fromCompToSurface(thisComp.layer("ant_0204").toComp([0,0,0]));
points[5] = fromCompToSurface(thisComp.layer("ant_0205").toComp([0,0,0]));
points[6] = fromCompToSurface(thisComp.layer("ant_0206").toComp([0,0,0]));
points[7] = fromCompToSurface(thisComp.layer("ant_0207").toComp([0,0,0]));
points[8] = fromCompToSurface(thisComp.layer("ant_0208").toComp([0,0,0]));
points[9] = fromCompToSurface(thisComp.layer("ant_0209").toComp([0,0,0]));
points[10] = fromCompToSurface(thisComp.layer("ant_0210").toComp([0,0,0]));
points[11] = fromCompToSurface(thisComp.layer("ant_0211").toComp([0,0,0]));
points[12] = fromCompToSurface(thisComp.layer("ant_0212").toComp([0,0,0]));
points[13] = fromCompToSurface(thisComp.layer("ant_0213").toComp([0,0,0]));
points[14] = fromCompToSurface(thisComp.layer("ant_0214").toComp([0,0,0]));
points[15] = fromCompToSurface(thisComp.layer("ant_0215").toComp([0,0,0]));
points[16] = fromCompToSurface(thisComp.layer("ant_0216").toComp([0,0,0]));
points[17] = fromCompToSurface(thisComp.layer("ant_0217").toComp([0,0,0]));
points[18] = fromCompToSurface(thisComp.layer("ant_0218").toComp([0,0,0]));
// simple modulo to make sure our indices stay in-range of the length of the points array
function cInd(_i){ 
  return _i%points.length;
}
// find left-most point
i1 = indexOfMin(points);
// add leftMost to hullArray
addToHull(i1);
// tentative winner
cwInd = cInd(i1+1); 
// loop through every point to find the actual winner
for (var j = 1; j < points.length; j++){
	checkInd = cInd(pInd[hullInd-1]+j); // the contender
	v1 = sub(hull[hullInd-1], points[cwInd]); // vector from the last point on the hull to the tentative winner
	v2 = sub(hull[hullInd-1], points[checkInd]); // vector from the last point on the hull to the contender
	if (cross(v1,v2)[2] < 0){ // if v2 is counter-clockwise to v1, v2 is the new winner
	  cwInd = checkInd;
	}
	// when we reach the end of the loop, add the winner to the hull
	if (j == points.length-1){
		reached = addToHull(cwInd);
		cwInd = cInd(cwInd+1);
		// reset loop until the winning hull point is the left-most point again
		if (!reached){
		j=1; 
		}
	}
}
function addToHull(_pInd) {
    temp = hullInd;
    endReached = false;
    if (pInd[0] == null || pInd[0] !== _pInd) {
        hull[hullInd] = points[_pInd]; // add winning point to hull
        pInd[hullInd] = _pInd; // store index of this point's index 
        t[hullInd] = [0, 0]; // add point tangents
        hullInd++;
    } else {
        endReached = true;
    }
    return endReached;
}
createPath(hull, t, t, true);

This expression assumes two things:

  1. Every point has its anchor point at [0,0,0]… as Null objects do… or as Shape Layers you populate using the “Add” menu in the timeline do.
  2. This expression is on a Path property (either within a Shape Layer or a Mask Path).

The algorithm is fairly simple. I’ll do my best to describe it, but know that I’m a bit of a computer science ding-dong, so you might want to check out some learning materials about Graham scans for more info.

  1. Find the left-most point in our set. This is going to be the first point on our wrapping shape.
  2. We then check every point to find which one has the most “counter-clockwise-est” angle from the first point. (You could also think of this as traveling from the first point to the point that is the greatest left turn.) The winner gets added to the hull.
  3. Finding the subsequent ‘winners’ is a matter of repeating the above step, using the last winner we’ve added to our wrapping shape as our “traveling from” point.
  4. We exit out of the algorithm when the left-most point (the first point we added to our hull) is the winner.

Setup

For any of this stuff to work, we first need to load up an array with all of the locations of every point layer in the comp. To be completely honest with you, this was the most tiring step in the process of working out the expression. You declare an empty array as a variable (points = [];), and then you can manually fill the array, hard-coding the indices of the array with the locations of your point layers. I opted to use a fromCompToSurface via the comp space position (toComp) for each layer, so the placement of the convex hull layer (the Solid or Shape Layer) would be irrelevant…. if I accidentally nudged it out of place.

points[0] = fromCompToSurface(thisComp.layer("ant_0200").toComp([0,0,0]));
points[1] = fromCompToSurface(thisComp.layer("ant_0201").toComp([0,0,0]));
points[2] = fromCompToSurface(thisComp.layer("ant_0202").toComp([0,0,0]));
points[3] = fromCompToSurface(thisComp.layer("ant_0203").toComp([0,0,0]));
points[4] = fromCompToSurface(thisComp.layer("ant_0204").toComp([0,0,0]));
points[5] = fromCompToSurface(thisComp.layer("ant_0205").toComp([0,0,0]));
points[6] = fromCompToSurface(thisComp.layer("ant_0206").toComp([0,0,0]));
points[7] = fromCompToSurface(thisComp.layer("ant_0207").toComp([0,0,0]));
points[8] = fromCompToSurface(thisComp.layer("ant_0208").toComp([0,0,0]));
points[9] = fromCompToSurface(thisComp.layer("ant_0209").toComp([0,0,0]));
points[10] = fromCompToSurface(thisComp.layer("ant_0210").toComp([0,0,0]));
points[11] = fromCompToSurface(thisComp.layer("ant_0211").toComp([0,0,0]));
points[12] = fromCompToSurface(thisComp.layer("ant_0212").toComp([0,0,0]));
points[13] = fromCompToSurface(thisComp.layer("ant_0213").toComp([0,0,0]));
points[14] = fromCompToSurface(thisComp.layer("ant_0214").toComp([0,0,0]));
points[15] = fromCompToSurface(thisComp.layer("ant_0215").toComp([0,0,0]));
points[16] = fromCompToSurface(thisComp.layer("ant_0216").toComp([0,0,0]));
points[17] = fromCompToSurface(thisComp.layer("ant_0217").toComp([0,0,0]));
points[18] = fromCompToSurface(thisComp.layer("ant_0218").toComp([0,0,0]));

And yes– I know what you’re thinking. Can I avoid that manual code and just use a loop to fill the array like how the Create Nulls From Paths window does it? Yes. You absolutely can. I opted for the long-winded way of doing it to save on a bit of calculation/processing time. The algorithm expression is pretty heavy, so anywhere I can save on the number of for loops I need to do– it can’t hurt. And if you have a bit of scripting know-how**, generating that array-fill list can be a pretty fast operation.

My suggestion for creating that array manually (by typing) is making sure your layers have predictable, repeatable names… so you only need to alter a number. In the list above, you can see that my null name suffixes are directly related to their index within the array “ant_0218“, for example, is at index 18 within my array.

The array t (t = [];) is the array where tangent positions get added for use in createPath(). It’s simply populated alongside the hull array with [0,0] positions. (…to make pointy corners.)

The array called hull (hull = [];) is the array that will be populated with the positions of all the vertices of our wrapping shape (the calculated position values of the ‘winners’). It’s the one we use to draw the polygon with createPath(). The array pInd (pInd = [];) gets populated alongside the hull array with the index of the ‘winners’. For example, let’s say the 6th vertex on our wrapping shape (the 6th point added to hull) is points[11]. hull[5] is the 2D position that is calculated (fromCompToSurface via the layer’s [0,0] in composition space). The value in the array pInd at index 5 (pInd[5], in other words) is 11.

points[11] = fromCompToSurface(thisComp.layer("ant_0211").toComp([0,0,0]));

The Processing

Lucky for you, once you have the list for your points array written out, the rest of the expression just works. If you don’t care about how or why the thing works, that’s fine. Thanks for reading. I’m gonna do my best to break down the expression piece by piece so you can get an idea of why things are written the way they are.

The left-most layer

This function is the way to find our left-most layer. It’s a modified version of this “find index of greatest value” JavaScript function:

function indexOfMin(arr) { // find index of point that has the lowest x-position.
    if (arr.length === 0) {
        return -1;
    }
    var min = arr[0][0];
    var minIndex = 0;
    for (var i = 1; i < arr.length; i++) {
        if (arr[i][0] < min) {
            minIndex = i;
            min = arr[i][0];
        }
    }
    return minIndex;
}

The reason we need to find the index, as opposed to just finding the value of the left-most x-position (like Math.min() would give us), is that we need the position array– the x and y to do our calculations in the algorithm– not just the x-value.

Keeping track of indices

The next thing I’d like to look at is this weird little function:

function cInd(_i){ 
  return _i%points.length;
}

I knew the looping on this algorithm was going to be messy if I didn’t manage the starts and ends of my loops in a smart, predictable way. I wanted to make sure I was excluding the vertex added last to the hull array by default, without having to write a conditional statement in the winner-search loop. The loop to find the next winner would start with the layer at the next index of my ‘points’ array from the current “traveling from” layer (the winner that just got added to the hull). So, if points[5] was just added to the hull array, the search for the next winner would start at points[6]. The modulo operator (% – the remainder) in that cInd function is the way to handle the case where points[18] (in this example) would roll back over to points[0].

The variables cwInd and checkInd are the ways of keeping track of the ‘current winner’ and the ‘contender’, respectively. They duke it out to see who’s the most counter-clockwise-est.

Who’s the most counter-clockwise-est?

This block of code is the math that makes it all happen:

v1 = sub(hull[hullInd-1], points[cwInd]); // vector from the last point on the hull to the tentative winner
v2 = sub(hull[hullInd-1], points[checkInd]); // vector from the last point on the hull to the contender
if (cross(v1,v2)[2] < 0){ // if v2 is counter-clockwise to v1, v2 is the new winner
  cwInd = checkInd;
  }

In short, we use the cross product (the ‘z’ value of the cross product, specifically) of 2 vectors:

  1. The vector defined by looking from the last point that got added to the hull array to the current, tentative winner.
  2. The vector defined by looking from the last point that got added to the hull array to the contender.

Think about a pair of 2D vectors sticking out of a common point on a screen (let’s call them a and b). The cross product, basically, (apologies to actual smart trigonometry/vector math people for this explanation)…. is a vector that points either into the screen (away from you) or out of the screen (toward you), depending on whether a is clockwise in relation to b or vice versa. I would highly recommend seeking better reference materials to learn about the cross product… because I’m a bit of a vector math dingus.

Thankfully, to find our winner, we don’t need to know the exact value of the cross product, we just need to know if it’s negative or positive (whether it points at you or away from you). (Please forgive me, vector math experts. I get confused about which direction– positive or negative z-position– is the result of clockwise vs. counter clockwise.)

Add. Add. Add.

The addToHull function is fairly simple in the grand scheme of what’s going on in this expression.

function addToHull(_pInd) {
    temp = hullInd;
    endReached = false;
    if (pInd[0] == null || pInd[0] !== _pInd) {
        hull[hullInd] = points[_pInd]; // add winning point to hull
        pInd[hullInd] = _pInd; // store index of this point's index 
        t[hullInd] = [0, 0]; // add point tangents
        hullInd++;
    } else {
        endReached = true;
    }
    return endReached;
}

Basically, I take the index of the current winner, and use it to insert the necessary things into the necessary arrays (the hull array, the pInd array, and the t array). There’s a universal counter (hullInd) to keep track of how many things have been added to the arrays. It’s mainly a way to watch out for when the hull is ‘closed’: when the left-most layer shows up as the winner after the algorithm has run its course.

Exit. Exit. Please exit.

My first pass at writing this expression was rough. The nature of the looping/finding the vertices on our convex hull– not knowing how many points defined the boundaries (the hull could potentially have a different number of vertices on every frame)– made this expression very easy to go into an infinite loop. And I ran into that problem over and over working through the algorithm.

I’ll be honest– I had to start writing the expression in a text editor because accidentally clicking out of the expression editor in the middle of writing the expression was causing me to infinite loop/error-out/crash.

The end cases– when the end of each winner search loop is reached… and when the hull wraps around to the first-added vertex– they both had to be choreographed carefully so the expression would know when to exit.

if (j == points.length-1){ 
reached = addToHull(cwInd);
cwInd = cInd(cwInd+1);
// reset loop until the winning hull point is the left-most point again
if (!reached){
j=1; 
}

The lines above exist within the winner search loop. In short, they simply wait for the end of the winner search (the current-winner-vs.-contender process). When winner search loop ends, the current winner is added to the hull.

To find the next winner, though… we have to reset the winner search loop back to 1 (we do that using j=1).

In the addToHull function, I’ve written the exit logic for the whole process to be true when the index of the current winner is the index of left-most layer (the first vertex on the hull). Though, the conditional has to allow us to add the left-most layer to the hull at the beginning of the algorithm. That’s what pInd[0] == null does below.

function addToHull(_pInd) {
temp = hullInd;
endReached = false;
if (pInd[0] == null || pInd[0] !== _pInd) {
hull[hullInd] = points[_pInd]; // add winning point to hull
pInd[hullInd] = _pInd; // store index of this point's index 
t[hullInd] = [0, 0]; // add point tangents
hullInd++;
} else {
endReached = true;
}
return endReached;
}

The endReached variable is presumed false. We only switch it to true when the current winner is the left-most layer.

This endReached is vital in stopping the winner search loop from looping forever:

// reset loop until the winning hull point is the left-most point again
if (!reached){
j=1; 
}

Conclusion

This expression was an interesting challenge. Thankfully, I was able to follow a lot of the methodology in the Coding Train walk-through because of the expression language being built on top of JavaScript.

The open-ended nature of the algorithm– not knowing how many vertices make up the convex hull (so not knowing how many loops there would be on any given frame) made the process frustratingly delicate.

There are things I might do differently if I were to start over writing this expression. For instance– there’s likely a way to shorten the points array over time, as layers get added to the hull array. A vertex that gets added to the array (after the left-most point, that is) will never be declared a winner again, so there’s no use in having it part of the ‘current winner’/’contender’ search. It could shorten the processing time a touch– a negligible amount, I’m fairly certain. If the points array is really massive, though… it might save a second of calculation for every frame that the expression needs to run.

** A footnote

I wrote the linked jsx script to help me populate the points list and generate a Shape Layer with the expression applied to a path shape. (It was for a moving infographic with 3D point data that changed over the course of the project. I needed to be able to iterate/update the shapes quickly.)

convex hull auto-populate scripting demo
It’s gonna be that easy.

An update (09-26-2020) – This convex hull expression is a perfect example of an opportunity to save yourself some serious time using After Effects scripting. Check out this post on my AE scripting blog that covers this exact scenario. I feel all After Effects artist should have a basic understanding of how to write scripts, in the same way all Photoshop artists should have a basic understanding of Actions. There are repeatable, predictable workflows that you shouldn’t be wasting your time on… writing a list of 120 variables to simply fill an array with points included.

Adding value

I’ll try to keep this short.

When you include value as a facet of your expressions, you’re referring to the information you’d keyframe in whatever property your expression exists. Take the Position property, for example. If we want the ability to drag our layer around after we’ve added an expression, we’ll use value. (The alternate solution is using property-specific objects. transform.position, for example. value is just easier to remember. It’s a catch-all.)

When we don’t have value (or the property-specific objects) in our expression, the information in those attributes are ignored. Let’s look at a boring example– using an expression in the Position property of a layer. Here’s our expression:

[100,100]

Yeah. That’s it. Not very practical, but it illustrates the point. “What point?” you ask. If the above expression is in the Position property of a layer, that layer will always be at [100,100] in your comp (unless you go and do something crazy like parent it to a layer). No matter how much you try to drag your layer around in the comp window, or drag the Position numbers in the timeline, that layer won’t move. Let’s amend this expression with value.

[100,100]+value

Amazing. Now, the numbers in the Position x and y properties have an effect on where the layer is. If we punch 640 and 480 into the layer’s x and y (respectively), we get a layer at [740, 580]. (….because we added 100 to the x and 100 to the y! Pay attention, for crepe’s sake.)

Go add value, you filthy person.

Sending a Layer in a Circle. Two Ways.

Here’s a problem. A fairly straightforward problem. We have a layer and we want it to orbit some point, but we want it to stay upright (meaning we don’t want the rotation to be affected).

The Simple (and Potentially Messy) Solution

Circle with a Parent

The first solution is pretty simple. Make a null, place the null exactly where you want your layer to orbit around, parent your layer to the null, and rotate that null, baby! And to make sure the layer stays upright, we negate the null’s rotation by simply adding an expression into the orbiting layer’s Rotation property. We pick-whip the null’s Rotation property and multiply it by -1. It’ll look something like this:

thisComp.layer("Null 1").transform.rotation*-1

Easy peasy! And if you want to be able to animate the rotation of your layer, you can add the “value” to your expression. (More on adding value.) Like so:

(thisComp.layer("Null 1").transform.rotation*-1)+value

I should note: adding the parentheses around the first bit isn’t entirely necessary. Order of operations, don’t ya know. Math. Ever heard of it!? (You disgust me.)

So, why is this thing “potentially messy”? I’ll tell you why. I’ll tell you EXACTLY why…. once I’m finished building up the suspense….

I’ve started thinking about the use of expressions in big networks of layers. My hesitation to use the above solution stems from the fact that it takes two layers to work– so, you’re doubling the layer count of any comp made of a bunch of these layers. Also, you couldn’t really use this solution for something like the Center property of a Circle Effect (or similar effect that has a Center property). There’s an expression for that.

The More Math-y Solution

The second solution to this problem is based on the cyclical nature of Math.sin() and Math.cos(). Both return values between -1 and 1, but the wizardry happens in the fact that these waves are offset in such a way– when given 0 as an input– Math.sin() returns a 0, and Math.cos() returns a 1. Illustrated below.

Sin and Cos of Time
I’m just passing the time property into Math.sin() and Math.cos() to visualize the difference between their graphs.

If we pass a rotation value into these two expressions (using an Angle Control), we can mimic the effect of rotating a center null. Let’s add an Angle Control to our layer, and call it driver. We’ll also need to add a Slider Control, and it will be called size. And here’s the expression we’ll put into the Position property of our layer:

s=effect("size")("Slider");
d=degreesToRadians(effect("driver")("Angle"));
h=Math.sin(d);
v=-Math.cos(d);
([h,v]*s)+value

I’ll pick it apart piece by piece. We first set up our variables for the Expression Controls (size and driver). size is going to function as the radius of the circle around which our layer will be moving. Because Math.cos() and Math.sin() use radians to calculate their values, we have to convert the degrees from our Angle Control (driver) to radians. The next part– where we set up h and v– is where the magic happens.

The stuff in variable h is responsible for how the layer is moving horizontally. For h we feed our driver values into Math.sin(). Variable v is only responsible for how the layer moves vertically. For this, we’re feeding the values of driver into the Math.cos() and multiplying that by negative one. (The layer will move in the opposite direction of driver, if we don’t have the negative here.)

Before we multiply our expression by size, we get a layer–as we drag our driver control– that travels in a circle which has a radius of 1. Controlling the circle’s radius is as simple as multiplying by size. When size is 100, the circle has a radius of 100, for example.

Dragging our driver slider

To put it all together, we’re just dumping h and v in a Position array (where h is our x property and v is our y), multiplying by our size…. and then….!?!?! What is this? Adding value? What the fudge-factory, Steve!? The circle we’re traveling around, when we add value, is centered AT THAT LOCATION. So, when we punch 960 and 540 into the x and y Position properties (respectively), we get a circle, centered at [960,540], with a radius of whatever size happens to be. And another fun thing: we can parent this layer to another layer without the fear of it getting all whacky. Also, your layer’s Anchor Point, Scale, and Rotation properties will act just how you’d expect them to. There’s nothing crazy going on. (So, calm yourself. You’re making me nervous.)

Go forth! Make circles. Get dizzy.

EXTRA 3D FUN-ZONE

Because each dimension in our expression is controlled discretely, it’s absolutely possible to dump h and v into a Position array that has the third dimension (after you make your layer a 3D layer, of course). You could feed v into the z property of the array to have the layer orbit in the fashion of something like Saturn’s rings. “…but, Steve, there’s no up or down in space!” you exclaim in a stupid, whine-y voice. Fine. It’s like a hula hoop. Get over yourself. The expression would look like this:

s=effect("size")("Slider");
d=degreesToRadians(effect("driver")("Angle"));
h=Math.sin(d);
v=-Math.cos(d);
([h,0,v]*s)+value

Circle Expression in 3D

You’ll notice we just have to include a zero to keep the place of the dimension that isn’t using Math.sin() or Math.cos(). In the above case, the y-dimension will be 0. And the same rules apply as before. All of your other Transform properties will function like normal. Parenting won’t give you any unexpected results. Remember: if you find that your driver values are making your layer orbit in the wrong direction (as seen in the gif above), you can remove the negative on Math.cos().

Away with you. (My Gist link for this expression: https://gist.github.com/thatsmadden/d31503aa3b4e03a76d3f)

toComp() and toWorld()

weight-slider drag

These two expressions are beyond useful. The way they work finally clicked in my head recently. (I had a good idea of what toComp() did because it’s an expression that a lot of tutorials use. Drawing a line between two 3D nulls with the beam effect, for instance.)

toWorld() does essentially what toComp() does, but with the third dimension. So, knowing that– what is it that they do? Well, I was just about to explain it! Calm down. Holy Ravioli.

These two expressions basically calculate the whereabouts of a layer after it has been parented to another layer. Let’s think about it in terms of nulls….

We have NullA, NullB, NullC. All 3D layers. We’re going to parent NullB to NullA… and then move NullA around. We find that the values in the Position property of NullB don’t change at all. This being the case, if we want NullC to be exactly where NullB is (using expressions), we can’t simply expression-pick-whip to NullB’s Position– because that number doesn’t fully represent where NullB is in space.

toWorld() is built for this problem.  We can use something like thisComp.layer("NullB").toWorld([0,0,0]) in the Position property of NullC to access the point in space where NullB is located– after NullA has been moved around, rotated, and scaled.

I’ve started to use toWorld() in place of transform.position in some of my expression set-ups, to save myself the headache later on– in the event some of my layers need to be parented to other layers.

Hey! Let’s do something useful now!

Here is an expression that makes a layer travel between two other layers, based on the value of a slider (between 0 and 1, more specifically).

THE SETUP

We’ll use NullA, NullB, and NullC. Let’s unparent NullB from NullA (though, it doesn’t need to be unparented). NullC is going to exist between NullA and NullB. Where NullC exists (Closer to A? Closer to B?) will be driven by a Slider Control. We’ll call it weight. We’re going to want to edit the Slider Range in the  “Edit Value…” window, so instead of being set to the default 0 to 100, it will now be 0 to 1. This will just make it easier to control when we’re dragging this value.

Let’s get our keyboard fingers dirty! (Ugh. Gross.) We’re adding this expression to the Position property of NullC. I’ll paste the finished product here, and then I’ll explain it.

a=thisComp.layer("NullA").toWorld([0,0,0]);
b=thisComp.layer("NullB").toWorld([0,0,0]);
wh=thisLayer.effect("weight")("Slider");
aW=a*wh;
bW=b*(1-wh);
aW+bW

There are two parts, essentially. First, we’re setting up variables for NullA, NullB, and our weight Slider. Second, we’re using a bit of math to calculate the positions between NullA and B. To simplify the idea, consider the following: if you wanted to just have NullC exist exactly in the middle of A and B, you’d do something like this:

a=thisComp.layer("NullA").toWorld([0,0,0]);
b=thisComp.layer("NullB").toWorld([0,0,0]);
(a+b)/2

Which would be the same thing as this:

a=thisComp.layer("NullA").toWorld([0,0,0]);
b=thisComp.layer("NullB").toWorld([0,0,0]);
(a*.5)+(b*.5)

To vary C’s proximity to either A or B, we’re just affecting those .5’s– making sure they always add up to 1. If we want the C to be closer to A, we’ll just give that side more “weight”– .8, let’s say… and that would leave .2 on the B side. In our expression, this bit is handling that math:

aW=a*wh;
bW=b*(1-wh);

The exciting part is that the values BEYOND 0 and 1 on the slider will give you positions for NullC that are still in-line with NullA and NullB.

That’s it! Post questions if you have them!

I’ve started a GitHub account— so I’ll be publishing the expressions I post here to “Gists”, in case there’s ever a problem copy/pasting my code from WordPress.

Let’s get started!

My plan for this blog is to post a bunch of my After Effects expression work. Some of it will be small bits of useful expressions that end up in a lot of my creations. Some stuff will be bigger, cross-comp set-ups. I’ll try to make some of my actual After Effects projects available for download– maybe in the cases where a simple copy/paste of the code won’t suffice.

In doing this blog, my hope is that some of these posts will add some clarity to how expressions can be used, beyond the After Effects documentation.