Vitrine

No.002 Nimbus

Open the panel

Nimbus is a webgl specimen in this vitrine: one circle warped three dozen times by the same turbulence, stacked into a flowing band of light. It exposes 21 parameters, runs continuously, and was added on 12 August 2026.

Anatomy

KindWebGL
Lifecycleambient
Motioncontinuous:uniform-time
Stage rolesubject
Themeslight · dark
Parameters21
Added2026-08-12

Every parameter, and why it is set where it is

Each of the 21 parameters below is a real control in the panel. The reasoning is first-hand: where a bound comes from a measured property of the work, that measurement is named.

Ground

Ground colour bg

colour, default #000000 (dark) / #0a0a0b (light)

The shells are emissive: the shader accumulates from zero and outputs uBg + acc, so black is not painted, it is the absence of light. That is why it stayed hardcoded through the first extraction — adding zero changes nothing. Exposing it costs one uniform and lets the work sit on a ground other than black; the dot grid flips to darkening above 0.5 luma so it survives light grounds.

When there is a ground at all, it belongs to the work rather than to the room it sits in — but by default there is none (see Ground opacity). The context is created with alpha: false — there is no alpha channel to be transparent in — and uBg feeds the dot polarity, so the ground is something the shader reads rather than something painted behind it. It travels with Copy code, and every export carries it. To place this on your own background, composite the black-ground frame with mix-blend-mode: screen: emissive light adds, and that is the original, not an approximation of it.

Ground opacity bgA

0–1, default 0

The default is 0: this work ships as light on transparency. The shells are emissive, so a ground is something you add, not something you remove — and on a page that already has a background of its own, an opaque black rectangle reads as a hole rather than as the work. At 1 the shader outputs exactly vec4(uBg + acc, 1.0), which is the original rendering as first published; anything between the two dissolves one into the other.

The dot grid goes with it. The dots are the ground reacting to the light, not part of the light — that is what Light sensitivity does — so at 0 they leave along with the surface they sit on. Leaving them behind would hand you a transparent export that is still speckled, and the panel already promised otherwise by grouping them under GROUND.

Two more things follow the slider rather than switching at an endpoint. The dot polarity leans back to additive as the ground leaves — a darkening dot on transparency would have to be negative light, which does not exist. And alpha is groundOpacity + brightest_channel * (1 - groundOpacity): the max channel, not luma, because luma weights blue at 0.114 and would make a deep blue glow export nearly invisible.

⚠️ Video cannot carry this. The recorder fills an opaque background on every frame and the codecs offered (VP8/VP9/AVC) are configured without an alpha plane, so transparency is a still-image capability. For video, put the glow on black and composite with mix-blend-mode: screen — emissive light adds, so that is the original rather than an approximation of it.

Dot spacing dot

6–48 px, default 18

Not yet derived — tuned by eye. See the list below.

Dot size dotSize

0.5–5 px, default 1.6

Not yet derived — tuned by eye. See the list below.

Dot brightness dotAlpha

0–0.3, default 0.045

Not yet derived — tuned by eye. See the list below.

Light sensitivity dotReact

0–4, default 0.9

Not yet derived — tuned by eye. See the list below.

Form

Circle radius scale

0.05–0.42, default 0.18

The radius of the ring in uv units, where the short edge of the canvas is 1. The shader computes R = uScale * (0.72 + 0.28 * uLevel), so at full activity R equals this value directly — at the ceiling of 0.42 the ring is 0.84 of the short edge across, leaving about 8% of margin on each side for the glow, which extends past R. Half the short edge would put the ring itself on the boundary and clip the halo. The exact 0.42 is not forced; the constraint it respects is.

The floor is forced. Frequency is divided by the radius (mix(0.4, 3.0, uFreq) / max(uScale, 0.02)) and that max clamps at 0.02 — below it nothing changes any more. The slider stops at 0.05, above the clamp, so every position on it does something.

Shells shells

1–48, default 30

The ceiling of 48 is hardcoded in the shader: for (int i = 1; i <= 48; i++). GLSL ES 1.0 requires a constant loop bound, so raising this number means editing the shader too — a language constraint, not a taste one. The floor of 1 is deliberate: pulled right down you can see that the halo is only ever a handful of lines.

Motion

Flow speed speed

0–64, default 26.1

The ceiling of 64 was measured, not picked. The four turbulence layers advance at 1x/2x/3x/4x in time, and past a certain speed their per-frame phase advances cancel one another: the form starts to boil rather than to flow faster. Measured, 32 to 48 barely reads as quicker. So 64 marks where the returns have gone, not a limit of the technique.

Spin spin

-0.6–0.6, default 0.05

Not yet derived — tuned by eye. See the list below.

Turbulence amount amp

0–3, default 0.15

Not yet derived — tuned by eye. See the list below.

Turbulence frequency freq

0–1, default 0.12

0–1 is normalised; the shader maps it through mix(0.4, 3.0, uFreq) and then divides by the ring radius. That division is a real fix, not tidiness: without it, radii of 0.12/0.20/0.34 gave 1/2/1 main lobes and the form jumped around as you turned the size. With it, the lobe count holds while the radius changes.

Time trail trail

0–4, default 0.62

Measured in field time (the same units as t inside the shader), not seconds. In seconds the visual length of the trail would change with the speed — measured, at speed 40 a seconds-based trail decorrelates the layers into noise.

Shell phase spread spread

0–1, default 0.38

0–1 normalised, mapped to 0–2π in the shader (mix(0.0, 6.283185, uSpread)). A full turn is every phase arrangement there is; going further only comes back around.

Layer variance variance

0–1, default 0.29

0–1 is the interpolation factor in a *= mix(1.0, max(w.x, w.y), uVariance). At 0 every layer shares one amplitude; at 1 the noise sets it entirely.

Activity level

0–1, default 0.49

0–1 normalised loudness. The real source of this parameter is an external signal — microphone RMS or band energy fed to setLevel(); the slider is only standing in for it. The bounds are the normalisation itself, nothing more.

Palette

Base color color

colour, default #22d3ee (dark) / #0f2ee0 (light)

On a light ground this work moves from cyan to blue, and the reason is arithmetic. The alpha this work exports is its brightest channel, so a colour dark enough to read against white is also nearly transparent — while luminance weights green at 0.72 and blue at only 0.072. Blue is the only hue that can be both almost opaque and dark. Measured: #22d3ee reaches 11.62:1 against black and 1.71:1 against white, which is invisible; the best any colour can do against white under this alpha rule is 8.74:1, at a saturated blue. The light default sits at 6.56:1 and keeps some of the cyan lineage rather than going to a pure primary.

There is only this one colour; every other is derived from it via colorShift. Change the base and the whole palette follows on its own, which is what makes the work reusable — split into three HSL sliders it would lose that property entirely.

Colour shift shift

0–2, default 0.7

The 0.25 factor makes shift = 1 sweep a quarter of the colour wheel. The span is then multiplied by (1 − 1/N), so it moves with the shell count: at shells = 30, shift = 1 gives 87° and shift = 2 gives 174°.

Render

Edge softness edge

0–2, default 0.2

Not yet derived — tuned by eye. See the list below.

Motion blur smooth

0–3, default 1

Not yet derived — tuned by eye. See the list below.

Brightness bright

0–2, default 1

Not yet derived — tuned by eye. See the list below.

What is not derived

9 parameters were tuned by eye. They are listed rather than dressed up: an invented reason would be worse than an admitted gap.

Tuned by eye: spin · amp · edge · smooth · bright · dot · dotSize · dotAlpha · dotReact

Where it comes from

LiveKit Agents UI — AgentAudioVisualizerAura software · confirmed

Four techniques come from it: shell accumulation, sinusoidal turbulence domain warping, per-shell hue offset, and iterative directional motion blur. This implementation was rewritten from the principles rather than carried across as text, and every constant has been re-derived. ⚠️ What has not changed is the composition: the primitive is still a circle and the form is still a centred ring.

Domain warping technique · claimed

Pushing the coordinates through one function and then measuring distance in the pushed coordinates was popularised in real-time graphics by Inigo Quilez. The primary sources have not been verified one by one, which is why this reads claimed rather than confirmed.

Take it with you

Open the panel to change any parameter above and watch it apply immediately. From there the work leaves as a link, as source, as a PNG, or as a video.

Open No.002 in the panel