Property
The property handle's members: the address, the authored value and the keyframe list behind it.
A property handle.
id
Type: string (field).
The property id (layer_id:key).
Worked example: The full address in hand
The scene. One pale card resting near the centre of the stage with its rotation keyframed from 0 to 90 degrees - a still scene for reading a property's identity.
- The comp Plaque: 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 Address check and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const pos = thisLayer.position;
return pos.id === "layer_1:transform.position" ? 70 : 0;
}
Line by line:
- Lines 4-5: Every Property handle knows its own id - the layer id, a colon, then the key. That id is the exact string outputs declare and set() accepts, so a handle can hand its address onwards. The check passes and the Card rests at 70.
What you see. The card rests at 70 percent opacity - the visible receipt that a Property handle's id really is its layer id plus its key.
key
Type: string (field).
The property key.
Worked example: Which property is this
The scene. One pale card resting near the centre of the stage with its rotation keyframed from 0 to 90 degrees - a still scene for reading a property's identity.
- The comp Plaque: 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 Key length and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const key_name = thisLayer.rotation.key;
return key_name.length * 4;
}
Line by line:
- Line 4: key is the property's name on the wire - "transform.rotation", the same spelling the timeline's rows and layer.prop() use. Handles passed around as data can always say which property they are.
- Line 5: Eighteen characters, times four: the Card rests at 72 percent - a visible receipt for the read.
What you see. The card rests at 72 percent opacity, four percent for each character of the key name transform.rotation.
value
Type: T (accessor).
The resolved value (overlay or keyframed) at the current frame.
A read answers the WIRE value of the row's kind, exactly as the property vocabulary's units column states it: numbers in wire units (degrees, percent, pixels, decibels), points as [x, y], colours as [r, g, b, a] in 0..1, strings verbatim (text, enums; a layer reference as the id, null when unset), booleans, a dropdown control as its item index, corner radii / gradient stops / curve points / mask paths / subpath lists / width profiles / crop transforms as their wire arrays and objects, a font reference as its wire object (null when unset) and an asset reference as the asset id (null when unset). A control key reads by its control's kind. What you read is the shape set() accepts back, so a value can round-trip untouched.
Worked example: 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.
Worked example: Read a colour, write a darker one
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 Shade match and paste:
export const outputs = ["layer_2:content.fill_colour"];
export function frame() {
const leader = prop("Leader:content.fill_colour").value;
return [leader[0] * 0.5, leader[1] * 0.5, leader[2] * 0.5, 1];
}
Line by line:
- Line 4: A read answers the WIRE value of the row's kind: a colour comes back as [r, g, b, a] in 0..1, never a hex string and never a bare number. The Leader's teal 2ec4b6 reads as [0.18, 0.769, 0.714, 1].
- Line 5: Halving the three colour channels and keeping alpha at 1 builds a darker teal in the very same shape, and the single-output return writes it to the Follower's fill colour - a colour link that follows any repaint of the Leader.
What you see. The follower wears a darker shade of the leader's teal (#17625b) for the whole comp - recolour the leader and the follower re-shades itself.
numKeys
Type: number (accessor).
M10 DOM2-2: the track length (0 while un-keyed; projections read their one list track - the layerTrackLength rule).
Determinism: pure.
Worked example: Counting keyframes
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 Key counter and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const rotation = prop("Card:transform.rotation");
const position = prop("Card:transform.position");
set("layer_1:transform.opacity", 40 + rotation.numKeys * 10 + position.numKeys);
}
Line by line:
- Lines 4-5: Two handles on the same card: the rotation track carries the fixture's two keys, the position track carries none.
- Line 6: numKeys reads the track length, 0 while un-keyed, so the opacity lands on 40 + 20 + 0. Projections follow the same rule by reading their one list track.
What you see. The card fades to 60 percent: two rotation keys counted at ten each over the 40 base, and the un-keyed position track counted zero.
keyAt
keyAt(index: number): Keyframe | null
M10 DOM2-2: the keyframe at a 0-based index; null out of range (the soft-miss rule). Named keyAt because the shipped key member is the property-key string - the dossier's key(i) spelling, corrected.
Determinism: pure.
The method is keyAt, not key(i): the shipped key member is the property-key string and could not be repurposed. keyAt is 0-based and answers null out of range, the same soft-miss rule the layer traversal follows.
Worked example: First key, last key
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 Key spread and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const rotation = prop("Card:transform.rotation");
const first = rotation.keyAt(0);
const last = rotation.keyAt(rotation.numKeys - 1);
const past_the_end = rotation.keyAt(2);
const spread = last.value - first.value;
set("layer_1:transform.opacity", past_the_end === null ? spread + 5 : 0);
}
Line by line:
- Lines 5-6: keyAt is 0-based: index 0 is the first key (frame 0, value 0) and numKeys - 1 the last (frame 96, value 90). Each answer is a read-only Keyframe carrying the wire shape verbatim.
- Line 7: Index 2 is past the end of a two-key track, so the answer is null, never a throw.
- Lines 8-9: The value spread (90 degrees) drives the opacity, plus five only because the out-of-range read behaved.
What you see. The card settles at 95 percent opacity: the 90 degree spread between the first and last keys plus the five point proof that keyAt(2) answered null.
nearestKey
nearestKey(frame: number): Keyframe | null
M10 DOM2-2: the closest key by |frame| distance (ties round EARLIER, pinned); null on an un-keyed property.
Determinism: pure.
Worked example: Snapping to the nearest key
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 Nearest key and paste:
export const outputs = ["layer_1:transform.opacity"];
export function frame() {
const rotation = prop("Card:transform.rotation");
const tie = rotation.nearestKey(48);
const late = rotation.nearestKey(90);
set("layer_1:transform.opacity", tie.frame === 0 && late.frame === 96 ? 70 : 0);
}
Line by line:
- Line 5: Frame 48 sits exactly halfway between the keys at 0 and 96. Ties round EARLIER, so the answer is the frame 0 key.
- Line 6: Frame 90 is six frames from the late key and ninety from the early one, so the frame 96 key wins.
- Line 7: Both answers drive the proof value; an un-keyed property would have answered null instead.
What you see. The card rests at 70 percent opacity, proving the halfway tie snapped to the earlier key and the late probe snapped forward.