All projects
JavaScript CSS WebAPI Generative AI face-api.js

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.

Aetherpunk — steampunk parallax demo
AI-generated illustration split into 5 parallax depth planes

Input modes

PlatformPrimaryFallback
DesktopWebcam face tracking (face-api.js → nose tip)Skin-blob detector → mouse
MobileDeviceOrientationEvent gyroscopeTouch 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 @keyframes rise + 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.

Built by

Viseni Design Studio

3D, WebGL, interactive & VR/AR experiences

Visit viseni.com