Vector maths
Component by component arithmetic for the [x, y] arrays behind positions and scales, including scalar broadcasting.
Positions and scales travel as [x, y] arrays, and these helpers do arithmetic on them component by component - lenient like After Effects' vector maths, so a scalar broadcasts where that reads naturally.
add
add(a: T, b: T): T
Component-wise a + b.
Determinism: pure.
After Effects equivalent: add.
Worked example: Hover above the leader
The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.
- The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Leader: a solid layer, at [80, 140], 40 by 40, filled 2ec4b6.
- Keyframe transform.position at frame 0: [80, 140] (linear).
- Keyframe transform.position at frame 96: [560, 140] (linear).
- Follower: a solid layer, at [80, 240], 40 by 40, filled f2a833.
The script. Add a project script named Hover and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const leader_pos = prop("Leader:transform.position").value;
return add(leader_pos, [0, -60]);
}
Line by line:
- Line 4: The leader's live [x, y] at this frame.
- Line 5: add() sums vectors component by component - the follower rides exactly 60 pixels above the leader wherever it goes. Positions are plain arrays, so the vector helpers work on them directly.
What you see. The follower floats a fixed 60 pixels above the marching leader for the whole journey, like a balloon on a short string.
sub
sub(a: T, b: T): T
Component-wise a - b.
Determinism: pure.
After Effects equivalent: sub.
Worked example: Trail to the left
The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.
- The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Leader: a solid layer, at [80, 140], 40 by 40, filled 2ec4b6.
- Keyframe transform.position at frame 0: [80, 140] (linear).
- Keyframe transform.position at frame 96: [560, 140] (linear).
- Follower: a solid layer, at [80, 240], 40 by 40, filled f2a833.
The script. Add a project script named Trail and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const leader_pos = prop("Leader:transform.position").value;
return sub(leader_pos, [40, 0]);
}
Line by line:
- Line 5: sub() subtracts component by component: 40 pixels off the x keeps the follower one square-width behind the leader, at the leader's own height.
What you see. The follower rides one square-width behind the leader for the whole march, glued to its left flank.
mul
mul(a: T, b: number): T
Scale (a scalar on either side broadcasts).
Determinism: pure.
After Effects equivalent: mul.
Worked example: A quarter larger
The scene. One pale 160 by 100 card near the centre of the stage, for anchor, scale and skew reads.
- The comp Tilt: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Card: a solid layer, at [320, 180], 160 by 100, filled e8e6e3.
The script. Add a project script named Enlarge and paste:
export const outputs = ["layer_1:transform.scale"];
export function frame() {
return mul(value, 1.25);
}
Line by line:
- Line 4: mul() scales a vector by a number - the base [100, 100] becomes [125, 125]. Because it starts from value, scaling the card by hand in the panel multiplies on top.
What you see. The card renders a quarter larger than its panel scale - resize it by hand and the script keeps multiplying by 1.25.
div
div(a: T, b: number): T
Component-wise a / b (0 divisor yields 0).
Determinism: pure.
After Effects equivalent: div.
Worked example: Half strength
The scene. Three 200 by 200 swatches stacked on the same spot - coral at the back, gold in the middle, teal on top - ready for a script to cross-fade between them.
- The comp Palette: 640 by 360 at 25 fps, 150 frames (6 seconds).
- Coral: a solid layer, at [320, 180], 200 by 200, filled ff5d73.
- Gold: a solid layer, at [320, 180], 200 by 200, filled f2a833.
- Teal: a solid layer, at [320, 180], 200 by 200, filled 2ec4b6.
The script. Add a project script named Half teal and paste:
export const outputs = ["layer_3:transform.opacity"];
export function frame() {
return div(value, 2);
}
Line by line:
- Line 4: div() divides by a number (a zero divisor yields 0 rather than blowing up). The topmost teal swatch drops to half its panel opacity, letting the gold underneath glow through.
What you see. The teal square turns half transparent, and the gold swatch beneath shows through it as a warm blend.
dot
dot(a: T, b: T): number
Dot product.
Determinism: pure.
After Effects equivalent: dot.
Worked example: How far along an axis
The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.
- The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
- Sun: a solid layer, at [320, 180], 80 by 80, filled f2a833.
- Planet: a solid layer, at [320, 60], 30 by 30, filled 4d9de0.
- Rig: a null (an invisible controller host), at [320, 180].
- An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
- An expression control: Show planet, a checkbox, set to true.
- An expression control: Start angle, a angle, set to 90.
- An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
- An expression control: Centre, a point, set to [320, 180].
- An expression control: Target, a layer, set to layer_2.
The script. Add a project script named Vertical gap and paste:
export const outputs = ["layer_1:transform.rotation"];
export function frame() {
const planet = prop("Planet:transform.position").value;
const sun = prop("Sun:transform.position").value;
return dot([0, 1], sub(planet, sun));
}
Line by line:
- Lines 4-5: Both positions as vectors.
- Line 6: dot() measures how much one vector points along another. Against the unit y axis [0, 1] it extracts exactly the vertical part of the gap: the planet floats 120 pixels ABOVE the sun, so the answer is -120 and the sun rolls a quarter turn twice over, anticlockwise.
What you see. The sun square rests rotated -120 degrees - the exact vertical distance in pixels between planet and sun, read off with a dot product.
length
length(a: T, b?: T): number
Vector magnitude, or the distance between two points.
Determinism: pure.
After Effects equivalent: length.
Worked example: Fade with distance
The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.
- The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Leader: a solid layer, at [80, 140], 40 by 40, filled 2ec4b6.
- Keyframe transform.position at frame 0: [80, 140] (linear).
- Keyframe transform.position at frame 96: [560, 140] (linear).
- Follower: a solid layer, at [80, 240], 40 by 40, filled f2a833.
The script. Add a project script named Proximity and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
const leader_pos = prop("Leader:transform.position").value;
const here = prop("Follower:transform.position").value;
return 100 - clamp(length(leader_pos, here) / 5, 0, 100);
}
Line by line:
- Lines 4-5: The two positions at this frame - the leader marches, the follower rests.
- Line 6: length() with two arguments is the straight-line distance between two points. Divided by five and subtracted from 100, the follower is bright when the leader is near and fades as it walks away. Mid-march the distance is exactly 260 pixels, leaving 48 percent opacity.
What you see. The resting follower glows brighter as the marching leader approaches and dims as it passes on - a proximity light.
normalize
normalize(a: T): T
The unit vector (a zero vector stays zero).
Determinism: pure.
After Effects equivalent: normalize.
Worked example: Step towards the leader
The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.
- The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Leader: a solid layer, at [80, 140], 40 by 40, filled 2ec4b6.
- Keyframe transform.position at frame 0: [80, 140] (linear).
- Keyframe transform.position at frame 96: [560, 140] (linear).
- Follower: a solid layer, at [80, 240], 40 by 40, filled f2a833.
The script. Add a project script named Step towards and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const leader_pos = prop("Leader:transform.position").value;
const direction = normalize(sub(leader_pos, value));
return add(value, mul(direction, 80));
}
Line by line:
- Line 5: normalize() shrinks a vector to length one while keeping its direction (a zero vector stays zero) - the pure "which way" with the "how far" stripped out.
- Line 6: Eighty pixels along that unit direction: the follower always leans a fixed 80 pixels towards the leader, whichever way the leader happens to be.
What you see. The follower leans a constant 80 pixels towards the marching leader, swivelling its offset direction as the leader passes by.