Score

cue:o2p — Object-to-Path Animation

o2p(...) animates an SVG object along a named <path>.
For plain point-to-point movement without a drawn path, see trans() — o2p is for motion that follows a visible curve.

demo-o2p — a ring travelled only from start:0.25 to end:0.75, and the same journey with linear and easeInOutQuad easing
demo-o2p — a ring travelled only from start:0.25 to end:0.75, and the same journey with linear and easeInOutQuad easing

It supports path traversal, directional modes, rotation modes, looping, OSC output, partial-path motion, trigger-based activation, delayed start, visual pre-start states, interactive touch/drag control, grouped fader presets with launcher bars, pause-drag repositioning (spatial scores), live console hot-reload, and multi-client sync.


BASIC FORM

o2p(path:<id>, dur:<beats>, mode:fwd)

Required:

Key Meaning
path The ID of the <path> element to follow

TIMING

dur:<beats>

Duration of one full traversal of the path.

o2p(path:orbit, dur:6)

In mode:alt, the full A→B→A cycle lasts dur beats.

tdelay:<beats>

Delays the start of the animation after the cue triggers.

o2p(path:orbit, tdelay:3)

tdelay is in beats, independent of score position.


SPEED PROFILE — at: and dur:[…]

dur: times one traversal — one pass of the path, which is a lap where the path is closed and a run from end to end where it is a line or an arc. One number is one speed for that whole pass. When the movement has to be uneven — setting off from rest, covering one part of the path faster than another, each pass quicker than the last — cut the traversal into stretches and give each one its own beats and its own curve.

at:[t1, t2, …]   where the traversal is cut. Absolute normalized positions on
                 the path (the same space as start: and end:); 0 and 1 are
                 implied, so at:[0.25, 0.5, 0.75] makes four stretches.
dur:[…]          BEATS PER STRETCH. A list here is what turns the profile on.
ease:[…]         one curve per stretch, indexed the same way.

Entries are picked by (pass * segments + segment) % list.length, so they carry on from one traversal to the next instead of restarting: twelve dur entries over four stretches prescribe three different passes, then repeat. A shorter list cycles, the way trans()'s per-leg lists do.

Three passes of a path cut into quarters, setting off from rest and gaining speed pass by pass:

o2p(uid:m1, path:ring, start:0, trig:auto, loop:3,
    at:[0.25, 0.5, 0.75],
    dur:[8, 4, 3.2, 3.4,   2.8, 3.2, 2.8, 3.2,   2.5, 2.9, 2.5, 2.9],
    ease:[easeInQuad, linear, linear, linear,
          linear, linear, linear, linear,
          linear, linear, linear, linear])

18.6 beats for the first pass, 12 for the second, 10.8 for the third. The stretches need not be equal: at:[0.18, 0.5, 0.68] gives two short ones and two long ones, which is how a profile follows the shape of the path it is drawn on.

The splits belong to the path, not to the object. They are absolute positions, so several objects on one path all change speed at the same places, wherever each is drawn. An object starting mid-stretch covers a partial stretch at each end of its traversal, and the two partials share that stretch's beats between them: every object's traversal takes the prescribed time, wherever it starts. Give them all the same at:/dur:/ease:, and stagger their entrances with tdelay:.

Writing speed rather than counting it

An easing sets the speed at the ends of its stretch, and two stretches meet cleanly only when the first one's exit speed equals the second one's entry speed. As multiples of the stretch's own mean speed:

easing entry exit
linear 1 1
easeInQuad / easeInCubic / in(p) 0 2 / 3 / p
easeOutQuad / easeOutCubic / out(p) 2 / 3 / p 0
easeInOutQuad, easeInOutSine, … 0 0

So the in-curves are for leaving a standstill, the out-curves for arriving at one, and everything between them is linear. An easeInOut stretch stops dead at both ends — a profile with one in the middle is an object that halts at every split, which is rarely what "varied speed" meant.

A profile covers a whole traversal in one direction, so at:/dur:[…] need mode:forward and no end: sub-range; they say so in the console when they do not get them, rather than quietly using the first number. With no at: at all, dur:[12, 9, 7] is simply one duration per traversal — three passes, each faster than the last.


SPEED CONTROL

bipolar:1

Enables bipolar speed mode for duration-bound animations. This transforms the speed fader from unipolar (slow→fast) to bipolar (reverse→stop→forward).

Fader Position Speed Effect
Left (0) Maximum reverse speed (anticlockwise)
Center (0.5) Stopped
Right (1) Maximum forward speed (clockwise)
o2p(path:orbit, dur: follow(fader1.t, 0.1, 4), bipolar:1)

Use Cases:

Without bipolar (default), the speed fader is unipolar:


INITIAL VISIBILITY

init:

Controls how the object looks before the animation begins.

Value Effect
show Visible immediately (default)
hide Hidden until triggered
ghost Semi-transparent (30% opacity), no interaction
fadein(beats) Fade in over that many beats
armed Ghost opacity (0.7), click to start animation
armed(opacity) Custom ghost opacity, click to start
armed(opacity, fade) Custom opacity and fade duration, click to start

The object is positioned at its correct starting location before motion begins.

o2p(path:orbit, tdelay:4, init:hide)
o2p(path:ring, dur:8, init:fadein(2), trig:playhead)
o2p(path:spiral, dur:12, init:armed(0.3))

DIRECTION MODES

Mode Meaning
forward / fwd 0 → 1 along path
reverse / rev 1 → 0
alternate / alt back-and-forth motion
o2p(path:curve, mode:alt, dur:8)

Alternate and path shape

Alternate mode is path-shape-aware:


PAUSE-DRAG REPOSITIONING

drag:1

Enables free drag repositioning when an armed animation is paused. Default is off.

o2p(path:orbit, dur:10, init:armed, drag:1)

Without drag (default): click pauses the animation in place, click again resumes from the same position. Standard behaviour for musical scores.

With drag:1: click pauses, then the object can be dragged freely off the path. On resume, the object snaps to the nearest point on the path and continues from there. The original path range (start/end) is preserved — only the first cycle after drag starts from the new position; subsequent cycles use the full original range.

This is designed for spatial audio scores where a performer repositions a sound source during a pause, then resumes the trajectory from the new location. While it is dragged, a spatial o2p keeps sending where it is.

o2p(path:traj_spiral, uid:src1, dur:25, drag:1,
    spatial:dome, bounds:dome_bounds, osc:1)

PATH SEGMENTS

o2p(path:ring, start:0.2, end:0.8)

The object travels only between normalized positions start and end.

o2p(path:ring, start:auto)

start:auto begins at the point of the path nearest to where the object is placed in the SVG — useful on closed paths, where the path's first node is otherwise an arbitrary starting point. Motion still follows the path's drawing direction (mode:reverse flips it).


ROTATION

rotate: Description
none no rotation
aligned align to tangent direction
locked fixed heading using rotlock
spin continuous rotation (rotspeed, rotdir)

Additional keys:

Key Meaning
rotoffset add angular offset
rotlock fixed heading in degrees
rotspeed seconds per full rotation
rotdir ±1 direction multiplier

LOOPING

o2p(path:orbit, loop:3)
o2p(path:orbit, loop:0)   // infinite
o2p(path:curve, loop:1, freeze:1)   // A→B once, stop at B

By default a finished bounded loop snaps the object back to its start
position. Add `freeze:1` (or `fill:freeze`) to rest at the **end** of the
path instead — travel from A to B and stay at B.

A bounded run publishes <uid>.end on the parameter bus: 0 as it starts, 1 when it finishes. Anything can wait for it — a map-mode station (station(until:"buoy.end"), see stations) or any cue with a condition trigger (trig:"buoy.end").


OSC OUTPUT

o2p(path:orbit, osc:true)

Emits:

/o2p/<uid> <pathT> <normX> <normY> <angle>

Custom OSC address:

o2p(path:orbit, osc:true, oscAddr:myController)

Emits:

/myController <pathT> <normX> <normY> <angle>

TRIGGERING

Trigger Behavior
trig:auto starts automatically in page mode
trig:edge starts when the playhead intersects in scroll mode
trig:touch no auto-animation; object is draggable along path

Cues may also be activated programmatically.
tdelay applies after the trigger is detected.


TOUCH MODE (Interactive Control)

The trig:touch mode transforms the o2p animation into an interactive controller. Instead of automatically animating, the object waits for user interaction.

o2p(path:fader, trig:touch, osc:true)

Behavior:

Use Cases:

Example — Linear Fader:

o2p(path:verticalLine, trig:touch, osc:true, oscAddr:fader1)

Example — Circular Knob:

o2p(path:knobArc, trig:touch, start:0.1, end:0.9, osc:true, oscAddr:knob1)

Visual Indicator: Touch mode objects display an orange hit-label ring (instead of purple) to indicate they are draggable.

Moved by cues and by OSC: ui(uid:<fader>, value:…) and step: move a touch fader as if dragged (see ui()), and so does OSC arriving at /<uid>/t (say an expression pedal): the handle moves on every client, and the sync leader shares the value with clients that join later.

Grabbing a running motion — scrub:1: the other way round from a touch fader: the motion runs by itself, and a press-and-drag on the object takes hold of it like tape under a finger. It moves along its path by the drag's component along the path, is held still while held (on every client), and carries on from where it is let go. A tap without a drag runs tap:. With scrub:flip a drag against the way the motion runs also turns it round on release: the bipolar fader its speed follows goes to 1−t, as ui(uid:…, value:flip) would.

o2p(path:lane1, uid:L1, dur:follow(spd1.t, 120, 30), bipolar:1, loop:0,
    trig:auto, hit:0, scrub:flip, tap:"ui(uid:spd1, value:hold)")

Who may drag it: allow:[names], optionally with lock:"cond", limits who may drag (or double-tap) the fader — see ui().


LIVE RE-RUN

Run from the live console, an o2p() that is already running is not restarted: it carries on from where its object is, with the new parameters. Re-run with trig:touch and it stops there as a touch fader; re-run without it and the fader sets off from where it was left; loop:1 runs it out from where it is. Playhead and button re-triggers in a score still restart from start:.

path: may name a group around the geometry, such as a line made with shape(sides:1).


FADER GROUPS AND PRESETS

Touch-mode faders can be collected into named groups. A group enables saving and recalling all fader positions as named presets, tweening between presets with easing, and sequencing preset recalls over time.

Grouping Faders

Add group:<name> and uid:<id> to each touch-mode fader:

o2p(path:track1, trig:touch, group:mixer1, uid:vol, osc:true)
o2p(path:track2, trig:touch, group:mixer1, uid:pan, osc:true)
o2p(path:track3, trig:touch, group:mixer1, uid:send, osc:true)

All three faders register under window._o2pTouchGroups["mixer1"] and can be saved/recalled as a unit. The group value is freeform — any string.

Launcher Bar

When a group registers, a launcher bar appears below the group's bounding box. It provides direct access to presets and sequences without opening the full preset panel.

The launcher includes:

An orange toggle circle appears to the right of the group. Click it to show or hide the launcher bar.

Preset Manager Panel

The preset panel (keyboard shortcut Alt+Shift+O, or gear button on launcher) provides full preset management:

Console access: window.o2pPresetUI.toggle()

Preset Data

Each preset stores the normalized position of every fader in the group:

{
  "mixer1": {
    "vol": { "t": 0.75 },
    "pan": { "t": 0.3 },
    "send": { "t": 0.5 }
  }
}

If a fader has a rotation handle (hmode:), a p value is also stored:

{ "vol": { "t": 0.75, "p": 0.6 } }

Console API

window.o2pPresets.save("intro")           // save current positions
window.o2pPresets.recall("intro")         // instant recall
window.o2pPresets.recall("intro", { dur: 2, ease: "easeInOutSine" })  // tween
window.o2pPresets.list()                  // list all preset names
window.o2pPresets.delete("intro")         // delete a preset

// Sequences
window.o2pPresets.defineSequence("drift", [
  { preset: "intro", dur: 3 },
  { preset: "verse", dur: 2 },
  { preset: "chorus", dur: 4 }
], { loop: true })
window.o2pPresets.playSequence("drift")
window.o2pPresets.stopSequence()

ROTATION HANDLES

Touch-mode faders can have a secondary rotation handle that adds a second control dimension. This is useful for parameters like pan, filter cutoff, or any value that benefits from a rotary control alongside the linear fader.

hmode:<continuous|limited>

Determines the rotation behavior:

rotrange:<degrees>

Sets the arc range for hmode:limited. Default is 270 degrees.

o2p(path:track1, trig:touch, group:mixer1, uid:vol, hmode:limited, rotrange:270, osc:true)

handle:<elementId>

Reference a user-drawn SVG element as the rotation handle instead of the auto-generated one:

o2p(path:track1, trig:touch, group:mixer1, uid:vol, hmode:limited, handle:knob1, osc:true)

Where knob1 is the ID of an existing SVG element in the score.


TARGET BUTTONS

Click-type o2p elements can control other faders in the same group using the target parameter. This allows creating buttons that set specific fader positions — useful for presets, mute/unmute toggles, or recall buttons.

Basic Target Button

Use target:<faderId> and targetT:<value> to create a button that sets a fader to a specific position when clicked:

o2p(path:btnPath, trig:click, group:mixer1, target:vol, targetT:0.8)

When clicked, this sets the vol fader in mixer1 to t=0.8.

Toggle Button

Use toggle:<val1>,<val2> to alternate between two positions on each click:

o2p(path:btnPath, trig:click, group:mixer1, target:vol, toggle:0,0.8)

First click sets vol to t=0.8, second click sets it to t=0, and so on.

Example: Mute/Unity Buttons for a Mixer

<!-- Fader paths -->
<path id="track0" d="M 0,120 L 0,0" />
<path id="track1" d="M 30,120 L 30,0" />

<!-- Faders -->
<rect id="in0" data-oscilla="o2p(path:track0, trig:touch, group:mixer, uid:in0, osc:1)" ... />
<rect id="in1" data-oscilla="o2p(path:track1, trig:touch, group:mixer, uid:in1, osc:1)" ... />

<!-- Toggle button paths (minimal) -->
<path id="btn0" d="M 0,130 L 0,131" style="stroke:none" />
<path id="btn1" d="M 30,130 L 30,131" style="stroke:none" />

<!-- Toggle buttons: click to toggle fader between mute and 0dB -->
<rect id="mute0" data-oscilla="o2p(path:btn0, trig:click, group:mixer, target:in0, toggle:0,0.8, osc:1)" ... />
<rect id="mute1" data-oscilla="o2p(path:btn1, trig:click, group:mixer, target:in1, toggle:0,0.8, osc:1)" ... />

Two kinds of id are in play here and the difference matters. track0 and btn0 are plain ids because something points at them — path: resolves an element by id. The cues themselves live in data-oscilla, with the id left as the uid: other cues address them by (target:in0).

The toggle values 0,0.8 correspond to:

Parameters

Parameter Description
target:<id> The uid of the fader to control (must be in same group)
targetT:<value> Set fader to this t value (0-1) on click
toggle:<v1>,<v2> Alternate between two t values on each click

Note: target requires group to be set. The target fader must also be registered in the same group.


TOGGLE STATES (Interactive Claiming)

Toggle states provide an interactive claiming mechanism independent of OSC output. Performers can double-click to cycle through named states, with visual feedback and optional callbacks. This is useful for instrument assignment, ownership indication, or any multi-state interaction.

Basic Syntax

o2p(path:orbit, dur:10, toggleStates:[idle, claimed])

Parameters

Key Description
toggleStates Array of state names to cycle through on double-click
toggleColors Optional array of colors per state. Use #client for claiming client's color
onToggle Cue expression to execute when state changes

Toggle Colors

Define colors for each state:

o2p(path:orbit, dur:10,
    toggleStates:[idle, claimed],
    toggleColors:[default, #client])
Color Value Effect
default Use the default overlay color
#client Use the claiming client's assigned color
#ff0000 Any hex color

onToggle Callback

Execute a cue expression when the state changes. Supports variable substitution:

Variable Value
$uid Animation UID
$state New state index (0-based)
$stateName New state name
$clientIndex Client's 1-based index in playbackClientNames preference
o2p(path:orbit, dur:10, uid:src1,
    toggleStates:[idle, claimed],
    toggleColors:[default, #client],
    onToggle:"osc(addr:/source/claim, uid:$uid, state:$state, client:$clientIndex)")

Stopping: off:1

o2p(off:1) on the same element stops it where it is and takes it down: no touch handle and no fader are left, and its t keeps the last value. To hold it as a fader instead, run the o2p again with trig:touch. In the live console Ctrl+Shift+. on the line does this.

State Persistence

Toggle states are:

Independence from OSC

Toggle states are independent from osc:1. Having toggleStates does not affect continuous OSC output. You can have claiming UI without OSC, OSC without claiming, or both together.


LIVE CONSOLE EDITING

Running o2p animations can be modified in real time from the live console. Pick an element, edit DSL parameters in the editor, and evaluate.

What happens:

Editable parameters: dur, loop, mode, ease, start, end, osc, oscAddr

Identity parameters (path, uid, init) cannot be changed mid-flight.


MULTI-CLIENT SYNC

Armed animations sync across connected clients via WebSocket:

With drag:1, the resume position is transmitted so remote clients restart from the same path location.

Limitation: late-joining clients see armed elements but do not auto-start running animations. A connected client must stop and restart to bring late joiners into sync.


LIVE UPDATE (uid:)

o2p(path:ring, uid:a1)
o2p(uid:a1, mode:alt)

Later cues with the same uid modify running animations.


FULL PARAMETER LIST

Key Description
path required path reference
dur motion duration — or a list of beats per stretch, with at:
at split points that cut a traversal into stretches
mode direction mode
loop repeat count (0 = infinite)
start, end normalized path range
rotate rotation behavior
rotoffset, rotlock, rotspeed, rotdir rotation parameters
ease easing preset, a parameterised one (in(1.05)), or a list
osc OSC emission
oscAddr custom OSC address (without leading /)
uid animation identity tag
trig trigger mode (auto, edge, touch)
tdelay time delay after trigger
init initial visibility: show/hide/ghost/armed
drag enable pause-drag repositioning (default: off)
bipolar bipolar speed mode: center=stop, left=reverse, right=forward (default: off)
anchor the point of the object that rides the path: the centre of its bounding box (default), origin — its own (0,0) — or [x, y] in its own coordinates. A fixed point does not move as the drawing grows, so a path can be placed exactly: anchor:origin on a group with transform="translate(x,y)" puts that corner on the path
hit hit:0: no hit label, so nothing on the object takes clicks — for a motion nobody clicks, especially a large one, whose label (sized to the object) would cover the controls around it (default: on)
scrub scrub:1: press on the moving object and drag it along its path — it follows the pointer (the drag along the path's direction), is held while held, and carries on at its speed from where it is let go; every client sees it. scrub:flip also reverses a bipolar-speed motion (at the same speed) when it is dragged against the way it runs. allow:/lock: apply (default: off)
tap with scrub:1, a cue to run on a press without a drag: tap:"ui(uid:spd1, value:hold)"
spatial the object is a sound source: plan or dome. Its position on the bounds circle is sent as azimuth / elevation / distance to /oscilla/spat/<uid> instead of its usual values (Spatialisation)
bounds id of the element that is the room (a circle, an ellipse or any shape's box)
format what a spatial o2p sends: aed (default), ae, ad, xyz, aedg, raw
elev a spatial o2p in plan mode: its height in degrees (default 0, ear level)
group fader group name (touch mode only)
hmode rotation handle mode: continuous or limited (touch mode only)
rotrange arc range in degrees for hmode:limited (default 270)
handle ID of user-drawn SVG element to use as rotation handle
launcher show launcher bar for group (default: true, set false to suppress)
toggleStates array of state names for double-click cycling
toggleColors array of colors per state (#client for client color)
onToggle cue expression executed on state change

EXAMPLES

o2p(path:orbitA, dur:8, tdelay:3, init:hide)
o2p(path:spiral, rotate:aligned, rotoffset:-90)
o2p(path:ring, start:0.2, end:0.9, mode:alt)
o2p(path:circle, rotate:spin, rotspeed:2, rotdir:-1)

// Touch mode controllers
o2p(path:faderTrack, trig:touch, osc:true, oscAddr:volume)
o2p(path:knobPath, trig:touch, start:0.15, end:0.85, osc:true)

// Grouped faders with preset system
o2p(path:track1, trig:touch, group:mixer1, uid:vol, osc:true)
o2p(path:track2, trig:touch, group:mixer1, uid:pan, osc:true)
o2p(path:track3, trig:touch, group:mixer1, uid:send, osc:true)

// Grouped fader with rotation handle
o2p(path:track1, trig:touch, group:mixer1, uid:vol, hmode:limited, rotrange:270, osc:true)

// Grouped fader with user-drawn rotation handle
o2p(path:track1, trig:touch, group:mixer1, uid:vol, hmode:continuous, handle:knob1, osc:true)

// Spatial source with pause-drag repositioning
o2p(path:traj_spiral, uid:src1, dur:25, mode:fwd, ease:linear,
    drag:1, spatial:dome, bounds:dome_bounds, osc:1)

// Armed alternate without drag (standard score)
o2p(path:path2, mode:alt, dur:5, loop:0, ease:linear, init:armed)

// Bipolar speed control (center=stop, left=reverse, right=forward)
o2p(path:orbit, dur: follow(speedFader.t, 0.5, 8), bipolar:1, loop:0)

What an o2p publishes

An o2p() is the workhorse source of the control plane — a dot on a path is a fader, a curve reader, a position:

Channel Meaning
t how far along the path, 0–1
x x position within the path's box, 0–1
y y position, 0–1
angle heading, in degrees
p rotation handle, 0–1 (only with hmode)

It takes dur: and rotspeed: as follow() inlets, and being a mover it can also fire a cue it passes over with trig:<uid>.

o2p(path:pFader, trig:touch, uid:fx)
synth(uid:pad, wave:sine, freq: follow(fx.t, 90, 2000), amp:0.1)

Open the connections view to see these as ports, and demo-connections for a worked example.

Demo project

See demo-o2p: travel once vs freeze:1, fwd/rev/alt loops, aligned and spinning rotation on curves, segments, easing, speed profiles, init prestates, and a touch fader streaming OSC.

Section H is the profile: two objects share one ring and both take 6 beats a pass, but one holds a constant speed while the other is cut into quarters by at: and timed dur:[1, 2, 1, 2] — so they part company between the ticks and meet again on every one of them, with a live tvel readout under each. Beside it, a line that leaves from rest and hands over to a constant speed without lurching, and one that takes a different duration on each of its three passes.

http://localhost:8001/?project=demo-o2p

Tip: use ← → or ↑ ↓ to navigate the docs