Everything behind the wall panel's voice visuals, written as a single brief you can paste into another AI: the shared engine, all nineteen skins, the Home Assistant chooser and the tests. It is long on purpose. It carries the exact numbers and the lessons that took several rounds to get right.
Copy it with the button and send it to a coding-capable AI as one message. Trimming it loses the numbers that make the visuals behave.
02
Let it build in three stages
First the engine and test harness with Lens Flares and Classic VU, then the other skins in batches of four or five, then the Home Assistant side. The finished bundle here is about 175 KB, far more than any AI writes in one reply.
03
Review the stills after each batch
The prompt asks for every skin rendered at 2.3 s (listening), 4.3 s (thinking) and 7.7 s (replying) of a demo conversation. An AI that can see images checks them itself; one that can't will ask you for screenshots.
3,504 words · copies as plain text
Voice Visuals: build brief
You're building Voice Visuals, a full-screen, audio-reactive visual for a wall-mounted Home Assistant panel. Whenever someone talks to the house's voice assistant (she's called Luna), the panel shows an animated visual that reacts to the person's voice while they speak, to Luna's voice while she replies, and settles into a calm "thinking" motion in between. The conversation transcript sits on top as large, readable text. There are 19 selectable styles ("skins") plus Surprise me (a different skin each conversation), all running on one shared engine.
Aim for premium and cinematic: soft bloom, light that merges and glows, smooth motion, nothing garish or jittery. Every skin should look good as a still frame and feel alive in all four states.
Hard constraints
One self-contained JavaScript file: no frameworks, no libraries, no WebGL. Draw with Canvas 2D; the text layer is DOM + CSS.
It runs in a kiosk Chromium browser on a laptop screen, capped at 30 fps. Each skin must draw a frame in under about 5 ms of main-thread time at 1280×720 on a mid-range laptop; most should manage 0.3–2.5 ms.
Resolution-independent: size everything from the canvas, with u = min(w, h) / 800 as the unit for line widths and particle sizes.
Never allocate canvases or large arrays per frame; cache offscreen buffers and reuse them.
Deterministic: use a seeded RNG for layouts (stars, skylines, particle seeds) so a given moment always renders the same frame.
States and colour language
The voice satellite reports four states: idle, listening (the person is talking), processing (Luna is thinking) and responding (Luna is speaking).
Cyan/blue = you. Magenta, rose and violet = Luna. Gold/amber = thinking.
The four film skins (16–19) keep their films' own palettes, but still tell you, Luna and thinking apart.
Cross-fade palettes between states over roughly a second with smoothed mix values; never snap.
The overlay fades in when a conversation starts and fades out about 5 s after it ends.
Audio analysis (shared by every skin)
Two live sources:
Your voice: the panel's microphone through getUserMedia and an AnalyserNode, giving a level, a 32-band spectrum and a 256-sample waveform every frame. The page must be served over HTTPS for mic access.
Luna's voice: fetch the actual reply audio file the TTS engine produced, decodeAudioData it, and analyse it offline into 20 ms frames (RMS envelope + 32 bands). Then play the analysis back in step with the speaker, which starts about 0.3 s after the file is ready.
Spectrum: FFT size 512 with a Hann window, 32 log-spaced bands from 80 Hz to 7 kHz. Each band is the largest FFT magnitude in its bin range, converted to dB, then normalised per clip so the 98th-percentile dB maps to 1.0 and 55 dB below that maps to 0. The bands are therefore on a dB scale (0.64 is about 20 dB down), and speech keeps most bands fairly high. Meter-style skins have to remap them (see Digital VU).
Fallbacks: with no spectrum, synthesise a speech-shaped one from the level (humps around bands 6, 14 and 22, modulated by noise). With no audio at all, show an organic idle texture.
Per-frame input. A frame builder turns the raw values into one object that every skin receives:
t in seconds, dt clamped to at most 0.1, state, stateAge
level: fast smoothed loudness 0–1 (attack 0.2, release 0.08 per 30 fps frame; 0.5 / 0.25 when the source is the pre-analysed reply)
wave[256]: the current waveform, synthesised from the bands when there isn't one
onset: true on a syllable, meaning voice > 0.22, voice > 1.35 × the mean of the last 10 frames + 0.06, and at least 130 ms since the previous onset
boost: 1.25 while responding, otherwise 1
All smoothing is frame-rate independent: follow(cur, target, up, down, dt) = cur + (target − cur) × (1 − (1 − k)^(dt × 30)), where k is up when rising and down when falling.
layout puts the transcript where it won't cover the skin's focal point: full (centred over everything), left (text in the left ~56%, visual on the right), bottom (text in the bottom 30%) or top (text in the top 34%). LCARS adds its own lcars layout, placing the text in the console frame's upper panel (left 22%, top 17%, at most 36% tall).
text picks a text theme: default (white with a soft shadow), warm (cream and amber, for the analogue meters) or terminal (green monospace with a "> " status prompt, a blinking cursor and a "you: " prefix, for the oscilloscope). The film skins add mother (uppercase aqua monospace, a "> " before the question and a blinking block cursor after the status), tron (cyan glow for you, a warm orange glow for Luna) and lcars (flat, condensed capitals in the LCARS palette: orange status, blue question, peach reply; use a condensed system font such as Bahnschrift or Arial Narrow rather than loading a web font on the panel).
hiDpi renders at the device pixel ratio. Use it only for skins with fine lines.
The renderer r provides w, h, u, ctx, S (a fresh state object each time the skin is activated), thumb (true in gallery thumbnails; halve particle counts), quality (0.5–1), buf(name, w, h) (a cached offscreen canvas), scene() (a full-frame buffer), bloom(src, amount, radius, passes) and present(scene, bloomAmount, bloomRadius, passes).
Bloom: downsample the source 4×, blur it with the canvas filter: blur(), and add it back with lighter compositing. Each extra pass runs at half the resolution, twice the radius and a lower weight.
If a skin throws, log it once and fall back to Lens Flares for the rest of that conversation.
Text layer
DOM over the canvas: a status line (letter-spaced uppercase, such as "LISTENING"), the person's words (smaller and dimmer) and Luna's reply (larger). Replies over 150 characters drop a size. Use clamp() font sizes (for example clamp(24px, 3.8vw, 48px) for the reply in full and left, smaller in bottom and top) and strong text shadows so the words stay legible over bright frames.
The 19 skins
Each entry gives the layout, the look, and what drives it.
Lens Flares (full): the original. About 16 tall anamorphic light streaks and soft bokeh discs drifting across black, blue for you and rose-pink for Luna, under a heavy soft bloom. The fast level lifts the streaks' height and brightness; the slow level sets the drift and the bloom.
Classic VU (bottom · warm · hiDpi): two backlit analogue VU meters side by side in a dark studio panel, MIC for you and LUNA for her. Cream dials, a red zone above 0 VU, a red peak lamp above +0 VU, and an amber lamp while she thinks. Use real ballistics: deflection is linear in amplitude (the dB marks sit at 10^(dB/20)/1.413 of full scale), and the needle is a second-order spring (ζ = 0.81, ωn = 18.9 rad/s, giving about a 300 ms rise and 1.3% overshoot, as the VU standard specifies).
Aurora (bottom): northern-lights curtains over a silhouette of pines and a deep-blue starry sky. Green curtains for you, violet for Luna; each voice brightens its own curtains, and every syllable sends a ripple running along them. Add small folds along the hem and a vertical ray texture.
Mercury (bottom): liquid-chrome metaballs. A central drop swells with loudness, the bands size the orbiting droplets, syllables fling new drops that fly out and flow back to merge, and thinking pulls everything into one gold drop. The technique matters. Compute a field on a low-res grid (about H/3 rows) as Σ (1 − d²/R²)³ with R = 2.2 r. The surface is where F > 0.5, and the height is √(F − 0.5) × 1.41 × (Σ c·r / Σ c). That gives a near-perfect sphere for a lone drop and a smooth bulge where drops join. The usual Σ r²/d² field goes flat where drops merge and makes banded reflections. Shade with a smooth studio environment map (soft top light, a low curved horizon as if the camera sits slightly above it), a key light, and a cyan or magenta fresnel rim.
Ribbons (top): five silky ribbons of light that twist and cross, like a luxurious Siri waveform. Additive colours melt to white where they overlap. Five band groups set the five ribbons' amplitudes, and syllables shed sparks.
Galaxy (left): a tilted spiral galaxy of about 2,600 star particles (17% bulge, 63% in two logarithmic arms, 20% faint disc), centred at two-thirds width. Loudness spins it up and brightens the arms; every syllable sends a shockwave rippling out through them.
Halo (bottom): the 32 bands mirrored into 72 radial bars around a glowing ring, inside a zoom-feedback tunnel (each frame, redraw the previous one slightly enlarged and faded, at half resolution). Syllables flash a shockwave outward and throw sparks; a spinner arc runs while she thinks.
Pulsar (bottom · hiDpi): the "Unknown Pleasures" pulsar plot, live. 54 stacked lines of 80 points; every 55 ms the current spectrum becomes a new ridge, so the plot scrolls through the last three seconds of voice. Draw back to front, filling black beneath each line's own curve (hidden-line removal) before stroking it.
Phosphor (bottom · terminal): a vintage green P31 oscilloscope with bezel, graticule and panel labels. Listening: a triggered Y-T trace of your waveform. Responding: an X-Y figure of Luna's signal plotted against itself delayed by a quarter window, slowly rotating. Thinking: a 3:2 Lissajous knot turning over. Beam brightness is inversely proportional to beam speed (slow parts burn bright), with phosphor persistence and glow.
Tesla (bottom): a plasma globe on a pedestal. Lightning filaments crawl from the electrode to the glass; loudness sets how many burn (5 up to 16) and how thick they are, and syllables crackle off side branches with a flash. Violet-blue for you, magenta for Luna, gold while thinking.
Luna Orb (bottom): a slowly turning sphere of 1,500 points on a Fibonacci lattice, with a nearly edge-on, Saturn-like ring. The bands shape the sphere by latitude, syllables send ripples across its surface, and loudness lights "plexus" lines from each point to its two nearest neighbours.
Digital VU (bottom · hiDpi): an LED spectrum analyser with 32 columns × 22 segments, running green → lime → yellow → orange → red from bottom to top. Unlit segments glow at about 15% of their colour, and red peak caps hold for 0.6 s and then fall. Everything is mirrored in a black glass floor (a clear reflection that fades out quickly, plus a faint glass edge line). Remap the bands as (b − 0.28) / 0.72 with a slight treble tilt, so speech sits mid-meter and only loud syllables reach red. LED ballistics: near-instant attack, linear release of about 1.3 × full height per second. A scanner sweeps across while she thinks, with a low shimmer when idle.
Paint Splash (left): liquid paint thrown on every syllable, inspired by the "paint splash" OLED screensaver on ASUS laptops. Each splash is a core blob, 3–6 arms (chains of shrinking blobs) and a spray of small droplets; sustained loud speech adds extra spray. Render the paint as coloured metaballs using Mercury's kernel, where each pixel's colour is the blobs' colours weighted by the square of their field contribution (distinct paints with soft seams). Shade it as glossy wet paint: diffuse light, a tight Blinn highlight, and darker creases where blobs meet. Blobs slow with drag, then shrink and leave a faint stain that fades. Cool paints for you, hot paints for Luna, and a slow swirl while she thinks. Cap it at about 280 blobs.
Fireworks (bottom): a night sky over a city skyline with lit windows and a harbour. Every syllable launches a rocket; its strength sets how high it climbs and how big it bursts (peony, ring or willow shells), loudness sets the pace of the barrage, and gold shells fly while she thinks. Sparks fly with drag and gravity into a fading trail buffer (long streaks) and glitter as they die. Each burst flashes the sky and lights the skyline, and the harbour shows a squashed reflection. Cap it at about 1,800 sparks, and batch sparks into Path2D objects by colour and alpha bucket.
Synthwave (top): an 80s neon sunset. A striped yellow-to-pink sun whose stripes thicken with the bass; two mountain ridges whose profile is the spectrum mirrored out from the sun, leaving a gap for the sun, with neon edge lines; and a perspective grid that rushes toward you faster as the voice gets louder. Each syllable sends a bright wave rolling down the grid, with haze at the horizon. Cyan edges for you, pink for Luna, orange while thinking.
HAL 9000 (left · hiDpi): the red eye from *2001: A Space Odyssey*. A tall brushed-aluminium panel on the right that runs off the top and bottom of the screen, a black "HAL 9000" nameplate, and a lens in a machined chrome ring (a conic gradient bands it like turned metal) and a black bezel. The eye is a radial gradient from a pale-yellow core through orange and red to near-black at the rim, its hot centre growing with intensity. Over it sits a cached glass layer: faint concentric lens elements, curved fisheye reflections of windows and ceiling lights, a specular highlight and a dark edge. Only the eye blooms, and a little red spill catches the chrome. Intensity follows Luna's loudness while she speaks and syllables flicker the core. Your voice adds a cool cyan rim light; while she thinks, the eye breathes slowly and its centre turns amber.
Mother (left · mother · hiDpi): MU/TH/UR 6000 from *Alien*. On the left, a phosphor CRT terminal (aqua #8cffd6, scanlines, vignette, a little flicker and persistence) whose header reads "MU/TH/UR 6000" and then types "INTERFACE 2037 READY FOR INQUIRY" at about 34 characters a second at the start of every conversation, with a block cursor. Its foot traces the live waveform, or fills a progress row of blocks labelled "COMPUTING" while she thinks. On the right, a wall of 3 × 5 dark panels of 5 × 4 lamps (about two-thirds white, a quarter amber, the rest red), dark sockets drawn once, lit ones drawn as glow sprites. Each lamp toggles at its own rate, faster as the voice gets louder and as its column's band gets stronger; every syllable throws a cluster on. While she thinks, a diagonal wave sweeps the wall. Tint the lit lamps cool for you, rose for Luna and gold while thinking with a source-atop fill over the lamp buffer.
Light Cycles (top · tron · hiDpi): the Grid from *Tron*. A static perspective floor (camera 14 units up, horizon at 38% of the height, the floor at z = 10 meeting the bottom edge) with a glowing arena outline and a grid that fades toward the horizon, all drawn once. Two cycles, cyan for you and orange for Luna, ride the arena. Whoever is speaking rides at 6 + 14 × level units a second and turns 90° on their syllables (at most every 0.32 s); the other turns on a 1–3 s timer. Before hitting the edge, turn toward the side with room. Each cycle samples its position 30 times a second into a trail with a height of 0.5 + 6 × level, so the wall behind it is the voice's loudness over time, a waveform in 3-D. Draw the trail as translucent quads plus bright top and floor edges, bucketed by age into five paths, fading over six seconds. The cycle itself is a hot streak with a glow sprite and a flattened pool of light on the floor. While she thinks, both slow down and gold rings roll out across the floor.
LCARS (lcars · lcars · hiDpi): the Starfleet console from *Star Trek: The Next Generation*. A black screen framed by an orange top elbow and a lavender bottom elbow (a large outer radius and a small inner fillet, drawn with arcTo), five sidebar blocks in lavender, peach, blue, orange and tan with black number labels, top and bottom bars of coloured segments (the bottom one ends in a rounded cap) and the title "VOICE INTERFACE" in orange at the top right. In the lower half of the frame, 28 rounded bars on dark tracks show the spectrum, mapped as on the Digital VU: blue and sky-blue for you, lavender and pink for Luna, a gold chase while she thinks, dim tan when idle. A state word (RECEIVING, TRANSMITTING, COMPUTING, STANDBY) sits above the bars on the right. Syllables flash a sidebar block white and roll the numbers in the bottom bar; while she thinks, the blocks and top segments light in sequence. The frame is drawn once and the highlights go on top of it.
Where it runs (Home Assistant)
If you're only building the visuals, skip this section and deliver the engine, skins, gallery and test harness.
The chosen skin lives in an input_select helper holding the 19 names plus "Surprise me". Switch skins only between conversation turns, never mid-sentence.
The engine is added to the dashboard as a Lovelace JavaScript resource and takes over only the wall-panel dashboard's path.
Inputs from Home Assistant: the voice satellite's state, the "last heard" and "last reply" transcript sensors, the reply audio URL from the Assist pipeline's debug events (tts_output), and a microphone room-level sensor as a fallback level source. Keep every entity ID in one config block.
Home Assistant serves /local files with a month-long cache, so ship a tiny, stable loader at the registered resource URL. It fetches version.json with cache: "no-store", loads voice-visuals.js?v=<version>, re-checks every 5 minutes, and hot-swaps a new build when no conversation is running.
A gallery page: a live thumbnail of every skin playing a demo conversation, a full-size preview on tap, and a "Use this visual" button that sets the helper.
Debug switches in localStorage: force the overlay on or off, an on-screen readout (skin, mic level, reply level, ms per frame), and an fps cap.
Demo mode and testing (build these first)
Embed the analysis (not the audio) of one real exchange as base64 frames of [envelope + 32 bands] every 20 ms, plus a scripted timeline: 0.6–3.6 s listening ("Hey Luna, can you show me something beautiful?"), 3.6–4.9 s thinking, and 4.9–14.1 s responding ("Of course. Here's a little light show: nineteen different looks, all dancing to my voice. Pick your favourite, and I'll wear it every time we talk."). The gallery and the tests both run on it.
test.html?skin=<id>&t=<seconds>&run=0 renders a deterministic frame at time t, after simulating the preceding 3–4 s at 30 fps so trails and physics have history. Render every skin at t = 2.3 (listening), 4.3 (thinking) and 7.7 (responding) and look at the images (or send them to me). Fix anything blown out to white, empty, static, cluttered or hidden behind the text.
Measure the average ms per frame in each state at 1280×720, and fix anything over budget.
Build step: concatenate the source files, escape non-ASCII characters, syntax-check the output, and write version.json.
Lessons already learned (apply these from the start)
Additive light saturates fast. Check the loud responding state for white-out: the first Halo became a solid white disc, and Ribbons blew out in the middle. Keep alphas and bloom modest and let the bloom supply the glow.
Use the bounded metaball kernel above; the classic one flattens and bands.
Meter-style skins must remap the dB-scaled bands, or every column sits pinned at the top.
Seed time-based state on the first draw, not in init. In the live overlay t is the page's uptime, so blobs born at t = 0 expire instantly.
Pre-render static parts (skies, skylines, dials, bezels) once in init, use fixed-size sprite buffers, and never resize a buffer every frame.
Every skin must look intentional when idle and while thinking, not only while someone speaks.
Deliverables, in this order
The engine: audio analysis, frame builder, renderer with bloom, text layer, demo data and test harness, plus Lens Flares and Classic VU.
The remaining skins in batches of four or five. After each batch, render the stills, review them honestly and tune.
The Home Assistant overlay, loader, input_select wiring and gallery.
Output: voice-visuals.js (one file), loader.js, gallery.html, test.html, version.json and the build script.
# Voice Visuals: build brief
You're building **Voice Visuals**, a full-screen, audio-reactive visual for a wall-mounted Home Assistant panel. Whenever someone talks to the house's voice assistant (she's called **Luna**), the panel shows an animated visual that reacts to the person's voice while they speak, to Luna's voice while she replies, and settles into a calm "thinking" motion in between. The conversation transcript sits on top as large, readable text. There are **19 selectable styles ("skins")** plus **Surprise me** (a different skin each conversation), all running on one shared engine.
Aim for premium and cinematic: soft bloom, light that merges and glows, smooth motion, nothing garish or jittery. Every skin should look good as a still frame and feel alive in all four states.
## Hard constraints
- One self-contained JavaScript file: no frameworks, no libraries, no WebGL. Draw with Canvas 2D; the text layer is DOM + CSS.
- It runs in a kiosk Chromium browser on a laptop screen, capped at 30 fps. Each skin must draw a frame in under about 5 ms of main-thread time at 1280×720 on a mid-range laptop; most should manage 0.3–2.5 ms.
- Resolution-independent: size everything from the canvas, with `u = min(w, h) / 800` as the unit for line widths and particle sizes.
- Never allocate canvases or large arrays per frame; cache offscreen buffers and reuse them.
- Deterministic: use a seeded RNG for layouts (stars, skylines, particle seeds) so a given moment always renders the same frame.
## States and colour language
The voice satellite reports four states: `idle`, `listening` (the person is talking), `processing` (Luna is thinking) and `responding` (Luna is speaking).
- Cyan/blue = you. Magenta, rose and violet = Luna. Gold/amber = thinking.
- The four film skins (16–19) keep their films' own palettes, but still tell you, Luna and thinking apart.
- Cross-fade palettes between states over roughly a second with smoothed mix values; never snap.
- The overlay fades in when a conversation starts and fades out about 5 s after it ends.
## Audio analysis (shared by every skin)
Two live sources:
1. **Your voice:** the panel's microphone through `getUserMedia` and an `AnalyserNode`, giving a level, a 32-band spectrum and a 256-sample waveform every frame. The page must be served over HTTPS for mic access.
2. **Luna's voice:** fetch the actual reply audio file the TTS engine produced, `decodeAudioData` it, and analyse it offline into 20 ms frames (RMS envelope + 32 bands). Then play the analysis back in step with the speaker, which starts about 0.3 s after the file is ready.
Spectrum: FFT size 512 with a Hann window, **32 log-spaced bands from 80 Hz to 7 kHz**. Each band is the largest FFT magnitude in its bin range, converted to dB, then normalised per clip so the 98th-percentile dB maps to 1.0 and 55 dB below that maps to 0. The bands are therefore on a **dB scale** (0.64 is about 20 dB down), and speech keeps most bands fairly high. Meter-style skins have to remap them (see Digital VU).
Fallbacks: with no spectrum, synthesise a speech-shaped one from the level (humps around bands 6, 14 and 22, modulated by noise). With no audio at all, show an organic idle texture.
**Per-frame input.** A frame builder turns the raw values into one object that every skin receives:
- `t` in seconds, `dt` clamped to at most 0.1, `state`, `stateAge`
- `level`: fast smoothed loudness 0–1 (attack 0.2, release 0.08 per 30 fps frame; 0.5 / 0.25 when the source is the pre-analysed reply)
- `slow`: slow smoothed loudness (attack 0.08, release 0.03)
- `voice`: instantaneous loudness 0–1
- `bands[32]`: smoothed spectrum (attack 0.6, release 0.22)
- `wave[256]`: the current waveform, synthesised from the bands when there isn't one
- `onset`: true on a syllable, meaning voice > 0.22, voice > 1.35 × the mean of the last 10 frames + 0.06, and at least 130 ms since the previous onset
- `boost`: 1.25 while responding, otherwise 1
All smoothing is frame-rate independent: `follow(cur, target, up, down, dt) = cur + (target − cur) × (1 − (1 − k)^(dt × 30))`, where k is `up` when rising and `down` when falling.
## Engine API
Each skin registers itself:
```js
register({ id, name, layout, text, hiDpi, blurb, init(r), draw(r, f) })
```
- `layout` puts the transcript where it won't cover the skin's focal point: `full` (centred over everything), `left` (text in the left ~56%, visual on the right), `bottom` (text in the bottom 30%) or `top` (text in the top 34%). LCARS adds its own `lcars` layout, placing the text in the console frame's upper panel (left 22%, top 17%, at most 36% tall).
- `text` picks a text theme: `default` (white with a soft shadow), `warm` (cream and amber, for the analogue meters) or `terminal` (green monospace with a "> " status prompt, a blinking cursor and a "you: " prefix, for the oscilloscope). The film skins add `mother` (uppercase aqua monospace, a "> " before the question and a blinking block cursor after the status), `tron` (cyan glow for you, a warm orange glow for Luna) and `lcars` (flat, condensed capitals in the LCARS palette: orange status, blue question, peach reply; use a condensed system font such as Bahnschrift or Arial Narrow rather than loading a web font on the panel).
- `hiDpi` renders at the device pixel ratio. Use it only for skins with fine lines.
- The renderer `r` provides `w`, `h`, `u`, `ctx`, `S` (a fresh state object each time the skin is activated), `thumb` (true in gallery thumbnails; halve particle counts), `quality` (0.5–1), `buf(name, w, h)` (a cached offscreen canvas), `scene()` (a full-frame buffer), `bloom(src, amount, radius, passes)` and `present(scene, bloomAmount, bloomRadius, passes)`.
- Bloom: downsample the source 4×, blur it with the canvas `filter: blur()`, and add it back with `lighter` compositing. Each extra pass runs at half the resolution, twice the radius and a lower weight.
- If a skin throws, log it once and fall back to Lens Flares for the rest of that conversation.
## Text layer
DOM over the canvas: a status line (letter-spaced uppercase, such as "LISTENING"), the person's words (smaller and dimmer) and Luna's reply (larger). Replies over 150 characters drop a size. Use `clamp()` font sizes (for example `clamp(24px, 3.8vw, 48px)` for the reply in `full` and `left`, smaller in `bottom` and `top`) and strong text shadows so the words stay legible over bright frames.
## The 19 skins
Each entry gives the layout, the look, and what drives it.
1. **Lens Flares** (full): the original. About 16 tall anamorphic light streaks and soft bokeh discs drifting across black, blue for you and rose-pink for Luna, under a heavy soft bloom. The fast level lifts the streaks' height and brightness; the slow level sets the drift and the bloom.
2. **Classic VU** (bottom · warm · hiDpi): two backlit analogue VU meters side by side in a dark studio panel, **MIC** for you and **LUNA** for her. Cream dials, a red zone above 0 VU, a red peak lamp above +0 VU, and an amber lamp while she thinks. Use real ballistics: deflection is linear in amplitude (the dB marks sit at 10^(dB/20)/1.413 of full scale), and the needle is a second-order spring (ζ = 0.81, ωn = 18.9 rad/s, giving about a 300 ms rise and 1.3% overshoot, as the VU standard specifies).
3. **Aurora** (bottom): northern-lights curtains over a silhouette of pines and a deep-blue starry sky. Green curtains for you, violet for Luna; each voice brightens its own curtains, and every syllable sends a ripple running along them. Add small folds along the hem and a vertical ray texture.
4. **Mercury** (bottom): liquid-chrome metaballs. A central drop swells with loudness, the bands size the orbiting droplets, syllables fling new drops that fly out and flow back to merge, and thinking pulls everything into one gold drop. The technique matters. Compute a field on a low-res grid (about H/3 rows) as Σ (1 − d²/R²)³ with R = 2.2 r. The surface is where F > 0.5, and the height is √(F − 0.5) × 1.41 × (Σ c·r / Σ c). That gives a near-perfect sphere for a lone drop and a smooth bulge where drops join. The usual Σ r²/d² field goes flat where drops merge and makes banded reflections. Shade with a smooth studio environment map (soft top light, a low curved horizon as if the camera sits slightly above it), a key light, and a cyan or magenta fresnel rim.
5. **Ribbons** (top): five silky ribbons of light that twist and cross, like a luxurious Siri waveform. Additive colours melt to white where they overlap. Five band groups set the five ribbons' amplitudes, and syllables shed sparks.
6. **Galaxy** (left): a tilted spiral galaxy of about 2,600 star particles (17% bulge, 63% in two logarithmic arms, 20% faint disc), centred at two-thirds width. Loudness spins it up and brightens the arms; every syllable sends a shockwave rippling out through them.
7. **Halo** (bottom): the 32 bands mirrored into 72 radial bars around a glowing ring, inside a zoom-feedback tunnel (each frame, redraw the previous one slightly enlarged and faded, at half resolution). Syllables flash a shockwave outward and throw sparks; a spinner arc runs while she thinks.
8. **Pulsar** (bottom · hiDpi): the "Unknown Pleasures" pulsar plot, live. 54 stacked lines of 80 points; every 55 ms the current spectrum becomes a new ridge, so the plot scrolls through the last three seconds of voice. Draw back to front, filling black beneath each line's own curve (hidden-line removal) before stroking it.
9. **Phosphor** (bottom · terminal): a vintage green P31 oscilloscope with bezel, graticule and panel labels. Listening: a triggered Y-T trace of your waveform. Responding: an X-Y figure of Luna's signal plotted against itself delayed by a quarter window, slowly rotating. Thinking: a 3:2 Lissajous knot turning over. Beam brightness is inversely proportional to beam speed (slow parts burn bright), with phosphor persistence and glow.
10. **Tesla** (bottom): a plasma globe on a pedestal. Lightning filaments crawl from the electrode to the glass; loudness sets how many burn (5 up to 16) and how thick they are, and syllables crackle off side branches with a flash. Violet-blue for you, magenta for Luna, gold while thinking.
11. **Luna Orb** (bottom): a slowly turning sphere of 1,500 points on a Fibonacci lattice, with a nearly edge-on, Saturn-like ring. The bands shape the sphere by latitude, syllables send ripples across its surface, and loudness lights "plexus" lines from each point to its two nearest neighbours.
12. **Digital VU** (bottom · hiDpi): an LED spectrum analyser with 32 columns × 22 segments, running green → lime → yellow → orange → red from bottom to top. Unlit segments glow at about 15% of their colour, and red peak caps hold for 0.6 s and then fall. Everything is mirrored in a black glass floor (a clear reflection that fades out quickly, plus a faint glass edge line). Remap the bands as (b − 0.28) / 0.72 with a slight treble tilt, so speech sits mid-meter and only loud syllables reach red. LED ballistics: near-instant attack, linear release of about 1.3 × full height per second. A scanner sweeps across while she thinks, with a low shimmer when idle.
13. **Paint Splash** (left): liquid paint thrown on every syllable, inspired by the "paint splash" OLED screensaver on ASUS laptops. Each splash is a core blob, 3–6 arms (chains of shrinking blobs) and a spray of small droplets; sustained loud speech adds extra spray. Render the paint as coloured metaballs using Mercury's kernel, where each pixel's colour is the blobs' colours weighted by the square of their field contribution (distinct paints with soft seams). Shade it as glossy wet paint: diffuse light, a tight Blinn highlight, and darker creases where blobs meet. Blobs slow with drag, then shrink and leave a faint stain that fades. Cool paints for you, hot paints for Luna, and a slow swirl while she thinks. Cap it at about 280 blobs.
14. **Fireworks** (bottom): a night sky over a city skyline with lit windows and a harbour. Every syllable launches a rocket; its strength sets how high it climbs and how big it bursts (peony, ring or willow shells), loudness sets the pace of the barrage, and gold shells fly while she thinks. Sparks fly with drag and gravity into a fading trail buffer (long streaks) and glitter as they die. Each burst flashes the sky and lights the skyline, and the harbour shows a squashed reflection. Cap it at about 1,800 sparks, and batch sparks into `Path2D` objects by colour and alpha bucket.
15. **Synthwave** (top): an 80s neon sunset. A striped yellow-to-pink sun whose stripes thicken with the bass; two mountain ridges whose profile is the spectrum mirrored out from the sun, leaving a gap for the sun, with neon edge lines; and a perspective grid that rushes toward you faster as the voice gets louder. Each syllable sends a bright wave rolling down the grid, with haze at the horizon. Cyan edges for you, pink for Luna, orange while thinking.
16. **HAL 9000** (left · hiDpi): the red eye from *2001: A Space Odyssey*. A tall brushed-aluminium panel on the right that runs off the top and bottom of the screen, a black "HAL 9000" nameplate, and a lens in a machined chrome ring (a conic gradient bands it like turned metal) and a black bezel. The eye is a radial gradient from a pale-yellow core through orange and red to near-black at the rim, its hot centre growing with intensity. Over it sits a cached glass layer: faint concentric lens elements, curved fisheye reflections of windows and ceiling lights, a specular highlight and a dark edge. Only the eye blooms, and a little red spill catches the chrome. Intensity follows Luna's loudness while she speaks and syllables flicker the core. Your voice adds a cool cyan rim light; while she thinks, the eye breathes slowly and its centre turns amber.
17. **Mother** (left · mother · hiDpi): MU/TH/UR 6000 from *Alien*. On the left, a phosphor CRT terminal (aqua `#8cffd6`, scanlines, vignette, a little flicker and persistence) whose header reads "MU/TH/UR 6000" and then types "INTERFACE 2037 READY FOR INQUIRY" at about 34 characters a second at the start of every conversation, with a block cursor. Its foot traces the live waveform, or fills a progress row of blocks labelled "COMPUTING" while she thinks. On the right, a wall of 3 × 5 dark panels of 5 × 4 lamps (about two-thirds white, a quarter amber, the rest red), dark sockets drawn once, lit ones drawn as glow sprites. Each lamp toggles at its own rate, faster as the voice gets louder and as its column's band gets stronger; every syllable throws a cluster on. While she thinks, a diagonal wave sweeps the wall. Tint the lit lamps cool for you, rose for Luna and gold while thinking with a `source-atop` fill over the lamp buffer.
18. **Light Cycles** (top · tron · hiDpi): the Grid from *Tron*. A static perspective floor (camera 14 units up, horizon at 38% of the height, the floor at z = 10 meeting the bottom edge) with a glowing arena outline and a grid that fades toward the horizon, all drawn once. Two cycles, cyan for you and orange for Luna, ride the arena. Whoever is speaking rides at 6 + 14 × level units a second and turns 90° on their syllables (at most every 0.32 s); the other turns on a 1–3 s timer. Before hitting the edge, turn toward the side with room. Each cycle samples its position 30 times a second into a trail with a height of 0.5 + 6 × level, so the wall behind it is the voice's loudness over time, a waveform in 3-D. Draw the trail as translucent quads plus bright top and floor edges, bucketed by age into five paths, fading over six seconds. The cycle itself is a hot streak with a glow sprite and a flattened pool of light on the floor. While she thinks, both slow down and gold rings roll out across the floor.
19. **LCARS** (lcars · lcars · hiDpi): the Starfleet console from *Star Trek: The Next Generation*. A black screen framed by an orange top elbow and a lavender bottom elbow (a large outer radius and a small inner fillet, drawn with `arcTo`), five sidebar blocks in lavender, peach, blue, orange and tan with black number labels, top and bottom bars of coloured segments (the bottom one ends in a rounded cap) and the title "VOICE INTERFACE" in orange at the top right. In the lower half of the frame, 28 rounded bars on dark tracks show the spectrum, mapped as on the Digital VU: blue and sky-blue for you, lavender and pink for Luna, a gold chase while she thinks, dim tan when idle. A state word (RECEIVING, TRANSMITTING, COMPUTING, STANDBY) sits above the bars on the right. Syllables flash a sidebar block white and roll the numbers in the bottom bar; while she thinks, the blocks and top segments light in sequence. The frame is drawn once and the highlights go on top of it.
## Where it runs (Home Assistant)
If you're only building the visuals, skip this section and deliver the engine, skins, gallery and test harness.
- The chosen skin lives in an `input_select` helper holding the 19 names plus "Surprise me". Switch skins only between conversation turns, never mid-sentence.
- The engine is added to the dashboard as a Lovelace JavaScript resource and takes over only the wall-panel dashboard's path.
- Inputs from Home Assistant: the voice satellite's state, the "last heard" and "last reply" transcript sensors, the reply audio URL from the Assist pipeline's debug events (`tts_output`), and a microphone room-level sensor as a fallback level source. Keep every entity ID in one config block.
- Home Assistant serves `/local` files with a month-long cache, so ship a tiny, stable **loader** at the registered resource URL. It fetches `version.json` with `cache: "no-store"`, loads `voice-visuals.js?v=<version>`, re-checks every 5 minutes, and hot-swaps a new build when no conversation is running.
- A **gallery page**: a live thumbnail of every skin playing a demo conversation, a full-size preview on tap, and a "Use this visual" button that sets the helper.
- Debug switches in `localStorage`: force the overlay on or off, an on-screen readout (skin, mic level, reply level, ms per frame), and an fps cap.
## Demo mode and testing (build these first)
- Embed the analysis (not the audio) of one real exchange as base64 frames of [envelope + 32 bands] every 20 ms, plus a scripted timeline: 0.6–3.6 s listening ("Hey Luna, can you show me something beautiful?"), 3.6–4.9 s thinking, and 4.9–14.1 s responding ("Of course. Here's a little light show: nineteen different looks, all dancing to my voice. Pick your favourite, and I'll wear it every time we talk."). The gallery and the tests both run on it.
- `test.html?skin=<id>&t=<seconds>&run=0` renders a deterministic frame at time t, after simulating the preceding 3–4 s at 30 fps so trails and physics have history. Render every skin at t = 2.3 (listening), 4.3 (thinking) and 7.7 (responding) and look at the images (or send them to me). Fix anything blown out to white, empty, static, cluttered or hidden behind the text.
- Measure the average ms per frame in each state at 1280×720, and fix anything over budget.
- Build step: concatenate the source files, escape non-ASCII characters, syntax-check the output, and write `version.json`.
## Lessons already learned (apply these from the start)
- Additive light saturates fast. Check the loud responding state for white-out: the first Halo became a solid white disc, and Ribbons blew out in the middle. Keep alphas and bloom modest and let the bloom supply the glow.
- Use the bounded metaball kernel above; the classic one flattens and bands.
- Meter-style skins must remap the dB-scaled bands, or every column sits pinned at the top.
- Seed time-based state on the first `draw`, not in `init`. In the live overlay `t` is the page's uptime, so blobs born at t = 0 expire instantly.
- Pre-render static parts (skies, skylines, dials, bezels) once in `init`, use fixed-size sprite buffers, and never resize a buffer every frame.
- Every skin must look intentional when idle and while thinking, not only while someone speaks.
## Deliverables, in this order
1. The engine: audio analysis, frame builder, renderer with bloom, text layer, demo data and test harness, plus Lens Flares and Classic VU.
2. The remaining skins in batches of four or five. After each batch, render the stills, review them honestly and tune.
3. The Home Assistant overlay, loader, `input_select` wiring and gallery.
Output: `voice-visuals.js` (one file), `loader.js`, `gallery.html`, `test.html`, `version.json` and the build script.