Layer
The layer handle's members: identity, timing, the transform properties and the lookups that reach paths and controls.
A layer handle. The transform members are Property objects (the design's section 3.5) - read .value (WIRE units; points are [x, y]) or pass one to set().
id
Type: string (field).
The stable layer id.
Worked example: Build a property id
The scene. One pale card near the centre with its rotation keyframed from 0 to 90 degrees across the comp, for scripts that read and reshape keyframed motion.
- The comp Keyed: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Card: a solid layer, at [320, 180], 120 by 80, filled e8e6e3.
- Keyframe transform.rotation at frame 0: 0 (linear).
- Keyframe transform.rotation at frame 96: 90 (linear).
The script. Add a project script named Counter-fade and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const card_id = layer("Card").id;
return 100 - prop(card_id + ":transform.rotation").value;
}
Line by line:
- Line 4: Every layer carries a stable id that never changes, however often it is renamed - id is how scripts and property ids stay rename-proof.
- Line 5: Gluing the id to a property key makes a property id by hand: the Card's keyframed rotation (0 to 90 degrees across the comp) reads back and inverts into opacity, so the card fades exactly as fast as it turns.
What you see. As the card's keyframed rotation sweeps from 0 to 90 degrees, its opacity fades in lockstep from 100 down to 10 - the two motions locked together, meeting at 55 half-way.
name
Type: string (field).
The display name.
Worked example: Read the display name
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 Name weight and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
return thisLayer.name.length * 10;
}
Line by line:
- Line 4: name is the label shown in the Layers panel - here "Follower", eight characters, so the layer rests at 80 percent. Names are for reading and display; REFERENCES always travel by id, so renaming this layer changes the result of this read but never breaks a reference.
What you see. The follower square rests at 80 percent opacity - ten percent per letter of its own name. Rename the layer and the opacity follows the new length.
kind
Type: string (field).
The layer kind.
Worked example: Branch on a layer's kind
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 Rig check and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
const rig = layer("Rig");
return rig.kind === "null" ? 60 : 100;
}
Line by line:
- Lines 4-5: kind names what a layer is - solid, text, path, null, precomp, component and so on. The Rig is a null (an invisible controller host), so the check passes and the Planet renders at 60. Kind checks let one script adapt to whatever scene it lands in.
What you see. The planet dims to 60 percent - the script's confirmation that its controller host really is a null layer.
index
Type: number (field).
The 0-based document-order index (0 = bottom).
Worked example: Stagger by stacking order
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 Stack fade and paste:
export const outputs = ["layer_3:transform.opacity"];
export function frame() {
return thisLayer.index * 30 + 10;
}
Line by line:
- Line 4: index is the layer's 0-based place in the stack, counting FROM THE BOTTOM - the back layer is 0. Teal sits on top of three, so its index is 2 and it renders at 70 percent, letting the gold beneath tint it. Duplicate a script across layers and index gives each copy its own number - the classic stagger trick.
What you see. The topmost teal swatch renders at 70 percent - its stacking index turned into opacity - with the gold showing through faintly from beneath.
inFrame
Type: number (field).
The layer's authored in point (frames, READ-ONLY - 1.40.0). Writes are self-referential against the clock evaluating the script; timing edits ride set_layer_timing.
Worked example: Count from the layer's own start
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 Start-relative ramp and paste:
export const outputs = ["layer_3:transform.opacity"];
export function frame({ frame }) {
const since_start = frame - thisLayer.inFrame;
return Math.min(100, since_start * 5);
}
Line by line:
- Lines 4-5: inFrame is the layer's authored in point in frames - a READ-ONLY field (writes are self-referential against the clock evaluating the script; timing edits ride the layer's own trim). Subtracting it from the comp frame gives a clock that starts at zero when the layer does, so a fade-in written this way survives the layer being trimmed later. Teal starts at frame 0, so at frame 10 it sits at 50 percent.
What you see. The teal swatch fades in over the first twenty frames - half opaque at frame 10 - counting from its own in point rather than the comp's.
outFrame
Type: number (field).
The layer's authored out point (frames, READ-ONLY - 1.40.0, exclusive like the wire).
Worked example: Frames left before the layer ends
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 Countdown turn and paste:
export const outputs = ["layer_3:transform.rotation"];
export function frame({ frame }) {
return thisLayer.outFrame - frame;
}
Line by line:
- Line 4: outFrame is the authored out point - EXCLUSIVE, exactly like the wire (a layer over frames 0..149 reads 150), and read-only like inFrame. The difference to the current frame is how many frames the layer has left, so the swatch's angle winds down to zero as it ends: 140 degrees at frame 10 on a 150-frame layer.
What you see. The teal swatch starts turned 150 degrees and unwinds one degree per frame, settling flat exactly as its out point arrives.
stretch
Type: number (field).
The layer's time-stretch factor (1 = none, READ-ONLY - 1.40.0).
Worked example: Scale a motion by the time stretch
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 Stretch-aware turn and paste:
export const outputs = ["layer_3:transform.rotation"];
export function frame() {
return 30 * thisLayer.stretch;
}
Line by line:
- Line 4: stretch is the layer's time-stretch factor - 1 means no stretch, 2 plays the layer at half speed over twice the frames. It is read-only (a script cannot re-time the clock it is being evaluated against), but reading it lets a motion stay proportionate when an editor later stretches the layer. This swatch is unstretched, so the turn is exactly 30 degrees.
What you see. The teal swatch holds a 30-degree turn - the factor is 1 on an unstretched layer; stretching the layer to 2 would double it.
position
Type: Property (field).
The transform.position Property (a live [x, y] value; a set() target).
Worked example: One layer reads another's position
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 Height dial and paste:
export const outputs = ["layer_1:transform.rotation"];
export function frame() {
const planet = layer("Planet");
return planet.position.value[1];
}
Line by line:
- Lines 4-5: position is a Property handle, not a bare number - .value reads the live [x, y] at the current frame (keyframes and other scripts' outputs included), and the handle itself can be handed to set(). The Planet rests at y 60, so the Sun turns 60 degrees.
What you see. The sun square sits rotated 60 degrees - the planet's height read straight into an angle. Drag the planet down and the sun turns further.
scale
Type: Property (field).
The transform.scale Property (a live [x, y] percent value; a set() target).
Worked example: Match another layer's scale
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 Size echo and paste:
export const outputs = ["layer_2:transform.scale"];
export function frame() {
const sun_scale = layer("Sun").scale.value;
return [sun_scale[0] + 20, sun_scale[1] + 20];
}
Line by line:
- Line 4: scale is a Property whose value is an [x, y] percentage pair - [100, 100] means untouched.
- Line 5: The Planet always renders 20 points larger than the Sun's scale: resize the Sun and the Planet keeps its lead.
What you see. The planet renders at 120 percent scale - always exactly 20 points above whatever the sun's scale is set to.
rotation
Type: Property (field).
The transform.rotation Property (a live degrees value; a set() target).
Worked example: A steady spin
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 Spin and paste:
export const outputs = ["layer_1:transform.rotation"];
export function frame() {
return time * 45;
}
Line by line:
- Line 4: rotation speaks degrees, positive clockwise, and keeps counting past 360 - no wrapping needed. Forty-five degrees per second completes a half turn every four seconds; at the two second mark the card stands at exactly 90 degrees.
What you see. The card spins clockwise at a steady 45 degrees per second, upright again every eight seconds.
anchor
Type: Property (field).
The transform.anchor Property (a live [x, y] value; a set() target).
Worked example: Move the pivot
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 Corner pivot and paste:
export const outputs = ["layer_1:transform.anchor"];
export function frame() {
return [160, 100];
}
Line by line:
- Line 4: anchor is the layer's own pivot point, in pixels from its top-left corner. The Card is 160 by 100, so [160, 100] plants the pivot on its bottom-right corner. One nuance worth knowing: when a SCRIPT drives the anchor the drawing shifts (the position stays put and the content re-hangs from the new pivot), whereas the panel's anchor gesture moves position in step so pixels appear still - scripts get the raw property.
What you see. The card shifts up and left as its pivot moves to the bottom-right corner - and any rotation added afterwards swings around that corner like a door on a hinge.
opacity
Type: Property (field).
The transform.opacity Property (a live percent value; a set() target).
Worked example: 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.
skew
Type: Property (field).
The transform.skew Property (a live degrees value; a set() target).
Worked example: An italic lean
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 Lean italic and paste:
export const outputs = ["layer_1:transform.skew"];
export function frame() {
return 20;
}
Line by line:
- Line 4: skew shears the layer by an angle in degrees, sliding its top edge sideways like italic type. Twenty degrees is a confident lean; the layer's outline stays a parallelogram, never distorting into perspective.
What you see. The card shears into a parallelogram, its top edge slid to the side in a 20 degree italic lean.
skewAxis
Type: Property (field).
The transform.skew_axis Property (a live degrees value; a set() target).
Worked example: Shear along the other axis
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 Vertical shear and paste:
export const outputs = [
"layer_1:transform.skew",
"layer_1:transform.skew_axis",
];
export function frame() {
set(thisLayer.skew, 25);
set(thisLayer.skewAxis, 90);
}
Line by line:
- Lines 7-8: skewAxis turns the direction the shear slides along - 0 slides horizontally, 90 vertically. Here both members are driven through set() using the layer's own Property handles as targets, the handle-flavoured twin of passing an id string.
What you see. The card shears vertically - its right edge slides downwards - instead of the usual sideways italic lean.
prop
prop(key: string): Property
A property handle for this layer's key.
Worked example: Any property by key
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 Distance dimmer and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
const leader = layer("Leader");
return leader.prop("transform.position").value[0] / 8;
}
Line by line:
- Lines 4-5: prop(key) on a layer reaches ANY property by its key string - the same keys the timeline shows - not just the named transform members. Useful when the key arrives as data. Mid-march the Leader's x is 320, so the Follower rests at 40 percent.
What you see. The follower's opacity tracks the leader's march: dim at the left of the stage, brightening steadily to 70 percent as the leader reaches the right.
controls
Type: ExpressionControl[] (field).
The layer's expression controls in document order (each value live).
Worked example: Walk a layer's controls
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 Control census and paste:
export const outputs = ["layer_2:transform.scale"];
export function frame() {
const rig = layer("Rig");
const size = 80 + rig.controls.length * 5;
return [size, size];
}
Line by line:
- Lines 4-5: controls is the layer's expression controls as an array, in panel order - each entry a live ExpressionControl handle. The Rig carries six, so the Planet renders at 110 percent. Loop over the array to treat a rig's controls as data.
- Line 6: Scale takes the same number on both axes to stay square.
What you see. The planet renders at 110 percent scale - 80 plus five per control on the Rig. Add a seventh control and it grows again.
numControls
Type: number (field).
controls.length.
Worked example: Count without the array
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 Census turn and paste:
export const outputs = ["layer_1:transform.rotation"];
export function frame() {
return layer("Rig").numControls * 15;
}
Line by line:
- Line 4: numControls is controls.length without building the array - the cheap census. Six controls turn the Sun a quarter past a quarter: 90 degrees.
What you see. The sun square stands rotated 90 degrees - fifteen degrees for each of the Rig's six controls.
control
control(id: string): ExpressionControl | null
The layer's OWN control by id, or null.
Worked example: Ask politely for a control
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 Maybe control and paste:
export const outputs = ["layer_1:transform.rotation"];
export function frame() {
const dial = layer("Rig").control("xcontrol_9");
return dial ? dial.value : 15;
}
Line by line:
- Line 4: layer.control(id) looks only among THIS layer's own controls and answers null when the id is not there - unlike the global control(), which raises the typed reference_missing. The null form suits optional rig features.
- Line 5: No ninth control exists on the Rig, so the fallback of 15 degrees stands. Add one later and its value takes over without editing the script.
What you see. The sun rests at a modest 15 degree turn - the script's fallback, because the Rig has no ninth control to consult.
path
path(shape_id?: string): PathGeometry | null
M10 DOM2-3: the layer's EFFECTIVE outline (a path layer's combined contents, else its inline geometry after roundness/trim; a solid/frame's rounded box) - or ONE contents leaf by shape id. Document-pure at the layer's local clock (no overlay - the cycle-free rule); null when nothing drawable (the soft-miss).
Determinism: pure.
path() answers the layer's EFFECTIVE outline: a path layer's combined contents when it has contents, else its inline geometry after roundness and trim; a solid or frame answers its rounded box. Pass a shape id for one contents leaf. Reads are document-pure at the layer's local clock, so a script-driven geometry term never feeds back into a path read.
Worked example: A chip rides the wave
The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.
- The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Wave: a path layer, at [0, 0].
- Chip: a solid layer, at [320, 180], 24 by 24, filled ffd166.
- Plate: a solid layer, at [480, 80], 160 by 40, filled 1a1e26.
The script. Add a project script named Point on path and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const rail = layer("Wave").path();
const p = rail.pointAtFraction(0.5);
set("layer_2:transform.position", [p.x, p.y]);
}
Line by line:
- Line 4: The Wave layer's own effective outline arrives as a PathGeometry sampler. A layer with nothing drawable would have answered null.
- Line 5: Halfway along the 300 px rail by arc length is the point (250, 200): 150 px into the 200 px horizontal run.
- Line 6: The Chip's position takes the sample. The Wave sits at the origin, so its layer-local px read directly as comp coordinates here.
What you see. The chip locks onto the middle of the wave's horizontal run at (250, 200), the classic point-on-path setup.
maskPath
maskPath(mask_id: string): PathGeometry | null
M10 DOM2-3: one mask's evaluated path as geometry (any mode); null for a missing mask or placeholder path.
Determinism: pure.
Worked example: A chip rides a mask's rim
The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.
- The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Wave: a path layer, at [0, 0].
- Chip: a solid layer, at [320, 180], 24 by 24, filled ffd166.
- Plate: a solid layer, at [480, 80], 160 by 40, filled 1a1e26.
The script. Add a project script named Mask rim rider and paste:
export const outputs = ["layer_2:transform.position"];
export function frame() {
const rim = layer("Plate").maskPath("mask_1");
const p = rim.pointAtFraction(0.25);
const ghost = layer("Plate").maskPath("mask_9");
set("layer_2:transform.position", ghost === null ? [p.x + 200, p.y + 300] : [0, 0]);
}
Line by line:
- Line 4: One mask by id, any mode: the Plate's rectangular mask answers its evaluated path as a closed 300 px rim.
- Line 5: A quarter of the way round the rim by arc length is 75 px along the top edge: the mask-local point (75, 0).
- Lines 6-7: A mask id nothing carries answers null (the soft-miss rule), and the chip parks at the offset rim point only when that held. Mask points are layer-local, so the script offsets them into view.
What you see. The chip parks at (275, 300), carrying the rim sample plus its offset, and the deliberate miss on mask_9 answered null.
textMetrics
textMetrics(): TextMetrics | null
M10 round 2.8 (Scripting 1): the SHAPED text geometry in LAYER-LOCAL px at the layer's LOCAL clock, document-pure (the authored truth - script-driven text does not feed back into metric reads, the path-reads rule). {box: {x, y, width, height} - the E19 derived box; lines: [{index, x, y, width, height, baseline, begin, end}] - begin/end are BYTE offsets into the shaped (case-mapped) text; clusters: [{line, x, y, width, height, begin, end, text}] - one entry per caret gap (the shaper's cluster-granular truth: 'characters' of complex scripts are clusters); y/height are the line's top/extent}. The shaper builds from the document's OWN embedded font bytes, so every host answers identically; null for non-text layers, legacy mode, missing font bytes or a shaping failure. Enables typewriter reveals (drive content.text per frame and read the growing bounds), per-word/character backgrounds (drive a shape layer's path or position from cluster boxes) and metrics-reactive layouts; PER-CHARACTER transforms/opacity/paint of the text layer's own glyphs stay the TEXT ANIMATORS feature (the dossier).
Determinism: pure.
textMetrics() answers the SHAPED text geometry in layer-local px at the layer's local clock, document-pure (script-driven text never feeds back into a metric read - the path-reads rule): {box, lines[], clusters[]} with begin/end as BYTE offsets into the shaped (case-mapped) text. Clusters are one entry per caret gap - complex-script "characters" ARE clusters. Null is the soft miss: a non-text layer, legacy mode, missing font bytes or a shaping failure.
Worked example: Counting the headline
The scene. A shaped headline reading YOUR STORY beside a small amber chip - the text-metrics examples read the headline's lines and clusters and drive the chip from them.
- The comp Caption: 640 by 360 at 25 fps, 100 frames (4 seconds).
- Headline: a text layer, at [325, 240], filled e8e6e3, reading "YOUR STORY".
- Chip: a solid layer, at [320, 245], 24 by 24, filled ffd166.
The script. Add a project script named Word counter and paste:
export const outputs = ["layer_2:transform.opacity"];
export function frame() {
const metrics = layer("Headline").textMetrics();
if (metrics === null) {
set("layer_2:transform.opacity", 0);
return;
}
const word = metrics.clusters.filter((c) => c.end <= 4).length;
set("layer_2:transform.opacity", metrics.lines.length * 10 + word * 5);
}
Line by line:
- Lines 4-8: The headline's shaped geometry arrives whole - box, lines and clusters. A solid, a legacy text layer or a fontless one would have answered null, and the guard keeps the chip honest instead of erroring.
- Line 9: YOUR spans the first four bytes of the shaped text, so exactly four clusters end at or before byte 4 - the per-word selection every word-background recipe starts from (cluster boxes then size the backing shape).
- Line 10: One line and four word clusters drive the chip to 30 percent - counts and byte offsets are shaper-stable, so the value holds on every host.
What you see. The chip settles at 30 percent opacity: one shaped line (10) plus the four clusters of YOUR (20).