A camera-free way to represent users in a VoceSpace room. Each participant is an Avo: a small procedural creature rendered with p5.js that reacts to the user's voice and pointer, replacing the gray silhouette in participant tiles.
- Voice-reactive — body swells, mouth opens, halos/orbits/pulses speed up
with the speaker's audio level (Web Audio
AnalyserNodein this demo; in the real app, feed it LiveKit's per-participant audio level). - Alive when idle — breathing, blinking, wandering eyes.
- Pointer-interactive — eyes follow the cursor, the body leans toward it, hovering excites it, rubbing pets it (happy squint + hearts), clicking pokes it (sparkle burst).
- Theme-harmonic colors — the whole palette is derived from the VoceSpace
cyan (
THEME_HUE = 193) via color-harmony offsets, so every possible Avo (including random shuffles) matches the product theme by construction.
No build step. Any static file server works:
python3 -m http.server 8123
# then open http://localhost:8123p5.js is vendored in lib/ (installed once via npm install p5), so the page
works fully offline.
The page is the Avo settings screen: live preview (talk / poke / pet it) plus name, style, color, energy, shuffle, and reset controls. Changes auto-save to localStorage — in VoceSpace this screen becomes the per-user Avo settings page, and the resulting params render inside each participant tile.
Everything the VoceSpace app needs is js/avo.js (plus p5.js). An Avo is
fully described by 5 serializable values:
{ name: 'Han', variant: 0, hue: 193, style: 'blob', energy: 0.6 }Sync those few bytes through your existing room state and every client renders the identical Avo locally — no images, no video tracks.
// One Avo per participant tile:
const avo = Avo.mount(tileElement, {
params: user.avoParams,
getLevel: () => participant.audioLevel, // 0..1, e.g. from LiveKit
onSpeaking: (s) => tileElement.classList.toggle('speaking', s),
onLevel: (lvl) => {}, // optional, for meters
interactive: true, // pointer reactions
});
avo.setParams(newParams); // room state updated
avo.pop(); // remote "nudge" gesture
avo.destroy(); // participant leftOther API surface:
Avo.PALETTE/Avo.STYLES— build your own pickersAvo.randomParams(name)— harmonic shuffleAvo.loadParams()/Avo.saveParams(p)— active avatar (localStorage in the demo; swap for your API in production)Avo.saveToLibrary(p, label?)/Avo.listSaved()/Avo.deleteSaved(id)— saved-Avo library (preset slots, max 12)Avo.snapshot(p, size?)— static one-frame PNG dataURL for previews/listsAvo.exportToFile(p)/Avo.importFromFile(file)— share as.avo.jsonAvo.normalizeParams(p)— validate/clamp any received or imported params
| File | Purpose |
|---|---|
js/avo.js |
The whole widget: rendering core + mount API + params persistence. Copy this into the app. |
js/audio.js |
VoiceLevel — demo-only mic level via Web Audio, plus a no-mic simulation mode |
js/settings.js |
Settings page wiring (controls, auto-save) |
Pure 2D canvas, no WebGL, no assets; each Avo is a few dozen shapes per frame
and runs fine on low-end laptops. One p5 instance per tile; call
handle.destroy() when a tile unmounts.