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:
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 Dividingvalue * 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 NumberNaN;
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 processingdiv([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-processingnumKeys;
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.
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 functionglobalThis.__preprocess(codeString);
Using this function, you can compare an expression before and after pre-processing. For example:
Before processing:
js
// beforeif (time < -1) {[0, 0];} else {value + wiggle(2, 2);}
The processed result:
js
// afterwith (__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