Ever wanted to project your own images onto physical spaces — a logo on a box, a pattern on a wall, video on a stage prop?
Light Mapper turns any HDMI projector into a browser-based mapping tool. Draw corner-pinned zones, drop in images, video, or live effects, warp them to match real surfaces, and send a fullscreen composite to a second display. Optional mic and MIDI reactivity keep the look in sync with the room.
Stack: Vanilla TypeScript · Vite · WebGL2 · BroadcastChannel editor↔output sync.
See TUTORIAL.md for a logo-on-object walkthrough and how the mapping works, PROGRESS.md for build history, and SPEC.md for the product spec.
npm install
npm run devProduction static build:
npm run build
npx serve distOpen the editor URL Vite prints (usually http://localhost:5173). Use Chrome or Edge for MIDI and the best second-screen support.
- Connect the HDMI projector as an extended display (not mirrored).
- In the editor, click Open output (allow popups if the browser blocks them).
- Drag the output window onto the projector screen.
- Fullscreen it (
Fullscreenbutton,F, orF11). - In the editor, enable Test pattern or White to focus/align, then switch back to Live.
- Corner-pin each zone to a physical surface; use arrow keys for 1px nudges (Shift = 10px).
- Press B anytime for an instant blackout.
If getScreenDetails() is available and permitted, Open output tries to place the window on a non-primary screen. Otherwise place it manually.
| Area | What it does |
|---|---|
| Preview | Live WebGL composite; drag corners / edge midpoints; drag inside to move; Shift-drag to scale |
| Zones | Add, rename, duplicate, delete, ↑↓ z-order, Split 2×2 / 3×3 |
| Source | Effect / solid / image / video; colors, speed, params; opacity, feather, blend |
| Audio | Enable mic; per-zone band→target bindings (opacity / speed / scale / hue) |
| MIDI | Chrome/Edge: Learn mappings for blackout, opacity, visibility, effect speed |
| Persistence | Auto-saves to localStorage; JSON export/import |
- Double-click empty preview space to add a centered zone.
- Click a corner handle (yellow when active) then use arrows to nudge that corner.
- Split 2×2 / 3×3 replaces the selected zone with a grid filling the same quad (handy for multi-surface layouts).
- Image/video files use blob URLs. After reload, re-import files (filenames are remembered; missing media shows a placeholder).
- Video defaults to muted + loop to satisfy autoplay policies.
- Enable mic (grant permission).
- Select a zone → Source → enable audio binding → band bass, target opacity.
- Clap or play bass-heavy audio — the zone should pulse.
- Connect MIDI in Chrome/Edge.
- Click Learn next to a target.
- Move a CC knob or press a pad/note within 15 seconds.
- Mappings are stored in the project JSON.
| Key | Action |
|---|---|
| Arrow keys | Nudge selected corner or zone (1px) |
| Shift + arrows | Nudge 10px |
| B | Toggle blackout |
| F (output window) | Toggle fullscreen |
| Double-click empty canvas | Add zone |
The Strobe effect is capped at 3 Hz by default. Unlocking higher rates shows an in-app warning. Flashing lights can trigger seizures in photosensitive people — leave the cap on unless you know your audience and venue.
- Editor and output each run their own WebGL render loop.
- Shared project state syncs over
BroadcastChannel(light-mapper-sync-v1). - Zone warps use a CPU 3×3 homography with per-fragment inverse mapping (no affine triangle seams).
No AirPlay/casting, no multi-projector, no accounts, no mobile touch editor, no video recording/export.
TBD — add a license before public redistribution if needed.