Visuals
Nimbus can render visuals behind the editor, driven by meta events yielded from
your song generator. Yield any object whose type starts with v_ and the
editor’s visual stage (powered by the @scriptedcity/lumos package) picks it up. Use
v_editor to make the editor itself translucent so the visuals show through —
enough to build a simple lyric video alongside your track.
All visual events support the standard meta-event fields beat (fire at a
beat within the cycle, default 0) and once (fire only on the first cycle
after start/swap).
The v() helper
The v global builds visual events for you — it is to visuals what cast()
is to sound. Each helper returns a plain event object, so yielding raw objects
(the “raw form” shown throughout this page) remains equally valid.
yield v.bg('linear-gradient(160deg, #0b1320, #2a1530)', { fade: 3, once: true })yield v.bg('/img/cover.png', { kenburns: true }) // URL/path → srcyield v.video('/clips/rain.mp4', { opacity: 0.6 })yield v.text('NEON LETTER', { id: 'title', anim: 'typewriter' })yield v.editor(0.55, { once: true })yield v.move('bg', { scale: 1.1, dur: 8 })yield v.clear('text')yield v('bg', { color: '#0b1320' }) // generic formv(type, opts)— generic form; thev_prefix is added automatically.v.bg(srcOrColor, opts?)— the first argument becomessrcwhen it looks like a URL or path (/...,./...,http(s)://,data:, or an image extension), otherwisecolor.v.video(src, opts?),v.text(text, opts?),v.editor(opacity, opts?),v.move(target, opts?),v.clear(target?).v.spectrum(opts?),v.scope(opts?),v.meter(opts?)— placeable audio-analysis visualizers (see Audio analyzers).
v_bg — background
Sets the background layer. New backgrounds crossfade over the previous one.
yield v.bg('linear-gradient(160deg, #0b1320, #2a1530)', { fade: 3, once: true })
// raw formyield { type: 'v_bg', color: 'linear-gradient(160deg, #0b1320, #2a1530)', fade: 3, once: true,}| Field | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL. Omit to use color |
color | string | dark gradient | Any CSS background (color / gradient) |
opacity | number | 1 | Layer opacity |
fade | number | 1 | Crossfade duration in seconds |
fit | 'cover' | 'contain' | 'cover' | Image sizing |
kenburns | boolean | object | — | Slow pan/zoom. true for a default drift, or { from: {x,y,scale}, to: {x,y,scale}, dur } (x/y in %, dur in seconds) |
v_video — background video
yield v.video('/clips/rain.mp4', { opacity: 0.6, loop: true })| Field | Type | Default | Description |
|---|---|---|---|
src | string | required | Video URL. Audio is always muted |
opacity | number | 1 | Layer opacity |
fade | number | 1 | Fade-in duration in seconds |
loop | boolean | true | Loop playback |
v_text — animated text
Displays a text layer (lyrics, titles, captions). Texts are keyed by id:
yielding the same id replaces the previous text and replays the animation.
Without an id, each event creates a new auto-numbered layer.
yield v.text('streetlight hum, a tape rewinds', { id: 'lyric', anim: 'slide-up', deco: 'rule', y: 68, beat: 2,})| Field | Type | Default | Description |
|---|---|---|---|
text | string | required | Text to display |
id | string | auto | Layer id. Same id replaces the previous text |
x, y | number | 50 | Position in % of the viewport |
size | number | 28 | Font size in px |
color | string | theme steel | Text color |
font | string | theme mono | CSS font-family |
anim | string | 'fade' | 'fade', 'slide-up', 'slide-left', 'pop', 'glitch', 'typewriter' |
deco | string | — | 'blocks' (▰▰ TEXT ▰▱), 'corners' (◢◤ / ◥◣), 'rule' (▬▬▬ rules above/below), 'brackets' (【 TEXT 】) |
dur | number | until replaced | Display duration in seconds |
align | string | 'center' | 'left', 'center', 'right' |
v_editor — editor opacity
Sets the opacity of the editor pane itself (text included). The editor stays fully opaque until you yield this event.
yield v.editor(0.55, { once: true })| Field | Type | Default | Description |
|---|---|---|---|
opacity | number | required | Editor opacity, 0–1 (1 = opaque) |
yield v.clear('editor') (or a full v.clear()) restores full opacity.
v_move — pan / zoom
Moves the background or a text layer by id.
yield v.move('lyric', { y: -10, scale: 1.2, dur: 4 })yield v.move('bg', { scale: 1.1, dur: 8 })| Field | Type | Default | Description |
|---|---|---|---|
target | 'bg' | string | required | 'bg' or a text id |
x, y | number | 0 | Offset in vw/vh |
scale | number | — | Scale factor |
dur | number | 1 | Transition duration in seconds |
v_particles — audio-reactive particles
Layered above the background (and video) but below texts, particle layers
react to the main audio output in real time. Layers with the same id
replace each other; omitting id stacks new layers.
yield v.particles('rise', { y: 80, color: '#56c9d8', opacity: 0.6, once: true })yield v.particles('sparks', { id: 'hit', react: 'bass', density: 1.5 })| Preset | Behavior |
|---|---|
rise | Glowing dots drifting upward from the origin. Loudness increases spawn rate and brightness |
sparks | Bursts of sparks on audio peaks (sudden rises), scattering and decaying |
orbit | A few orbs circling the center. Loudness pulses the orbit radius and glow |
rain | Thin streaks falling from the top. Loudness increases fall speed and amount |
pulse | Expanding rings emitted on audio peaks. Loudness controls line thickness |
| Field | Type | Default | Description |
|---|---|---|---|
preset | string | required | One of the presets above |
id | string | auto | Same id replaces the existing layer |
x, y | number | 50 | Emission center in % of viewport |
color | string | theme cyan | Particle color (glow uses the same hue) |
opacity | number | 1 | Opacity of the whole layer |
density | number | 1 | Amount multiplier |
speed | number | 1 | Velocity multiplier |
size | number | 1 | Particle size multiplier |
react | 'level' | 'bass' | 'treble' | 'level' | Frequency band the layer reacts to |
Without audio playing, layers stay in a calm idle animation.
Audio analyzers
v.spectrum, v.scope, and v.meter place real-time audio-analysis
visualizers anywhere on the stage. Unlike the full-screen particle layers,
each is a sized rectangle you position with x/y (center) and w/h
(size), all in % of the viewport.
Analyzers are declarative: each cycle the stage shows exactly the set of
analyzers your generator yields that cycle. Because the generator body runs
every cycle, yield v.spectrum(...) at the top keeps it on screen
continuously — and the moment you stop yielding it (e.g. after an APPLY that
removes the line), it disappears. There is no state to clear by hand. Give an
analyzer an id to keep a stable identity across cycles (this preserves a
meter’s peak-hold and avoids re-initialization); without an id, analyzers are
matched by their order in the yield sequence.
By default each analyzer reflects the master output. Set source to a
machine name (the machine’s name, e.g. 'kick', 'vasynth') to analyze just
that track instead.
yield v.spectrum({ x: 50, y: 85, w: 60, h: 18, color: '#9d7fd4' })yield v.scope({ id: 'osc', y: 20, source: 'vasynth' })yield v.meter({ x: 92, y: 50, w: 4, h: 50, source: 'kick' })| Helper | Event | Renders |
|---|---|---|
v.spectrum(opts?) | v_spectrum | Log/linear FFT spectrum bars |
v.scope(opts?) | v_scope | Time-domain waveform (oscilloscope) |
v.meter(opts?) | v_meter | RMS / peak level bar with peak-hold |
Common fields for all three:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | by order | Stable identity across cycles |
x, y | number | 50 | Center position in % of viewport |
w, h | number | 30 / 12 | Size in % of viewport |
color | string | theme cyan | Base color |
source | string | master | Machine name to analyze (omit for master) |
opacity | number | 1 | Layer opacity |
Type-specific fields:
| Helper | Field | Type | Default | Description |
|---|---|---|---|---|
v.spectrum | scale | 'log' | 'linear' | 'log' | Frequency axis scale |
v.scope | thickness | number | 1.5 | Waveform line width |
v.meter | orient | 'v' | 'h' | 'v' | Bar orientation |
v_clear — remove visuals
yield v.clear() // everythingyield v.clear('bg') // background + videoyield v.clear('text') // all textsyield v.clear('particles') // all particle layersyield v.clear('editor') // restore editor opacityyield v.clear('lyric') // one text or particle layer by idExample: lyric video
function* song(ctx) { yield { type: 'tempo', bpm: 84, once: true }
yield v.bg('linear-gradient(165deg, #0b1320, #2a1530)', { fade: 3, once: true }) yield v.editor(0.55, { once: true }) yield v.text('NEON LETTER', { id: 'title', anim: 'typewriter', deco: 'corners', y: 26, size: 40, dur: 14, once: true, })
const LYRICS = [ 'streetlight hum, a tape rewinds', 'your name in vapor on the glass', ] yield v.text(LYRICS[ctx.cycle % LYRICS.length], { id: 'lyric', anim: 'slide-up', deco: 'rule', y: 68, })
const pn = piano({ gain: 0.3 }) yield cast(pn, scales(4, 'F4:dorian'), seq(4, '0,_,2,_,4,_,_,_'), ctx)}The Neon Letter (MV) preset in the editor is a complete version of this
example.