Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rainbow Aura

A single-file browser app that extracts your body silhouette from a live webcam feed and renders it as an inward-radiating ROYGBIV rainbow aura. Ghost frames accumulate and fade, giving movement a persistent afterimage trail. No build step, no server — open the HTML file and stand in frame.


Demo

Open index.html in a modern browser and grant camera access.

Stand back from the camera so your full silhouette is visible. The aura appears within a few seconds as the segmentation model loads.


Features

  • Live webcam → body silhouette → ROYGBIV contour aura
  • GPU-accelerated colorization via WebGL fragment shader with JS erosion fallback
  • Color bands cycle continuously, rotating inward through the spectrum
  • Ghost frame compositor — 50-frame pool with position-based alpha falloff
  • Mirror mode (horizontal flip)
  • Dark navy gradient background
  • Debug view with 5 live pipeline panels and all parameters exposed as sliders
  • Stats overlay showing render FPS, segmentation FPS, foreground pixel count, ghost frame count, and pipeline latency

How It Works

Architecture Overview

Webcam → MediaPipe segmentation → mask pipeline → WebGL shader → ghost buffer → canvas output

Two frame rates run in parallel: the render loop runs at 60fps for smooth ghost compositing, while segmentation runs at a lower rate (configurable via frame skip) to keep CPU load manageable.

Segmentation

MediaPipe Selfie Segmentation (model 1) processes frames at 320×240 — a deliberately small resolution that keeps segmentation fast without sacrificing mask quality. The raw mask is then bilinear-upscaled to the 480×360 processing resolution, which naturally softens the pixelated mask edge before any further processing.

Mask Pipeline

Each new mask frame goes through four stages:

  1. Binary threshold — the MediaPipe confidence channel is thresholded (default 80/255) to produce a hard foreground/background mask.

  2. Morphological close — a dilate followed by an erode pass fills small holes in the mask and smooths jagged edges. Radius is adjustable.

  3. Gaussian blur + re-threshold — a separable 1D Gaussian blur softens the mask, which is then re-thresholded to produce a clean anti-aliased silhouette edge.

  4. Colorization — the processed mask is passed to either the WebGL shader (primary path) or the JS erosion fallback.

WebGL Colorization

The fragment shader takes the binary mask as a luminance texture and estimates each foreground pixel's distance from the silhouette edge using a 16-tap multi-radius neighbourhood search (a jump flood approximation). Distance is divided by the stripe width to get a band index, which is then mapped to the ROYGBIV palette. A uTime uniform drives continuous color cycling so the bands appear to rotate inward through the body.

Pixels deeper than 7 band-widths from the edge are rendered solid black, giving the silhouette interior a dark core with the rainbow concentrated at the contour.

JS Fallback

If WebGL is unavailable, the colorization falls back to iterated morphological erosion on the CPU. Each erosion pass peels one ring off the mask; the pixels shed between passes are colored with the corresponding ROYGBIV band. Functionally equivalent to the shader but significantly slower.

Ghost Compositor

Colored frames are captured into a pre-allocated pool of 50 canvases at a configurable interval (default every 100ms). On every render tick, the pool is composited oldest-to-newest with position-based alpha: the newest frame gets topAlpha (default 0.85), and each older frame is multiplied by falloff (default 0.78). Frames older than ghostFade (default 4s) are skipped.

Position-based falloff means the visible depth of the trail is consistent regardless of how fast you're moving — you always see a clean gradient through the stack rather than a flat cluster of equally-bright frames.

Background

The main canvas draws a black-to-dark-navy vertical gradient on every frame before compositing the ghost stack. This gives the output its characteristic dark glow rather than a plain black background.


Debug View

Click DEBUG (bottom right) to open the debug view: a 3×2 grid of live pipeline panels plus a control panel.

Panel Contents
A1 · MORPH CLOSE Binary mask after morphological close
B1 · BLUR + THRESHOLD Final cleaned mask after gaussian blur and re-threshold
C1 · EROSION RINGS (JS) JS erosion ring colorization, throttled to every 8 pipeline calls
A2 · GHOST COMPOSITE Ghost stack composited on black
B2 · GHOST + GRADIENT BG Ghost stack on the navy gradient background
Controls All pipeline parameters as live sliders

Debug Controls

Parameter Description
stripe px Width of each color band in pixels
morph r Morphological close radius
blur r Gaussian blur radius for edge softening
threshold Re-threshold level after blur
conf MediaPipe confidence threshold
frame skip rAF ticks between segmentation dispatches
ghost Interval between ghost frame captures (ms)
fade Time before a ghost frame expires (ms)
top alpha Alpha of the newest (top) ghost frame
falloff Per-step alpha multiplier toward older frames

Controls

Control Location Action
DEBUG Bottom right Open debug view
PERF MODE Bottom right (debug only) Return to full-screen output
MIRROR Bottom center / debug controls Toggle horizontal flip

Requirements

Any modern browser with:

  • getUserMedia (webcam access, requires HTTPS or localhost)
  • WebGL (used for GPU colorization — JS fallback active if unavailable)
  • MediaPipe Selfie Segmentation loaded from CDN

The MediaPipe model loads from cdn.jsdelivr.net on first run — an internet connection is required.


License

MIT


Notes for Modification

SW and SH are load-bearing — do not increase them casually

SW = 480, SH = 360 is the processing resolution. All typed buffers (binA, dilA, eroA, finM, blrT, blrO, rgba, ringPrev, ringCur, lumData) are allocated once at SW × SH at startup. The WebGL texture, ghost pool canvases, and debug display canvases are all sized to SW × SH. Increasing SW/SH to something like 640×480 increases per-frame pixel work by ~78% across every pipeline stage — morphological close, gaussian blur, GL texture upload, ghost compositing, and debug rendering all scale with pixel count. Profile before increasing. If you change SW/SH you must also update the gl.viewport() call and the gl.uniform2f(glUniRes, ...) uniform.

initGL() must be called before the render loop starts

initGL() compiles the shader, links the program, uploads static uniforms, and sets up the mask texture. If it is not called — or called after startCamera() kicks off the loop — glProgram will be null, useGL will be true (WebGL context exists), and every pipeline call will silently skip colorization while glRenderMask() crashes on a null program reference. initGL() and startCamera() are both called at the bottom of the script in that order. Keep it that way.

The GL blend/clear sequence in glRenderMask must stay in order

glRenderMask() calls gl.disable(gl.BLEND) before clearing, then gl.drawArrays, then the caller composites to canvas. The blend must be disabled during the clear so that the clear actually writes transparent black rather than blending with the previous frame. If you add any draw calls between the clear and gl.drawArrays, re-check whether blend state is correct for each one. Getting this wrong produces red/dirty color artifacts in the ghost composite — the transparent shader background pixels write garbage into the canvas buffer.

The C1 debug ring draw is intentionally throttled

The erosion ring visualization in debug panel C1 runs every 8th pipeline call (ringThrottle % 8). Running it every call means 7 full morphErode passes per frame at SW×SH resolution — at stripeW=9, that's roughly 580 million comparisons per frame on the main thread, dropping render FPS to single digits. The throttle is not a bug. Do not remove it without replacing the JS erosion with a GPU approach.

The ghost pool is pre-allocated — do not create canvases inside the pipeline

ghostPool is an array of 50 pre-allocated canvases created at startup. The pipeline writes into the pool via index cycling (ghostPoolIdx++ % GHOST_MAX). Creating new canvas elements inside pipeline() causes per-frame GC pressure that accumulates into stutter, especially on mobile. If you need more ghost frames, increase GHOST_MAX and extend the pre-allocation array — do not add document.createElement('canvas') calls inside the hot path.

MediaPipe runs asynchronously — pendingMask decouples it from the render loop

mp.onResults() fires asynchronously and stores the mask in pendingMask. The main loop() checks pendingMask each tick and processes it if present. This decoupling is what allows the render loop to run at 60fps while segmentation runs at a lower rate. If you add synchronous work inside mp.onResults(), you block the MediaPipe callback thread and degrade segmentation fps.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages