Skip to content

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 → src
yield 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 form
  • v(type, opts) — generic form; the v_ prefix is added automatically.
  • v.bg(srcOrColor, opts?) — the first argument becomes src when it looks like a URL or path (/..., ./..., http(s)://, data:, or an image extension), otherwise color.
  • 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 form
yield {
type: 'v_bg',
color: 'linear-gradient(160deg, #0b1320, #2a1530)',
fade: 3,
once: true,
}
FieldTypeDefaultDescription
srcstringImage URL. Omit to use color
colorstringdark gradientAny CSS background (color / gradient)
opacitynumber1Layer opacity
fadenumber1Crossfade duration in seconds
fit'cover' | 'contain''cover'Image sizing
kenburnsboolean | objectSlow 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 })
FieldTypeDefaultDescription
srcstringrequiredVideo URL. Audio is always muted
opacitynumber1Layer opacity
fadenumber1Fade-in duration in seconds
loopbooleantrueLoop 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,
})
FieldTypeDefaultDescription
textstringrequiredText to display
idstringautoLayer id. Same id replaces the previous text
x, ynumber50Position in % of the viewport
sizenumber28Font size in px
colorstringtheme steelText color
fontstringtheme monoCSS font-family
animstring'fade''fade', 'slide-up', 'slide-left', 'pop', 'glitch', 'typewriter'
decostring'blocks' (▰▰ TEXT ▰▱), 'corners' (◢◤ / ◥◣), 'rule' (▬▬▬ rules above/below), 'brackets' (【 TEXT 】)
durnumberuntil replacedDisplay duration in seconds
alignstring'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 })
FieldTypeDefaultDescription
opacitynumberrequiredEditor opacity, 01 (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 })
FieldTypeDefaultDescription
target'bg' | stringrequired'bg' or a text id
x, ynumber0Offset in vw/vh
scalenumberScale factor
durnumber1Transition 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 })
PresetBehavior
riseGlowing dots drifting upward from the origin. Loudness increases spawn rate and brightness
sparksBursts of sparks on audio peaks (sudden rises), scattering and decaying
orbitA few orbs circling the center. Loudness pulses the orbit radius and glow
rainThin streaks falling from the top. Loudness increases fall speed and amount
pulseExpanding rings emitted on audio peaks. Loudness controls line thickness
FieldTypeDefaultDescription
presetstringrequiredOne of the presets above
idstringautoSame id replaces the existing layer
x, ynumber50Emission center in % of viewport
colorstringtheme cyanParticle color (glow uses the same hue)
opacitynumber1Opacity of the whole layer
densitynumber1Amount multiplier
speednumber1Velocity multiplier
sizenumber1Particle 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' })
HelperEventRenders
v.spectrum(opts?)v_spectrumLog/linear FFT spectrum bars
v.scope(opts?)v_scopeTime-domain waveform (oscilloscope)
v.meter(opts?)v_meterRMS / peak level bar with peak-hold

Common fields for all three:

FieldTypeDefaultDescription
idstringby orderStable identity across cycles
x, ynumber50Center position in % of viewport
w, hnumber30 / 12Size in % of viewport
colorstringtheme cyanBase color
sourcestringmasterMachine name to analyze (omit for master)
opacitynumber1Layer opacity

Type-specific fields:

HelperFieldTypeDefaultDescription
v.spectrumscale'log' | 'linear''log'Frequency axis scale
v.scopethicknessnumber1.5Waveform line width
v.meterorient'v' | 'h''v'Bar orientation

v_clear — remove visuals

yield v.clear() // everything
yield v.clear('bg') // background + video
yield v.clear('text') // all texts
yield v.clear('particles') // all particle layers
yield v.clear('editor') // restore editor opacity
yield v.clear('lyric') // one text or particle layer by id

Example: 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.