All our expressions are written for the newer JavaScript Expressions Engine

Blog

What happens during expression pre-processing

Photo of Tim Haywood
Tim Haywood

|

September 25, 2020

If you've read our articles on objects or writing expressions in external .jsx files, you'll know that After Effects pre-processes all expressions before running them.

You can learn the reason for this from the Adobe documentation:

"When using the JavaScript engine, expressions are pre-processed before evaluation in order to make some of the Legacy ExtendScript expression syntax readable by the new engine."


From this article: Syntax differences between the JavaScript and Legacy ExtendScript expression engines

In other words, the ExtendScript engine allowed syntax that isn't compatible with the newer JavaScript engine, and rather than have users make further adjustments to their code, After Effects will do parts of the conversion for you.

Watch our video explaining pre-processing here:

Play Youtube video

The pre-processing steps

There are two main tasks After Effects performs during the pre-processing stage:

  • Replacing math operations on array values with vector functions
  • Extending the scope chain for native attributes and methods

This pre-processing isn't done on code in external .jsx files, so you will need to do both of these manually. This lack of pre-processing results in a performance increase for long expressions.

Replacing math functions

Doing maths on arrays has always been a part of expressions, so it's easy to assume that it's part of the JavaScript standard. For example:

js
// Adding and Subtracting
[200, 200] + 50;
wiggle(2, 2) - value;
value + [960, 540];
value + otherLayer.transform.position;
// Multiplying and Dividing
value * 2;
[thisComp.width, thisComp.height] / 2;

All work as expected within After Effects. This is known as overloading math operators.

The math operators (+ - * /) in JavaScript work on numbers, not arrays. Using them on arrays (overloading) will result in unexpected results, for example:

js
[200, 200] + 50;
// Array is coerced to a string "200, 20050";
js
[200, 200] * 2;
js
// Not a Number
NaN;

To avoid this, After Effects replaces all instances of math operations with their vector math function counterparts:

  • add()
  • sub()
  • mul()
  • div()

For example:

js
// Before
[thisComp.width, thisComp.height] / 2;
4 * 8;
wiggle(2, 2) - value;
// After processing
div([thisComp.width, thisComp.height], 2);
mul(4, 8);
sub(wiggle(2, 2), value);

Technically speaking, it replaces them with a wrapper of these functions that uses the standard math operators when possible.

Native attributes and methods

The other major step of pre-processing is extending the scope your expression, looking up native attributes and methods such as toComp(), opacity and wiggle() on the thisLayer and thisProperty objects.

It does this by extending the scope chain.

In order to understand the need to extend the scope chain, it's helpful to have an understanding of variable scope and the prototype chain in JavaScript.

Within an expression, you can use these attributes and methods as if they were defined in the global scope, without prefixing them with a specific Layer or Property.

For the sake of clarity, After Effects Properties (such as position) will be written with the capital "P" Property, while properties of a JavaScript object will be written with the lowercase "p" property.

If they aren't prefixed with a specific Layer or Property, either thisLayer or thisProperty is used. For example, the following examples are equivalent:

js
wiggle(2, 2) === thisProperty.wiggle(2, 2);
toComp([960, 540]) === thisLayer.toComp([960, 540]);

Without pre-processing, you would get an error as these values (such as numKeys) aren't available in the global scope.

js
// Without pre-processing
numKeys;
js
ReferenceError: numKeys is not defined

To get around this need for prefixing (and make the JavaScript engine more compatible with legacy ExtendScript code), After Effects creates a proxy to intercept the property calls.

Part of this proxy checks if a value is undefined, and if so, looks for that value as a property on thisLayer, and if that fails, on thisProperty.

The code for this is:

js
if (v === undefined) {
target = oTarget.__thisLayer;
v = target[sKey];
if (v === undefined) {
target = oTarget.__thisProperty;
v = target[sKey];
}
if (v instanceof Function) {
v = v.bind(target);
}
}

It then extends the scope of your expression using a with statement and the Proxy object shown earlier.

js
with (__makeProxy(this, true)) {
// expression code
}

The with statement adds the given object to the head of the scope chain during the evaluation of its statement body.

If an unqualified name used in the body matches a property in the scope chain, then the name is bound to the property and the object containing the property.


MDN Docs

This with statement adds the Proxy to the scope chain of your expression. This process allows you to use the properties of Layers and Properties without prefixing them with thisLayer or thisProperty.

The pre-processing function

You can run the function __preprocess on a string of code to see the processed result:

js
// The pre-processing function
globalThis.__preprocess(codeString);

Using this function, you can compare an expression before and after pre-processing. For example:

Before processing:

js
// before
if (time < -1) {
[0, 0];
} else {
value + wiggle(2, 2);
}

The processed result:

js
// after
with (__makeProxy(this, true)) {
if (time < __mul(-1, 1)) {
[0, 0];
} else {
__add(value, wiggle(2, 2));
}
}

You can see in the example above that the pre-processing has replaced the math operators with their vector function equivalents, as well as used __makeProxy to and with to extend the scope chain, so you don't need to prefix native attributes and methods.

The math operators are replaced with their function equivalents using the __expr_rewrite function. This function uses recast to generate and transform an AST (Abstract Syntax Tree) of your expression.

Other behind the scenes functions

You can view the code for some of the other relevant functions below.

Wrapping up

Understanding what happens during the pre-processing of expressions gives you a deeper understanding of how expressions work, as well as the differences between writing code in external files and within After Effects.

Blog

For enquiries contact us at hey@motiondeveloper.com