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.particles(preset, opts?) — audio-reactive particle layers.
  • v.post(opts?), v.flash(opts?), v.shake(opts?) — full-frame color grading, impact flashes, and camera shake (see Post-processing).
  • 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
srcstring—Image URL. Omit to use color
colorstringdark gradientAny CSS background (color / gradient)
opacitynumber1Layer opacity
fadenumber1Crossfade duration in seconds
fit'cover' | 'contain''cover'Image sizing
kenburnsboolean | 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 })
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
weightnumber | string—CSS font-weight
italicbooleanfalseItalic text
uppercasebooleanfalseTransform to uppercase
letterSpacingnumber0.12Letter spacing in em
lineHeightnumber1.2Line height
rotatenumber0Rotation in degrees
glownumber18Glow blur radius in px
glowColorstringtheme cyanGlow color
strokenumber—Text outline width in px
strokeColorstringtext colorText outline color
gradientstring—CSS background clipped to the text (gradient text)
bgstring—Background color of the text box
paddingnumber8px 16px with bgPadding in px
blurnumber—Blur the text by px
animstring'fade''fade', 'slide-up', 'slide-left', 'slide-down', 'pop', 'zoom', 'blur', '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, 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 })
FieldTypeDefaultDescription
target'bg' | stringrequired'bg' or a text id
x, ynumber0Offset in vw/vh
scalenumber—Scale 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.

Post-processing (v_post)

v.post applies color grading and film effects to the entire frame — background, video, particles, text, and the editor UI — via a full-viewport overlay. Use it to give a screen recording a cohesive, cinematic look.

yield v.post({
grade: 'saturate(1.15) contrast(1.05)',
vignette: 0.35, grain: 0.1, scanlines: 0.06, bloom: 0.25,
}, { once: true })
FieldTypeDefaultDescription
gradestring—CSS filter string passed to backdrop-filter (e.g. 'sepia(0.2) saturate(1.2)')
blurnumber0Full-frame blur in px
bloomnumber0Soft highlight haze, 0–1
vignettenumber0Corner darkening, 0–1
grainnumber0Animated film grain, 0–1
scanlinesnumber0CRT-style scanlines, 0–1
tintstring—Color washed over the whole frame
tintAmountnumber0Tint opacity, 0–1
barsnumber0Height of the top/bottom letterbox bars, in % of viewport

The post layer persists until replaced or cleared with v.clear('post'). Because the overlay is backdrop-filter-based, grade and blur affect every pixel behind it, including the editor and header — great for a graded look, but it also means a strong blur will soften the UI.

Impact effects (v_flash / v_shake)

Two discrete accent events for drops, hits, and section changes. Both fade out automatically after dur.

yield v.flash({ color: '#56c9d8', opacity: 0.25, dur: 0.5, beat: 2 })
yield v.shake({ intensity: 6, dur: 0.4, beat: 2 })
v_flash fieldTypeDefaultDescription
colorstring#ffffffFlash color
opacitynumber1Peak opacity
durnumber0.4Fade-out time in seconds
v_shake fieldTypeDefaultDescription
intensitynumber8Maximum offset in px
durnumber0.5Duration in seconds

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('post') // color grading / film effects
yield v.clear('flash') // active flash
yield v.clear('shake') // active shake
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.post({ grade: 'saturate(1.15)', vignette: 0.35, grain: 0.1 }, { 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,
gradient: 'linear-gradient(90deg, #56c9d8, #9d7fd4)',
})
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, uppercase: true,
})
if (ctx.cycle % 4 === 3) {
yield v.flash({ color: '#56c9d8', opacity: 0.22, dur: 0.5, beat: 2 })
yield v.shake({ intensity: 6, dur: 0.4, beat: 2 })
}
const pn = modalPiano({ 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.

Unknown visual event names and invalid payloads are rejected during song evaluation with a descriptive error. Raw invalid visual events are ignored by the stage; Nimbus displays the validation error.

Timed text keeps its original dur deadline when other text is added. Replacing the same id starts a new duration; clear and unmount cancel its timers.

The rain preset pauses emission for speed <= 0. Particles expire after at most 10 seconds of animation time, with a maximum of 512 live particles per layer.