Featured Image

Layer parent slider in After Effects

6 min read

TLDR

In After Effects you can parent a layer through expressions and keyframe when it gets parented. Just remember that the desired layer itself must stay unparented in the "Parent and Link" tab.

I've made a preset for easy daily use with 2-position (AB) and 3-position slider (AoB) variations.

The Problem

The layer parenting method in After Effects is extremely useful, but it cannot be keyframed. As a result you are often left dazed and confused with shady quick-fixes.

For example, you might want to animate a character lifting up or throwing an object. Usually you've got 2 options:

  • Parent the layer to a hand, then cut and unparent it on a specific frame and if necessary cut and parent it back again later on
  • Duplicate the keyframes from the hand layer or animate new keyframes on top of existing motion paths to match the animation

Both of these options are far from ideal. You either have to use multiple layers to animate a single object or double the amount of keyframes to animate the same motion path. Or if you're lazy - both. And to top it all off, after a nice weekend the client feedback comes in saying they want the animation to happen 2 seconds earlier, much faster and with a different motion path. You pretty much have to start from scratch because it is faster to redo the whole thing than edit the original keyframes.

The Solution

Ok, maybe I exaggerated a bit (5% max), but having the ability to keyframe when a layer is parented would provide a more manageable, non-destructive workflow. In order to mimic the original parent function we need to link up 3 transform properties: position, rotation and scale. This seems like an easy task until you encounter a chain of parented layers which are pretty common in character rigs. Fortunately there are workarounds for each property.

For reference here is a basic example with 3 layers. The square and circle are on a simple loopOut('offset') and triangle is our desired layer.

Position

For position we have most of the work done for us with .toWorld() function which translates a layer's xyz values to global (as if they were unparented) values. All we need to do is jump between these values with a slider control which is where the linear() function comes in. Here is the expression that gets applied to the desired layer's position.

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;
sliderMIN = [-100];
sliderMAX = [100];

globalA = layerA.toWorld(layerA.anchorPoint);
globalB = layerB.toWorld(layerB.anchorPoint);

linear(slider,sliderMIN,sliderMAX,globalA,globalB);

Now before some of you madskillzbruh-pro-coders start throwing digital chairs at me saying that this could have been written even shorter, I believe this is a "very easy to read, use and adapt" version. You've got the pickwhippable (is that a word?) variables at the top and the rest of the code can essentially be ignored. In fact pickwhip_layer_A is written specifically with underscores instead of hyphens so you can double click right on the text and it selects exactly the part you need to pickwhip (wow! nerd alert!). This saves you milliseconds! You can use that extra time to crash After Effects!

Rotation

Unfortunately .toWorld() does not work for rotation or scale (Why, Adobe, WHY?!?). So we have to figure out a different solution. For our desired layer we need to sum up all of the parent chain rotations to get the same result as a child layer. Luckily for us, there are layer.hasParent and layer.parent properties which we can use. With layer.hasParent we check if a layer has a parent element and with layer.parent we target the parent of our layer. We can put this in a loop where we target the parent layer, get it's rotation, add it to a sum and target the next parent layer and do the same thing until we reach the end of the chain. This is done for both layers. Then the same slider and linear() function is used as before to jump between the values.

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;

rotA = layerA.rotation;
rotB = layerB.rotation;
sliderMIN = [-100];
sliderMAX = [100];

while ( layerA.hasParent ) {
	parentA = layerA.parent.rotation;
	rotA = rotA+parentA;
	layerA = layerA.parent;
}

while ( layerB.hasParent ) {
	parentB = layerB.parent.rotation;
	rotB = rotB+parentB;
	layerB = layerB.parent;
}

linear(slider,sliderMIN,sliderMAX,rotA,rotB);

Scale

For scale we use a similar principle as for rotation except instead of adding the values, we multiply them and divide by 100. Also, we have to specify the scale xy values individually otherwise we get an error as soon as anything is in a parent chain.

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;

sliderMIN = [-100];
sliderMAX = [100];
scaleA = layerA.transform.scale;
scaleB = layerB.transform.scale;

while ( layerA.hasParent ) {
	parentA = layerA.parent.transform.scale;
	scaleA = [scaleA[0]*parentA[0],scaleA[1]*parentA[1]]/100;
	layerA = layerA.parent;
}

while ( layerB.hasParent ) {
	parentB = layerB.parent.transform.scale;
	scaleB = [scaleB[0]*parentB[0],scaleB[1]*parentB[1]]/100;
	layerB = layerB.parent;
}

linear(slider,sliderMIN,sliderMAX,scaleA,scaleB);

And there you have it, folks! It works! We have achieved world peace, cured all diseases, After Effects is no longer single-threaded and Bob's your uncle (is he though?).

Customization

The default version should work fine for most cases, but sometimes you might need 2 parent layers and the original values of the layer (e.g. a character throws a ball to another character). We can achieve this by adding the 3rd option in the middle of the slider and a simple if statement at the end.

Position

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;
sliderMIN = [-100];
sliderMID = [0];
sliderMAX = [100];

globalA = layerA.toWorld(layerA.anchorPoint);
globalB = layerB.toWorld(layerB.anchorPoint);

if (slider < sliderMID) {
	linear(slider,sliderMIN,sliderMID,globalA,value);
} else {
	linear(slider,sliderMID,sliderMAX,value,globalB);
};

Scale

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;

sliderMIN = [-100];
sliderMID = [0];
sliderMAX = [100];
scaleA = layerA.transform.scale;
scaleB = layerB.transform.scale;

while ( layerA.hasParent ) {
	parentA = layerA.parent.transform.scale;
	scaleA = [scaleA[0]*parentA[0],scaleA[1]*parentA[1]]/100;
	layerA = layerA.parent;
};

while ( layerB.hasParent ) {
	parentB = layerB.parent.transform.scale;
	scaleB = [scaleB[0]*parentB[0],scaleB[1]*parentB[1]]/100;
	layerB = layerB.parent;
};

if (slider < sliderMID) {
	linear(slider,sliderMIN,sliderMID,scaleA,value);
} else {
	linear(slider,sliderMID,sliderMAX,value,scaleB);
};

Rotation

layerA = pickwhip_layer_A;
layerB = pickwhip_layer_B;
slider = pickwhip_slider_control;

rotA = layerA.rotation;
rotB = layerB.rotation;
sliderMIN = [-100];
sliderMID = [0];
sliderMAX = [100];

while ( layerA.hasParent ) {
	parentA = layerA.parent.rotation;
	rotA = rotA+parentA;
	layerA = layerA.parent;
};

while ( layerB.hasParent ) {
	parentB = layerB.parent.rotation;
	rotB = rotB+parentB;
	layerB = layerB.parent;
};

if (slider < sliderMID) {
	linear(slider,sliderMIN,sliderMID,rotA,value);
} else {
	linear(slider,sliderMID,sliderMAX,value,rotB);
};

Caveats

Bonus tip

Thanks for reading and good luck!

© Zigfrids Niklavics 2023

View