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 animationAssign and assignCues to 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

  1. Stops all running animations on current children (_oscillaColorAnim, _oscillaScaleAnim, _blinkTimer).
  2. Removes expanded children from window.cues.
  3. Clears expanded id, data-oscilla, data-oscilla-propagate-child from each child.
  4. Calls propagate(svg) — re-expands with fresh rnd() values.
  5. Calls animationAssign(groupEl) — restarts color/scale/fade animations immediately on the new children.
  6. Calls assignCues(groupEl, newCues) — registers new children in window.cues for 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