Quick start
Seven guided builds, from a gentle drift to a project script driving two comps at once, each starting from an empty project.
Six small scripts, each complete with the scene it plays in, then one project script driving two comps at once. Nothing here assumes you have written an expression before - each walkthrough names every layer and control it uses, and you can rebuild any of them from an empty project in a couple of minutes.
Before you start: names on the surface, ids underneath
In the editor you type and read NAMES: completions inside layer("..."), control("...") and prop("...") offer the scene's layers and controls by name, the @ pick whip drops a reference in for you, and applied scripts display those names in the code. What the document STORES is always the stable id underneath - so renaming a layer never breaks a script, and these pages show each example the way the editor displays it. Type a name by hand and Apply quietly fixes it to the id (a note in the gutter tells you what it resolved); an ambiguous name gets a pick-one quick fix instead.
Running any of these
- Build the little scene the walkthrough's Scene block describes (sizes are suggestions - only names and kinds matter).
- Open Scripts in the editor's left rail and add a script.
- Paste the code, then press Apply. The status chip turns green (ok) and the canvas takes the new motion at once.
- Scrub the timeline. Everything you see is deterministic - the render farm computes exactly the frames you scrub here.
While you type, the IDE evaluates your draft live in a scratch context: pure computation shows its value in the status strip immediately, and lines that read the scene (like layer("...")) come alive when you Apply - the script's status chip and the moving canvas are the truth.
1. A gentle drift
The scene. One orange square resting on a dark 640 by 360 stage.
- The comp Drift: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Square: a solid layer, at [320, 180], 60 by 60, filled f2a833.
The script. Add a project script named Drift and paste:
export const outputs = ["layer_1:transform.position"];
export function frame() {
const x = value[0] + wiggle(0.8, 24);
const y = value[1] + wiggle(0.8, 24, time + 100);
return [x, y];
}
Line by line:
- Line 1: Declare what this script is allowed to drive: the Square layer's position. The Scripts panel and the pick whip write these ids for you.
- Line 3: frame() runs once for every frame the comp shows. Whatever it returns becomes the value of the script's one declared output for that frame.
- Line 4: value is the position the Square would have without the script - here its resting [320, 180]. wiggle(0.8, 24) adds a smooth wander that changes about 0.8 times per second and strays up to 24 pixels.
- Line 5: The second axis offsets the wiggle's clock by 100 seconds. Without the offset both axes would get the SAME number and the Square would only slide along a diagonal.
- Line 6: Return the new position as an [x, y] array - positions are arrays in scripts, exactly as the panel shows them as X and Y.
What you see. The square drifts gently around its resting spot, never straying more than about 24 pixels, and the motion loops nowhere - it just keeps wandering.
The full reference for the pieces this uses lives on the global.wiggle page.
2. Link one property to another
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 Property link 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, 100]);
}
Line by line:
- Line 4: value on a Property handle is the resolved value at the current frame: keyframes are sampled, and if another script drives the property, its output is what you read. The Leader is keyframed to march across the stage, and this read follows every step.
- Line 5: One vector add hangs the Follower exactly 100 pixels below the Leader - a property link in two lines, no keyframes copied.
What you see. The follower marches in perfect lockstep 100 pixels below the keyframed leader - a live link, not a copy: re-time the leader's keys and the follower obeys.
The full reference for the pieces this uses lives on the Property.value page.
3. A countdown
The scene. One text layer near the centre of a dark 640 by 360 stage, reading 5, on a five second comp.
- The comp Counter: 640 by 360 at 25 fps, 125 frames (5 seconds).
- Counter: a text layer, at [465, 239], reading "5".
The script. Add a project script named Countdown and paste:
export const outputs = ["layer_1:content.text"];
export function frame({ frame, fps }) {
const seconds_gone = Math.floor(frame / fps);
return String(5 - Math.min(seconds_gone, 4));
}
Line by line:
- Line 1: One output: the Counter layer's TEXT. Since the drive set widened to every animatable property, a script may write the source text of a text layer - a per-frame HOLD, exactly like a text keyframe.
- Line 3: The frame count is taken from the function's argument. Inside frame() the bare word frame would name the function itself, so destructuring { frame, fps } is the way to read the clock.
- Line 4: Whole seconds elapsed: frames divided by frames-per-second, rounded down.
- Line 5: Second 0 shows 5, second 1 shows 4, and so on; Math.min holds the last digit on screen at the end. A script with exactly one output may simply RETURN the value - strings write text holds, so the layer re-shapes with the new digit every second.
What you see. The one text layer counts 5, 4, 3, 2, 1 - one digit per second - driven straight into its source text by the script.
The full reference for the pieces this uses lives on the global.frame page.
4. A colour cycle
The scene. One 200 by 200 swatch near the centre of a dark 640 by 360 stage, painted by a single coral fill in its fill stack - the paint model shapes and frames use - ready for a script to drive that fill's colour.
- The comp Swatch: 640 by 360 at 25 fps, 150 frames (6 seconds).
- Swatch: a solid layer, at [320, 180], 200 by 200.
- The stack fill Base (
fills.fill_1in scripts): a colour fill, ff5d73.
- The stack fill Base (
The script. Add a project script named Colour cycle and paste:
export const outputs = ["layer_1:fills.fill_1.colour"];
const palette = [
[1, 0.365, 0.451, 1],
[0.949, 0.659, 0.2, 1],
[0.18, 0.769, 0.714, 1],
];
export function frame() {
const phase = (time / 2) % 3;
const from = palette[Math.floor(phase)];
const to = palette[(Math.floor(phase) + 1) % 3];
return linear(phase - Math.floor(phase), 0, 1, from, to);
}
Line by line:
- Line 1: One output: the colour of the swatch's STACK FILL - the fill named Base in the scene, addressed by the id the document minted for it (fills.fill_1). Shapes and frames paint through a stack of fills like this one, and the Scripts panel and the @ pick whip write the id for you. Colours are writable like any other animatable property - the script hands back [r, g, b, a] in 0..1 (a "#rrggbb" string works too).
- Lines 3-7: The three colours of the cycle as [r, g, b, a] arrays: coral, gold and teal.
- Line 10: phase crawls 0, 1, 2 and wraps - one whole colour every two seconds.
- Lines 11-12: The colour the phase is leaving and the one it is heading for; the modulo wraps teal back round to coral so the loop closes seamlessly.
- Line 13: linear() works component by component on arrays: the fraction of the way through the current step blends every channel from one colour to the next, and the single-output return drives the fill directly.
What you see. The one swatch melts through coral, gold and teal on a six second loop, each colour cross-fading smoothly into the next - its stack fill's colour driven by the script.
The full reference for the pieces this uses lives on the global.linear page.
5. Follow a circular path
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 Orbit and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const centre = control("Centre").value;
const start = control("Start angle").value;
const speed = control("Speed").value;
const angle = degreesToRadians(start + time * speed * 3.6);
return [centre[0] + Math.cos(angle) * 120, centre[1] - Math.sin(angle) * 120];
}
Line by line:
- Lines 4-6: Three controls from the Rig steer the whole path: where the circle sits, where on it the planet starts, and how fast it travels. In the editor these strings show the control names; the stored script keeps their ids.
- Line 7: The angle grows with time - at Speed 100 that is 360 degrees per second. Math.cos and Math.sin speak radians, so degreesToRadians() converts the comfortable degree count first.
- Line 8: Cosine gives the x of a point on a circle, sine the y; a radius of 120 pixels draws the path. Subtracting the sine sends the planet anticlockwise, upwards first.
What you see. The planet sweeps a perfect 120 pixel circle around the sun, its pace set by the Speed slider - drag the Centre point control and the whole orbit relocates.
The full reference for the pieces this uses lives on the global.degreesToRadians page.
6. Opacity on and off
The scene. A gold sun near the centre with a small blue planet floating visibly above it, and a hidden Rig null carrying the expression controls that steer the scene - with the Show planet checkbox KEYED across the clip (hold keyframes: on, off just past three seconds, on again at five and a half).
- 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 Show planet and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
return control("Show planet").value ? 100 : 0;
}
Line by line:
- Line 4: The Rig's checkbox control answers true or false, and the script turns that into all-or-nothing opacity. This is the classic controller move: expose one honest switch, let the script do the showing and hiding. Bind the checkbox to a Dynamic Field and templates can flip the planet from outside the editor.
What you see. The planet obeys the Rig's Show planet checkbox: ticked, it is there; unticked, it vanishes - one switch controlling the scene. The Rig keys the checkbox across the clip (off just past three seconds, back on at five and a half), so the planet blinks out and returns.
The full reference for the pieces this uses lives on the Layer.opacity page.
7. One project script, two comps
The walkthrough that shows the model whole - a scene built FROM ZERO, a control-free script driving layers in two different comps, applied through the Scripts panel:
Build the document first:
- Create a project; the default comp becomes Main (640 by 360 works well).
- Add a second comp named Card (320 by 180).
- In Main, draw a wide solid near the bottom and name it Banner.
- In Card, draw a small solid at the centre and name it Badge.
- Open Scripts in the left rail, add a script, and name it House lights.
The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.
- The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
- The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
- Banner in Main: a solid layer, at [320, 300], 400 by 40, filled 2ec4b6.
- Badge in Card: a solid layer, at [160, 90], 60 by 60, filled ff5d73.
The script. Add a project script named House lights and paste:
export const outputs = [
"layer_1:transform.opacity",
"layer_2:transform.opacity",
];
export function frame() {
set("layer_1:transform.opacity", linear(time, 0, 2, 100, 40));
set("layer_2:transform.opacity", linear(time, 1, 3, 100, 80));
}
Line by line:
- Lines 1-4: The Banner lives in the Main comp and the Badge lives in the Card comp - one script may declare outputs across any number of comps.
- Lines 7-8: set(target, value) writes one declared output. With more than one output there is no single return value, so set() is the writing tool. Here each write carries its own linear(time, ...) ramp - the banner fades to 40 percent across the first two seconds, the badge to 80 on a later curve - one script animating layers in two comps at once. Targets outside the outputs list refuse with undeclared_output, and a value of the wrong shape refuses with type_mismatch - typed errors, never silent surprises.
What you see. The banner in Main fades down to 40 percent across the first two seconds while the badge over in Card eases to 80 percent on its own later ramp - both driven each frame by the same script.
Try the @ pick whip while you are here: with the caret inside the outputs array, type @ and drag onto any property row - the reference drops in wearing the layer's name, stored as its id.
Where next
- Overview - what scripts are and how they run.
- References and ids - the name-over-id model in full.
- Project scripts - outputs, set(), scheduling and cycles.
- State and time - state(), memo() and the fold.
- Global - everything a script can see.