OSCILLA Cheatsheet

Interactive Graphic Notation

Quick Install

git clone https://git.kompot.si/rob/oscilla.git
cd oscilla; npm install; npm start
Open in browser: http://localhost:8001

Project Structure

oscilla/             # cp template dir
├─ public/           # ⟶ rename OR
│  ├─ scores/        # scroll:40000×1024
│  │  └─ myProject/  # page:1366×1024px
│  │     ├─ score.svg        ⟶ required
│  │     ├─ audio/ video/    ⟶ optional
│  │     ├─ pages/ text/     ⟶ optional
│  │     └─ preferences.json ⟶ generated

Preferences (preferences.json)

projectTitle, projectAuthor, projectDescription, darkMode, defaultPlaybackSpeed, defaultViewMode, defaultPage, playzoneColor, playheadColor, playheadBorder, playheadWidth, audioSync, oscOutput, overlayMode, loopPlayback

Workflow (Inkscape → Oscilla)

1. Author in Inkscape - shapes,paths etc.
- Save: ~/oscilla-projects/myProject/score.svg
2. Add Behaviour
- XML Editor (Ctrl+Shift+X): data-oscilla = pause(dur:12)
- or Object Properties: type it into the id [e.g. pause(dur:12)]
3. Open Project http://localhost:8001 or
- http://localhost:8001/?project=myProject
4. Edit → Save → Refresh

Both work. data-oscilla is where a cue belongs — an id with parens and spaces is not valid SVG, and it is the one attribute that must be unique. The id form exists because Object Properties is quicker than the XML editor; when Oscilla finds it in your project it offers a one-click Normalize that moves the DSL to data-oscilla and leaves the uid: as the id. Behaviour is identical either way, and the bundled demos all ship normalized.

Timing & Navigation

stop(uid:s1) •  stop(next:nav(End))
pause(dur:12, count:true) // with countdown
pause(dur:4, next:nav(page3)) // chaining
speed(value:0.5, uid:s2) // half speed
speed(add:0.1, uid:s3) // a bit faster
speed(value:1.3, dur:2, uid:s4) //ramp
speed(value:1.4, dur:6, ease:linear)
nav(page3) •  nav(scroll@A) // AKA Navigate  
nav(scrollPaused@B) // jump and wait
nav(Coda) // jump to object id Coda
nav(scroll@G, repeats:3, uid:g1) // repeat
page(page1) // page*.svg in pages/ dir
page(Pseq([page1:2,page2:2],3))
page(Prand([pageA,pageB,pageC],4))
page(Pchoose([pageA,pageB]))
stopwatch(source:new, trig:auto)
stopwatch(source:main, scroll:true)
stopwatch(source:new, hold:6, offsetX:-40)
metro(bpm:90, visual:hex, trig:playhead)
metro(bpm:120,position:scrolling,
  target:beat1,uid:scrollA)
metro(bpm:110,beats:3,colour:#3f9,
  audio:1,osc:1,uid:green,trig:auto)
synth(freq:m67, dur:0.3, trig:readPH)   // fired by the object with uid readPH
rotate(uid:r, dur:1, trig:hand, once:1) // once ever

Logic — Conditional Triggering

audio(src:hit.wav, if:"fader1.t>0.7")
scale(dur:2, if:"slider.y>=0.5")
nav(scroll@B, if:"knob.t>0.8")
rotate(dur:3, if:adc.onset) // truthy

Operators: > < >= <= == !=

Reusable fragments

use(name)                                 a reuse() block from this score
use(path:gestures)                        an SVG file from blocks/gestures/
use(path:gestures, index:2)               the third file
use(path:gestures, index:rand(0,3))       a different one each load
use(path:gestures, index:Pseq(0,1,2,inf)) stepped across placeholders

use() INLINES the file, so DSL inside it is live. image(path:…) renders a picture and cannot see inside. Trailing bare number = repeat count: use inf.

Arithmetic on signals

sx: mul(follow(adc.amp, 0, 2), follow(lfo.norm, 0.5, 1))   // product
sx: add(follow(a.t, 0, 1), 0.25)                           // sum, number term
sx: mul(add(follow(a.t), follow(b.t)), follow(c.t))        // they nest

Each term carries its own range. Result clamped to the target's range. A term that has not published yet is skipped, not counted as zero.

Random & threshold triggers

random(uid:dice, min:1, max:8, int:1, trig:"adc.onset>0.5")
image(path:plates, index: follow(dice.norm))   // an attack deals a plate

nav(scroll@B, trig:"adc.amp>0.8")     // trig: CAUSES, if: only gates
                                      // fires on the rising edge; once:1 = first time only

Outlets: dice.value (the range you asked for) · dice.norm (0-1)

Audio Analysis — start it with adc(), or place a mic from the + button:

adc()                  adc(uid:room)      adc(off:1)
adc(ch:l, uid:left)    adc(ch:r, uid:right)    // stereo = two cues, two uids

adc.amp  adc.peak  adc.centroid  adc.onset
adc.low  adc.mid  adc.high
adc.pitch (Hz)  adc.note (MIDI)  adc.pitchConf

Animation — Scale | Rotate | o2p

scale(values:[1,1.5,1], dur:2)
scale(min:1, max:1.3, dur:2, loop:0)
scale(values:[1,2,1], dur:2, tdelay:3)
scaleXY([1,1.3],[1,0.6], dur:1)
scale(Pseq([1,1.4,1],inf), dur:Prand([0.5,1],inf))
scale([1,1.5,1],dur:12, mode:loop, osc:1, 
   oscaddr:"scale/spat/4chPan",  hold:0,  uid:aC2l)
rotate(dir:1, dur:1)
rotate(values:[0,120,240], dur:2)
rotate(values:Pseq([0,45,10],inf), dur:Pseq([1,0.2,2],inf))
rotate(values:[0,120,240], tdelay:2)
rotate(values:Pshuf([0,180],inf), dur:1, mode:alt)

o2p — Object-to-Path Animation

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

Animation — Color | Fade | Tex

color(uid:shape1, vals:[#f00,#0f0], dur:2)
color(uid:bars*, vals:[#f80,#08f], mode:alt, dur:1.2)
color(uid:rect, vals:Pseq([#f00,#ff0,#0ff],3), dur:3)
color(uid:bgStripe, vals:[#000,#444,#888,#ccc], dur:6)

fade() any SVG element (self||target)

fade(mode:out, dur:2, from:1, to:0)
fade(mode:in, dur:1, target:title)
fade(mode:pulse, dur:6, from:0.2, to:1)

ui() show/hide UI & SVG elements

ui("#playhead", opacity:0, dur:0.3)
ui("#playzone", visible:false, dur:0.25)
ui(uid:rot_triangle, visible:false) // SVG anim
ui(uid:rot_pentagon, action:toggle) // toggle
ui(uid:tri, action:toggle, visible:false, click:1) // start hidden

text() — pick | cycle | shuffle | scroll | ticker

text(pick:["A","B","C"], dur:2) // random one
text(cycle:"poem.txt", dur:3) // sequential
text(shuffle:"words.txt", dur:1, loop:0) // random order
text(scroll:"speech.txt", speed:60) // teleprompter
text(ticker:"Breaking news", speed:100) // news crawl
text(cycle:"hello world", unit:word, dur:0.5)
text(shuffle:"HELLO", unit:char, dur:0.2, loop:5)
text(pick:"poem.txt", unit:stanza, dur:5)
text(pick:["A","B"], trig:click, target:self, hold:true)
text(shuffle:"a;b;c", dur:0.5, loop:0, trig:toggle)
text(cycle:"x;y;z", dur:0.8, loop:0, trig:extent)

Audio | Video

audio(...) — Play a Single File

audio(src:noise.wav, 
  loop:2, amp:1, fade:1.5, uid:p8)
audio(src:hit, pan:rand(-1,1), uid:a3, 
  pitch:rand(0.8,1.3),  fadeOut:0.3)

audioPool(...) — 1-Shot Select from Dir

audioPool(uid:p1, path:sfx)
audioPool(uid:p2, path:foley, mode:rand)
audioPool(uid:p3, path:perc, amp:rand(0.4,0.9), 
  pan:rand(-0.7,0.7),pitch:rand(0.5,2),fade:rand(10%,40%))

audioImpulse(...) — Stochastic Process

audioImpulse(uid:i1, path:perc, rate:30)
audioImpulse(uid:i2, path:clicks, rate:20)
audioImpulse(uid:i3, path:metal, rate:12, 
  pan:rand(-1,1), pitch:rand(0.7,1.4))
audioImpulse(uid:i4, path:textures, rate:6, 
  jitter:0.8, amp:rand(0.2,0.6))

video(...) — In-Score Video Playback

video(file:intro.mp4, size:fs, clickable:1)
video(file:lum.webm,size:500,in:4,out:12, 
  opacity:0.5, loop:0, speed:3.2, 
  target:abc123, location:scroll)

Synthesis

synth() — Web Audio Synth, FX, Filters

synth(uid:ref, wave:sine, freq:440)
synth(uid:tune, wave:sine, freq:~A4)
synth(uid:noise, wave:noise)
synth(uid:fixedDur, freq:220, dur:5)
synth(uid:p1, freq:90, lifetime:process)
synth(uid:env, freq:99, env:{a:0.5,r:1})
synth(uid:chord, freq:[440,477,644])
synth(uid:seq, freq:Pseq(220,330), dur:1)
synth(uid:filter, freq:330, 
  filter:{type:lp,freq:Pseq(400,1200)})
synth(uid:osc, freq:330, 
  osc:1, oscAddr:/synth/a)
synthStop(uid:p1, rel:0.5)

OSC / External Control

osc --- Discrete OSC Event Cue

osc(addr:v1, pitch:y) // y | hz(440) | midi(60) | deg(2,4)
osc(addr:v2, pitch:y, uid:v1) // Y coord. normalised

oscCtrl --- Continuous Control Lanes

oscCtrl(addr:"/fx/pan") // acts like breakpoint function // default 0 → 1 
oscCtrl(addr:"/fx/pan", min:-1, max:1, mode:continuous) // mode:event
oscCtrl(addr:"/fx/ring/freq", min:60, max:800) // default 0 → 1 

Control Plane — Bindings

follow --- drive any parameter from any source

freq: follow(fader1.t, 90, 2000)          // source 0-1 -> 90..2000
dur:  follow(r1.norm, 8, 0.5)             // ranges may run high to low
freq: follow(/pedal/1, 90, 2000)          // leading slash = OSC address
cutoff: follow(fx.t, 200, 8000, exp2)     // lin exp2 exp3 exp4 log sqrt

map --- follow with the INPUT range spelled out

map(source, inMin, inMax, outMin, outMax [, default] [, curve])

sx: map(fx.t, 0, 1, 0.5, 2)             // identical to follow(fx.t, 0.5, 2)
sx: map(fx.t, 0.3, 0.35, 0.5, 2)        // a 5% slice -> the whole range
sx: map(transport.t, 0.6, 0.67, 0.02, 1) // full sweep over one stretch

Use it when a source is too slow or too narrow to move the parameter — window it and that slice is stretched across the output. Outside the window the value pins to the nearer end. follow is map with the input range assumed 0-1. link is the old name and still works.

velocity — everything that moves publishes it

obj.vel       // speed, score units/s (rotate: deg/s) -- use map(), it is not 0-1
obj.vx obj.vy // signed components (o2p, trans, drag)
obj.tvel      // rate of t: laps / turns per second, geometry only (not on drag)
amp: map(fader.vel, 0, 400, 0, 1)   cutoff: map(wheel.tvel, 0, 2, 200, 4000)

oscOut / display — take a signal out, or show it

oscOut(addr:/obj/vel, v: map(fader.vel, 0, 500, 0, 1)) // -> /oscilla/obj/vel [v]
oscOut(addr:/obj, x: follow(a.x), y: follow(a.y))          // -> /oscilla/obj [x y]
display(v: map(cyc.tvel, 0, 1, 0, 1440), fmt:"{v} km/h") // on a <text>: rewritten live

system sources — no cue declares these

transport.t  // playhead through the piece, 0-1   transport.elapsed  transport.speed
adc.amp      // audio analysis: peak centroid onset low mid high pitch note

Interaction & Structure

pin — Keep Element at Playhead

pin(30) // standalone pin for 30 seconds
audioImpulse(path:sfx, rate:20, pin:30) // as param
audio(src:drone, loop:0, fade:5, pin:15)
fade(mode:pulse, dur:4, pin:20)

drag — Moveable Score Elements

drag(1) // basic draggable element
drag(1, osc:1) // + send position via OSC
drag(1, osc:/my/addr) // custom OSC address
rotate(dir:1, dur:4) drag(1) // combine cues

button() to Trigger any Cue in Score

button(trigger:nav(page3))
button(trigger:pause(dur:12,count:true))
button(trigger:nav(scroll@A))
button(target:page(home, uid:12),
  style(size:"250x50", label:"Home",
  font:"Arial Black", fontsize:22))

click:1 — Native SVG Clickable Elements

<g id="ui(uid:rot_tri, action:toggle, click:1)">
  <rect fill="#d4af37" .../>
  <text>TRI</text>
</g>

propagate — Group-Level Cue Propagation

propagate( // need multiple cues? use this.
  scale(values:[${1},${2}], dur:${3}),
  rnd(0.8,1.2), rnd(1.2,1.6), rnd(0.4,1.2) )
propagate( // continuous random streams
osc( // all objects in group get the cue
  addr:pontalist, 
  pitch:deg(irand(0,11), irand(0,2)),
  env:size, uid:irand1235 ))
propagate(// ------------------------
    addr:pontalist, // pitch from Y-axis
    pitch:y, env:width, 
    bright:height, density:area,
    trig:playhead ))
propagate( // ------------------------
  osc( // ramdomise inputs with ${1}
    addr:voice,
    pitch:deg(${1}, 3),
    env:size, uid:rnd123
  ), rnd([0,2,4,5,7,9,11]) )

Define and Reuse Collections.

reuse(mainMenu) • use(mainMenu)
reuse(audioSampBtns) • use(audioSampBtns)

Combine Cues by Nesting Groups

// Behaviour stacks outer → inner; shapes inherit all enclosing cues.
<g id="o2p(...)">
  <g id="rotate(...)">
    <g id="propagate( osc(...) )">
      <!-- shapes --> </g></g></g>

Inkscape Extension (Recommended)

- Structured UI for editing cue & animation IDs
-  see docs docs/inkscape_extension/

Project Actions (Quick)

File → New Project, Save Project As…  
File → Import/Export Project (.oscilla)  

Links

Docs: https://oscilla.kompot.si
Repo: https://git.kompot.si/rob/oscilla
Session Layer Cheatsheet: see cheatsheet-session