Laser Hands — Neural Light Studio

A browser-native, zero-install, single-file hand-tracking creative experience

WebAI No backend MediaPipe Canvas 2D

What it is

Laser Hands is a self-contained single HTML file that uses your device's camera to track your hands in real time, then renders glowing multicolor laser beams across every finger bone, knuckle, and palm connection. No app to install, no server required, no video ever leaves your device. Open the file in a browser served over localhost or https, grant camera permission, and your hands become instruments of light.

It was built as a gesture-vocabulary playground — a creative tool that maps specific hand poses to visual events, with the infrastructure to add more gestures over time.

Delivery

1 HTML file

Dependencies

0 installed

Hand landmarks

21 per hand

Max hands

2 simultaneous

Laser modes

5 + random

Backend calls

Zero

How to open it

Browsers require a secure origin before granting camera access, so the file must be served — not opened directly from your filesystem.

Python — run python -m http.server 8080 in the file's folder, then open http://localhost:8080/hand-laser-spa-v2.html
Node — run npx serve . and follow the link it prints
Instant HTTPS — drag the file onto tiiny.host or paste into codepen.io for a shareable link

Gesture vocabulary

Each gesture is detected by analyzing the 21 landmark positions MediaPipe returns per hand. Gestures are mutually exclusive — the system checks them in priority order each frame.

🖐

Open palm — shockwave

Four or more fingers extended and spread apart. An expanding ring pulses outward from your wrist.

4+ fingers extended, avg tip gap > 0.45

🤏

Pinch + hold 1s — draw a line

Thumb and index tips together, other fingers open. Hold 1 second — a countdown ring fills — then release to commit a glowing line from anchor to release point. Fades after 0.5s.

pinchD < 0.45, middleExt || ringExt

☝️

Index point + hold 1s — free draw

Index extended, all other fingers curled. Hold 1 second to activate. Your fingertip lays a smooth curved laser trail as you move. Closing the index or opening other fingers ends the stroke, which fades over 1.2s.

idxExt && !midExt && !rngExt && !pkyExt

Fist — detected, reserved

All fingers curled. Recognized and shown in the HUD. Reserved for the next gesture to be added.

extended === 0

Laser color modes

SpectrumFull HSL rainbow cycling continuously across every joint. Each connection shifts hue over time.
FireRed → orange → yellow heat waves. Hue oscillates with a sine wave per connection for a flickering effect.
IceCyan and white crystal tones. Cool 185–220° hue range, high lightness.
NeonMagenta, green, pink, yellow, blue — each finger gets its own fixed color from the palette.
GhostPale translucent blue-white, opacity pulsing per joint on a sine wave. Ethereal, low-contrast look.
RandomAuto-cycles through all five modes every 3.5 seconds. Never repeats the current mode back-to-back.

Technology stack

MediaPipe Hands

AI hand tracking — 21 landmarks per hand

Loaded via CDN · no install

HTML5 Canvas 2D

All laser beam rendering and effects

Native browser API

WebRTC getUserMedia

Camera stream access

Native browser API

requestAnimationFrame

Dual render + detect loops

Native browser API

Google Fonts CDN

Orbitron + Share Tech Mono

UI typography only

How MediaPipe works (non-technical)

MediaPipe Hands is a machine-learning model trained by Google on millions of images of hands. When loaded in the browser, it runs entirely on your device using WebAssembly — a compact, fast format that lets complex AI code run in a browser tab without any server.

Every frame from your camera, it locates your hand and marks 21 key points — the base, middle, and tip of each finger, plus the wrist. These 21 points are called landmarks. The app connects them with laser lines, detects gestures by measuring distances and angles between specific points, and draws everything on a transparent canvas that sits on top of your dimmed camera feed.

How MediaPipe works (technical)

The detection pipeline:

getUserMedia stream
<video> element
hands.send({image})
WASM inference
onResults callback
Canvas render

Two separate requestAnimationFrame loops run in parallel — one drives the canvas render at up to 60fps, the other sends frames to MediaPipe (throttled by detectLast). Both loops store their rAF IDs so they can be cleanly cancelled on tab-away or page unload.

Model complexity

1 (full model)

Detection confidence

0.6 minimum

Tracking confidence

0.55 minimum

Connections drawn

23 per hand

Landmark coordinates

Normalized 0–1

Mirror correction

x = 1 − x

Rendering techniques

Laser beam layering

Each connection is drawn three times on the canvas in a single pass: first a wide soft outer glow layer using shadowBlur, then a crisp core line at full color, then a thin near-white hot-center line at reduced opacity. This three-pass technique produces the glowing neon tube look without any image filters or post-processing.

Velocity-adaptive smoothing

Raw MediaPipe landmarks jitter slightly between frames. Rather than applying fixed smoothing (which adds lag), the app measures how far each landmark moved since the last frame and adapts the blend factor dynamically. A still hand gets heavy smoothing (steady, no tremor). A fast-moving hand gets almost no smoothing (snaps to position immediately). Alpha ranges from 0.35 at rest to 0.9 in fast motion.

Free-draw curve smoothing

When drawing with the index finger, recorded points are rendered as quadratic Bézier curves through midpoints — the standard technique for smooth freehand paths. Each segment curves through the midpoint between two consecutive recorded points rather than connecting them with straight lines, producing natural flowing curves regardless of drawing speed.

Trail / echo control

Rather than clearing the canvas every frame, each frame paints a semi-transparent dark rectangle over the entire canvas. The opacity of that rectangle controls how quickly old frames fade. A higher value clears faster with less echo. The default uses a strength multiplier of 2.6, which clears aggressively for crisp hand tracking with minimal ghosting. A Trail toggle lets the user switch to a slower-fading mode for a motion-blur aesthetic.

DPR-aware canvas

The canvas is sized at innerWidth × devicePixelRatio (capped at 2×), then scaled back down with ctx.setTransform. This prevents blurry rendering on retina displays without any extra drawing cost.

Gesture detection (technical)

Finger extension test

A finger is extended when its tip landmark is farther from the wrist than its PIP (middle knuckle) landmark. This correctly rejects a fist — where the tip sits behind or beside the PIP — while accepting extended fingers at any angle. All distances are normalized by the wrist-to-middle-MCP distance so the test is scale-invariant.

Pinch with fist rejection

Pinch distance alone can't distinguish a pinch from a fist (both collapse the thumb-to-index distance). The detector additionally requires at least one of the middle or ring fingers to be extended. Hysteresis is applied: starting a pinch requires distance below 0.45, but sustaining one only requires below 0.60. A 180ms flicker grace window prevents brief tracking noise from resetting the 1-second arm timer.

Camera lifecycle and privacy

Video frames never leave your device. All processing runs in WebAssembly inside the browser tab.
When you switch to another tab, the camera stream is fully stopped — the OS camera indicator light goes off.
Closing or navigating away also stops the stream via pagehide and beforeunload handlers.
On return to the tab, the camera restarts fresh. The canvas is blacked out during the ~0.5s restart gap so raw video never shows through.
Requires https:// or http://localhost. Browsers block camera access on file:// as a security requirement — this is by design, not a bug.

Architecture overview

The entire app is a single flat JavaScript module — no bundler, no framework, no build step. State is held in a single state object. The two async loops (render and detect) communicate only through state.lastResults — the render loop reads whatever the detect loop last wrote. This avoids synchronization complexity while keeping frame rate independent of inference speed.

Color modes are pure functions — each mode exposes a line(i, t) and glow(i, t) function that takes a connection index and frame counter and returns an hsla() string. Switching modes at runtime is a single variable assignment with no state to reset.

Gesture state per hand is stored as small plain objects in arrays indexed by hand number, reset cleanly on hand loss or tab-away. This design makes adding new gestures straightforward — each gesture reads from the same normalized landmark array and writes to the same particle / shockwave / line queues.