Steampunk Parallax — Gyroscope & Face Tracking
AI-generated steampunk cityscape split into 5 parallax depth layers. Face tracking via webcam on desktop, gyroscope on mobile. Zeppelins, steam particles, and embers animate independently on each plane.
AI-generated steampunk cityscape — Aetherpunk, Ciudad de los Engranajes Eternos — deconstructed into 5 depth layers. On desktop the webcam tracks your face; on mobile the gyroscope drives the parallax. No framework, no build step.
Input modes
| Platform | Primary | Fallback |
|---|---|---|
| Desktop | Webcam face tracking (face-api.js → nose tip) | Skin-blob detector → mouse |
| Mobile | DeviceOrientationEvent gyroscope | Touch drag |
All modes converge on tgtX / tgtY values that feed the same lerp → transform3d pipeline.
Layer stack
5 planes with independent depth multipliers. Each PNG was chroma-keyed in the browser at load time — no pre-processed PNGs needed:
const LAYERS_CFG = [
{ depth: 0.00 }, // background — static
{ depth: 0.12 }, // distant buildings
{ depth: 0.38 }, // mid city
{ depth: 0.72 }, // foreground structures
{ depth: 1.00 }, // closest — maximum shift
]
Layers are drawn onto <canvas> elements with cover-scale so they fill the viewport at any aspect ratio. On resize they redraw from the cached chroma-keyed source.
Face tracking
Uses @vladmandic/face-api (TinyFaceDetector + 68-point landmarks) loaded from CDN. Landmark 30 (nose tip) drives tgtX/Y — most stable point for translation tracking.
const result = await faceapi
.detectSingleFace(videoEl, FA_OPTS)
.withFaceLandmarks(true) // tiny 68-point model
const nose = result.landmarks.positions[30]
// EMA-smooth the raw detection, then offset from calibration baseline
Falls back to a YCrCb skin-blob detector (no ML model needed) if face-api fails, then to mouse.
Gyroscope
Heavy EMA (α = 0.07) kills sensor noise. Rate limiting (max 6°/sample) blocks gimbal-lock spikes near β = ±90°. Smooth dead zone of ±5° absorbs hand tremor.
// Smooth recalibration — baseline drifts toward current position via EMA
// so parallax fades to zero gradually instead of jumping
gyroBaseGamma += (recalTarget.gamma - gyroBaseGamma) * GYRO_RECAL_SPD
Orientation remapping handles landscape rotation (screen.orientation.angle 90°/270°). iOS permission (DeviceOrientationEvent.requestPermission) gated behind the splash button.
Zeppelins
3 airship sprites fly across independent depth planes (z-index 15 and 25, between layer 4 and layer 3). Each respawns off-screen after a random delay, with Y-separation enforcement to avoid overlap:
// Gentle vertical bob synced to a per-zeppelin phase offset
const bobY = Math.sin(t * 0.4 + z.bob) * 4
// Parallax offset from head/gyro tracking applied on top
const px = curX * z.plane.depth
Particles
- Steam — radial-gradient blobs spawned from three pipe clusters (left, right, center-bottom), CSS
@keyframesrise + expand + fade - Embers — tiny glowing dots launched from furnace areas at ~12 fps, die after 2–6 s
Both are pure DOM elements with inline CSS custom properties — no canvas particle system.
AI-generated content
Illustration and layer separation created with generative AI. Layers were refined post-generation for clean alpha at depth boundaries. The chroma-key pass (G > R×1.35 && G > B×1.35) removes green-screen backing added during generation.
Experimental demo — no GitHub repo.