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.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 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 |
weight | number | string | — | CSS font-weight |
italic | boolean | false | Italic text |
uppercase | boolean | false | Transform to uppercase |
letterSpacing | number | 0.12 | Letter spacing in em |
lineHeight | number | 1.2 | Line height |
rotate | number | 0 | Rotation in degrees |
glow | number | 18 | Glow blur radius in px |
glowColor | string | theme cyan | Glow color |
stroke | number | — | Text outline width in px |
strokeColor | string | text color | Text outline color |
gradient | string | — | CSS background clipped to the text (gradient text) |
bg | string | — | Background color of the text box |
padding | number | 8px 16px with bg | Padding in px |
blur | number | — | Blur the text by px |
anim | string | 'fade' | 'fade', 'slide-up', 'slide-left', 'slide-down', 'pop', 'zoom', 'blur', '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.
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 })| Field | Type | Default | Description |
|---|---|---|---|
grade | string | — | CSS filter string passed to backdrop-filter (e.g. 'sepia(0.2) saturate(1.2)') |
blur | number | 0 | Full-frame blur in px |
bloom | number | 0 | Soft highlight haze, 0–1 |
vignette | number | 0 | Corner darkening, 0–1 |
grain | number | 0 | Animated film grain, 0–1 |
scanlines | number | 0 | CRT-style scanlines, 0–1 |
tint | string | — | Color washed over the whole frame |
tintAmount | number | 0 | Tint opacity, 0–1 |
bars | number | 0 | Height 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 field | Type | Default | Description |
|---|---|---|---|
color | string | #ffffff | Flash color |
opacity | number | 1 | Peak opacity |
dur | number | 0.4 | Fade-out time in seconds |
v_shake field | Type | Default | Description |
|---|---|---|---|
intensity | number | 8 | Maximum offset in px |
dur | number | 0.5 | Duration 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' })| 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('post') // color grading / film effectsyield v.clear('flash') // active flashyield v.clear('shake') // active shakeyield 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.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.