propagate() — Developer Reference
Technical reference for the propagate() pre-processor:
expansion pipeline, template mechanics, compound cues,
runtime data attributes, and live re-expansion.
Position in the Initialisation Pipeline
propagate() runs before any cue parsing or animation assignment:
SVG Load
propagate(svgElement) expand <g id="propagate(...)"> → child IDs
preProcessDrag(svgElement) strip drag() tokens
animationAssign(svgElement) start scale/rotate/color/fade animations
assignCues(svgElement) register playhead-triggered cues
Children are expanded in-place by rewriting their id attributes.
The parent group's id is removed after expansion.
File Locations
parser/
preProcessPropagate.js propagate() preprocessor + reexpandPropagateGroup()
also exports splitTopLevelArgs / isCueTemplate /
evaluateExpr for the DSL inspector's propagate editor
cueUtils.js splitCueId() — shared by propagate and animationAssign
Expression forms understood by evaluateExpr: rnd(min,max) (float),
irnd(min,max) (integer, inclusive), rnd([a,b,c]) (pick one), plain
numbers, and literal fallthrough.
How Expansion Works
Template detection
isCueTemplate(str) — returns true if the string looks like a cue call
(word() and is not rnd(. All other top-level args are treated as
argument expressions.
// given: propagate(scale(values:[${1},${2}], dur:${3}, uid:sq), rnd(1,2), rnd(1,2), rnd(0.4,1.2))
templates = ["scale(values:[${1},${2}], dur:${3}, uid:sq)"]
argExprs = ["rnd(1,2)", "rnd(1,2)", "rnd(0.4,1.2)"]
Placeholder substitution
applyPlaceholders(template, argExprs) replaces every ${n} with the
evaluated value of argExprs[n-1]. Each occurrence triggers an
independent evaluation, so the same ${1} used twice gets two
different rnd() results.
Multiple templates (compound cues)
The first comma-separated parts that match isCueTemplate are all
collected as templates. Each child receives every template expanded
and joined with a space:
// two templates in one propagate():
propagate(
color(vals:[white,black], mode:step, dur:${1}, uid:c)
scale(values:[${2},${3}], mode:alternate, dur:${4}, uid:s),
rnd(0.04,0.25), rnd(0.80,0.95), rnd(1.05,1.20), rnd(0.06,0.5)
)
// child 0 gets:
// id="color(vals:[white,black], mode:step, dur:0.11, uid:c_0) scale(values:[0.84,1.12], mode:alternate, dur:0.31, uid:s_0)"
Note: compound IDs require both
animationAssignandassignCuesto split them before parsing — see Compound IDs below.
UID injection
Each child gets a unique uid suffix (_0, _1, …) appended to any
uid:NAME found in the expanded cue. If no uid: is present,
uid:prop_GROUPINDEX_CHILDINDEX is appended automatically.
Runtime Data Attributes
After expansion two data attributes are written to the DOM and used by the DSL inspector:
| Attribute | Written to | Value | Purpose |
|---|---|---|---|
data-oscilla-propagate-id |
parent <g> |
original propagate DSL string | inspector lookup key; source SVG has id="..." matching this value |
data-oscilla-propagate-child |
each expanded child | "true" |
lets _cueIdAt in the inspector redirect clicks to the parent |
The parent's id attribute is removed after expansion.
The data-oscilla-propagate-id is the only retained trace of the
original template on the parent.
Compound IDs
A compound ID produced by a multi-template propagate looks like:
color(vals:[white,black], mode:step, dur:0.11, uid:c_0) scale(values:[0.84,1.12], mode:alternate, dur:0.31, uid:s_0)
Two subsystems handle these:
assignCues (cueDispatcher.js)
Uses splitCueId() before calling parseCueToAST(), producing one
AST per cue. Each is pushed to window.cues separately and triggered
independently by the playhead.
animationAssign (animation.js)
Also uses splitCueId() in a for loop, calling the appropriate
animation handler for each parsed AST. Without the split,
parseCueToAST throws NotAllInputParsedException on the trailing
cue expression.
splitCueId (parser/cueUtils.js)
Paren-depth-aware string splitter. Handles nested () and [] so
commas inside array arguments are not treated as expression separators.
Imported by both cueDispatcher.js and animation.js.
splitCueId('color(vals:[a,b], dur:0.1) scale(values:[0.9,1.1], dur:0.3)')
// → ['color(vals:[a,b], dur:0.1)', 'scale(values:[0.9,1.1], dur:0.3)']
reexpandPropagateGroup(groupEl)
Exported from preProcessPropagate.js. Used by the DSL inspector to
apply a template edit without a full page reload.
What it does
- Stops all running animations on current children (
_oscillaColorAnim,_oscillaScaleAnim,_blinkTimer). - Removes expanded children from
window.cues. - Clears expanded
id,data-oscilla,data-oscilla-propagate-childfrom each child. - Calls
propagate(svg)— re-expands with freshrnd()values. - Calls
animationAssign(groupEl)— restarts color/scale/fade animations immediately on the new children. - Calls
assignCues(groupEl, newCues)— registers new children inwindow.cuesfor playhead triggering.
Caller contract
The caller must restore groupEl.id to the new propagate DSL and
remove data-oscilla-propagate-id before calling this function,
so propagate(svg) can find the group via [id^="propagate("].
srcEl.setAttribute('id', newDsl);
srcEl.removeAttribute('data-oscilla-propagate-id');
await reexpandPropagateGroup(srcEl);
Circular dependency avoidance
cueDispatcher.js imports propagate from preProcessPropagate.js.
reexpandPropagateGroup needs animationAssign (animation.js) and
assignCues (cueDispatcher.js). To avoid a load-time cycle, both are
loaded via dynamic imports inside the function body:
const { animationAssign } = await import('../cues/animation.js');
const { assignCues } = await import('../cues/cueDispatcher.js');
Dynamic imports are deferred to call time, by which point both modules are already fully loaded. No circular dependency at the module graph level.
Important Constraints
# hex colours in templates break querySelectorAll.
The [id^="propagate("] selector works fine, but once a # appears in
a child's expanded id, subsequent CSS selectors on that string fail.
Use named colours (white, black, red etc.) or HSL/RGB notation
in vals:[...] arrays. Named colours are supported by parseColorToHSL
in color.js.
Propagate children are virtual — they have no source SVG ids.
The patch endpoint (/api/project/:name/patch-svg-id) looks up elements
by id in score.svg. Expanded children don't exist there. Always
redirect edits to the parent group via data-oscilla-propagate-id.
Multi-pass expansion.
propagate() loops until no more [id^="propagate("] groups are found.
Nested propagate() calls (propagate inside a propagate) are therefore
supported, though rarely needed.
Tip: use ← → or ↑ ↓ to navigate the docs