← back to Warp Starfield
Warp starfield tunnel: working self-contained build
7e3e351c0be435a8034f2478f4021473c159966d · 2026-07-24 20:43:44 -0700 · Steve
Built up from the Model Arena starting artifact. Kept perspective star
projection + motion-blur streaks; fixed resize star-pool bug, added live
FPS/speed HUD, hyperspace boost, tab-blur pause, reduced-motion support.
Files touched
A .gitignoreA README.mdA index.html
Diff
commit 7e3e351c0be435a8034f2478f4021473c159966d
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 24 20:43:44 2026 -0700
Warp starfield tunnel: working self-contained build
Built up from the Model Arena starting artifact. Kept perspective star
projection + motion-blur streaks; fixed resize star-pool bug, added live
FPS/speed HUD, hyperspace boost, tab-blur pause, reduced-motion support.
---
.gitignore | 8 ++
README.md | 44 ++++++++
index.html | 358 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 410 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..00c1231
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# Warp Starfield Tunnel
+
+A single self-contained HTML file: a procedurally generated warp-speed starfield
+tunnel rendered on a 2D canvas at 60fps. No dependencies, no build step — open
+`index.html` in any modern browser.
+
+## Controls
+
+| Input | Action |
+|-------|--------|
+| **Move mouse** | Steer the camera (banks the vanishing point) |
+| **Scroll wheel** | Change cruise speed |
+| **Hold click / Space** | Hyperspace boost (speed surge, tighter trails, pink glow) |
+| **↑ / ↓ arrows** | Nudge speed up / down |
+| **Touch: one-finger drag** | Steer + boost |
+| **Touch: two-finger vertical** | Change speed |
+
+The HUD shows live **FPS** and **speed** so the 60fps target is verifiable.
+
+## What it does
+
+- **Perspective star projection** — each star has a 3D position; `x*FOV/z` projects
+ it to screen, so stars streak outward from the vanishing point as `z` shrinks.
+- **Motion blur** — instead of clearing the frame, a translucent black wash is
+ drawn each frame; faster speed = lighter wash = longer streaks.
+- **Adaptive density** — star count scales with the viewport area and is rebuilt
+ on resize.
+- **Depth-aware shading** — nearer stars are larger, brighter, and get a bright
+ core dot; distant stars twinkle. Color shifts hotter/whiter with speed.
+- **Frame-rate independent** — motion is scaled by a clamped delta-time, so it
+ runs the same on 60/120/144Hz displays.
+- **Pauses when the tab is hidden** and respects `prefers-reduced-motion`.
+
+## Origin
+
+Built up from a Model Arena challenge starting artifact — kept the sound core
+(perspective projection + motion-blur streaks) and fixed/extended it:
+
+- Star pool is now **rebuilt on resize** (was computed once and never adjusted).
+- Added a **live FPS + speed HUD** (the challenge calls for verifiable 60fps).
+- Added **hyperspace boost**, arrow-key speed, and idle-fade chrome.
+- **Pauses on tab blur** and resets the clock so returning doesn't jump.
+- Tuned steering so the vanishing-point shift and world-drift no longer
+ double-lurch; removed dead code.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..3850404
--- /dev/null
+++ b/index.html
@@ -0,0 +1,358 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="theme-color" content="#000000">
+<title>Warp Starfield Tunnel</title>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: #000;
+ cursor: crosshair;
+ -webkit-tap-highlight-color: transparent;
+ }
+ canvas { display: block; }
+
+ #hud {
+ position: fixed;
+ left: 14px;
+ bottom: 12px;
+ font: 11px/1.6 "SF Mono", Menlo, Consolas, monospace;
+ color: rgba(160, 200, 255, 0.55);
+ pointer-events: none;
+ user-select: none;
+ letter-spacing: 0.5px;
+ text-shadow: 0 0 6px rgba(80, 140, 255, 0.4);
+ white-space: nowrap;
+ }
+ #hud b { color: rgba(200, 225, 255, 0.9); font-weight: 600; }
+
+ #stats {
+ position: fixed;
+ right: 14px;
+ bottom: 12px;
+ font: 11px/1.6 "SF Mono", Menlo, Consolas, monospace;
+ color: rgba(160, 200, 255, 0.45);
+ pointer-events: none;
+ user-select: none;
+ text-align: right;
+ text-shadow: 0 0 6px rgba(80, 140, 255, 0.35);
+ }
+ #stats .val { color: rgba(200, 225, 255, 0.85); }
+
+ #speedbar {
+ position: fixed;
+ left: 14px;
+ bottom: 46px;
+ width: 160px;
+ height: 3px;
+ background: rgba(120, 160, 255, 0.15);
+ border-radius: 2px;
+ overflow: hidden;
+ pointer-events: none;
+ }
+ #speedfill {
+ height: 100%;
+ width: 20%;
+ background: linear-gradient(90deg, #3af, #aef);
+ border-radius: 2px;
+ box-shadow: 0 0 8px rgba(90, 180, 255, 0.8);
+ transition: background 0.25s;
+ }
+ #speedfill.boost { background: linear-gradient(90deg, #f6a, #fdb); box-shadow: 0 0 10px rgba(255, 140, 180, 0.9); }
+
+ /* Fade the chrome out when idle so the field is unobstructed */
+ #hud, #stats, #speedbar { transition: opacity 0.6s; }
+ body.idle #hud, body.idle #stats, body.idle #speedbar { opacity: 0.25; }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div id="speedbar"><div id="speedfill"></div></div>
+<div id="hud">WARP DRIVE — move mouse to steer · scroll to change speed · hold <b>space</b>/click for hyperspace</div>
+<div id="stats"><span class="val" id="fps">--</span> fps · <span class="val" id="spd">--</span> ly/s</div>
+<script>
+(function () {
+ "use strict";
+
+ var canvas = document.getElementById("c");
+ var ctx = canvas.getContext("2d", { alpha: false });
+ var speedfill = document.getElementById("speedfill");
+ var fpsEl = document.getElementById("fps");
+ var spdEl = document.getElementById("spd");
+ var body = document.body;
+
+ var prefersReduced = window.matchMedia &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ var W = 0, H = 0, CX = 0, CY = 0, DPR = 1;
+
+ // ---- config ----
+ var DEPTH = 1600; // z range (near..far)
+ var FOV = 320; // projection focal length
+ var MIN_SPEED = 2;
+ var MAX_SPEED = 90;
+ var BOOST_SPEED = 150; // hyperspace target
+ var STAR_DENSITY = 1800; // one star per this many screen px^2
+
+ var stars = [];
+ var NUM_STARS = 0;
+
+ function targetStarCount() {
+ // scale with screen area, clamped to a sane range
+ return Math.min(1400, Math.max(400, Math.floor((W * H) / STAR_DENSITY)));
+ }
+
+ function spawn(s, randomZ) {
+ // spread wide so steering reveals new stars at the edges
+ s.x = (Math.random() * 2 - 1) * DEPTH * 1.2;
+ s.y = (Math.random() * 2 - 1) * DEPTH * 1.2;
+ s.z = randomZ ? Math.random() * DEPTH : DEPTH;
+ s.px = null;
+ s.py = null;
+ s.hue = 200 + Math.random() * 60 - 30; // blue-white core palette
+ if (Math.random() < 0.08) s.hue = 30 + Math.random() * 30; // occasional gold star
+ s.mag = 0.5 + Math.random() * 1.0; // brightness / size multiplier
+ s.tw = Math.random() * 6.283; // twinkle phase
+ return s;
+ }
+
+ function buildStars() {
+ NUM_STARS = targetStarCount();
+ if (stars.length > NUM_STARS) {
+ stars.length = NUM_STARS;
+ } else {
+ while (stars.length < NUM_STARS) stars.push(spawn({}, true));
+ }
+ }
+
+ function resize() {
+ DPR = Math.min(window.devicePixelRatio || 1, 2);
+ W = window.innerWidth;
+ H = window.innerHeight;
+ canvas.width = Math.floor(W * DPR);
+ canvas.height = Math.floor(H * DPR);
+ canvas.style.width = W + "px";
+ canvas.style.height = H + "px";
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
+ CX = W / 2;
+ CY = H / 2;
+ buildStars();
+ // hard reset so we don't smear stale pixels after a resize
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, W, H);
+ }
+ window.addEventListener("resize", resize);
+ resize();
+
+ // ---- state ----
+ var speed = 16; // scroll-controlled target speed
+ var curSpeed = 16; // smoothed speed actually rendered
+ var mouseX = 0, mouseY = 0; // -1..1 steer target
+ var steerX = 0, steerY = 0; // smoothed steer
+ var boosting = false;
+ var boostAmt = 0; // 0..1 eased boost
+ var idleTimer = null;
+
+ function wake() {
+ body.classList.remove("idle");
+ if (idleTimer) clearTimeout(idleTimer);
+ idleTimer = setTimeout(function () { body.classList.add("idle"); }, 2600);
+ }
+ wake();
+
+ // ---- input: steering ----
+ window.addEventListener("mousemove", function (e) {
+ mouseX = (e.clientX / W) * 2 - 1;
+ mouseY = (e.clientY / H) * 2 - 1;
+ wake();
+ });
+
+ // ---- input: speed via scroll ----
+ window.addEventListener("wheel", function (e) {
+ e.preventDefault();
+ speed += (e.deltaY < 0 ? 1 : -1) * (2 + speed * 0.12);
+ speed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, speed));
+ wake();
+ }, { passive: false });
+
+ // ---- input: hyperspace boost (hold) ----
+ function boostOn() { boosting = true; wake(); }
+ function boostOff() { boosting = false; }
+ window.addEventListener("mousedown", boostOn);
+ window.addEventListener("mouseup", boostOff);
+ window.addEventListener("keydown", function (e) {
+ if (e.code === "Space") { e.preventDefault(); boostOn(); }
+ else if (e.key === "ArrowUp") { speed = Math.min(MAX_SPEED, speed + 6); }
+ else if (e.key === "ArrowDown") { speed = Math.max(MIN_SPEED, speed - 6); }
+ });
+ window.addEventListener("keyup", function (e) {
+ if (e.code === "Space") boostOff();
+ });
+
+ // ---- input: touch (drag steers, two-finger vertical changes speed) ----
+ var lastTouchY = null;
+ window.addEventListener("touchstart", function (e) {
+ if (e.touches.length === 1) boostOn();
+ }, { passive: true });
+ window.addEventListener("touchmove", function (e) {
+ var t = e.touches[0];
+ mouseX = (t.clientX / W) * 2 - 1;
+ mouseY = (t.clientY / H) * 2 - 1;
+ if (e.touches.length === 2) {
+ boostOff();
+ if (lastTouchY !== null) {
+ speed += (lastTouchY - e.touches[1].clientY) * 0.3;
+ speed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, speed));
+ }
+ lastTouchY = e.touches[1].clientY;
+ }
+ wake();
+ e.preventDefault();
+ }, { passive: false });
+ window.addEventListener("touchend", function () { lastTouchY = null; boostOff(); });
+ window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
+
+ // ---- pause when tab is hidden (and avoid a huge dt on return) ----
+ var running = true;
+ document.addEventListener("visibilitychange", function () {
+ if (document.hidden) {
+ running = false;
+ } else if (!running) {
+ running = true;
+ lastT = performance.now();
+ requestAnimationFrame(frame);
+ }
+ });
+
+ // ---- fps sampling ----
+ var fpsAccum = 0, fpsFrames = 0, fpsLast = performance.now(), fpsShown = 0;
+
+ var lastT = performance.now();
+
+ function frame(now) {
+ if (!running) return;
+
+ var dt = Math.min((now - lastT) / 16.6667, 3); // normalize to 60fps units, clamp spikes
+ lastT = now;
+
+ // fps meter (updated ~4x/sec)
+ fpsFrames++;
+ if (now - fpsLast >= 250) {
+ fpsShown = Math.round((fpsFrames * 1000) / (now - fpsLast));
+ fpsFrames = 0;
+ fpsLast = now;
+ fpsEl.textContent = fpsShown;
+ }
+
+ // smooth boost + resulting speed target
+ boostAmt += ((boosting ? 1 : 0) - boostAmt) * 0.12 * dt;
+ var speedTarget = speed + (BOOST_SPEED - speed) * boostAmt;
+ curSpeed += (speedTarget - curSpeed) * 0.08 * dt;
+
+ var speedFrac = (curSpeed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED);
+ var displayFrac = Math.min(1.4, speedFrac); // may exceed 1 during boost
+
+ // smooth steering
+ steerX += (mouseX - steerX) * 0.05 * dt;
+ steerY += (mouseY - steerY) * 0.05 * dt;
+
+ // motion blur: translucent black wash — faster = less wash = longer streaks
+ var fade = 0.34 - Math.min(1, speedFrac + boostAmt * 0.6) * 0.26; // 0.34 slow .. 0.08 fast
+ ctx.fillStyle = "rgba(0,0,0," + fade.toFixed(3) + ")";
+ ctx.fillRect(0, 0, W, H);
+
+ // steering: shift the vanishing point toward the mouse for a "banking" feel,
+ // plus a gentle world-space cross-drift (kept subtle so it doesn't lurch)
+ var vpx = CX - steerX * W * 0.28;
+ var vpy = CY - steerY * H * 0.28;
+ var camVX = steerX * curSpeed * 6;
+ var camVY = steerY * curSpeed * 6;
+
+ ctx.lineCap = "round";
+
+ for (var i = 0; i < NUM_STARS; i++) {
+ var s = stars[i];
+
+ s.z -= curSpeed * dt;
+ s.x -= camVX * dt;
+ s.y -= camVY * dt;
+
+ if (s.z <= 1) { spawn(s, false); continue; }
+
+ var k = FOV / s.z;
+ var sx = vpx + s.x * k;
+ var sy = vpy + s.y * k;
+
+ // recycle stars that leave the viewport (with margin)
+ if (sx < -80 || sx > W + 80 || sy < -80 || sy > H + 80) {
+ spawn(s, false);
+ continue;
+ }
+
+ if (s.px !== null) {
+ var depthFrac = 1 - s.z / DEPTH; // 0 far .. 1 near
+ var size = (0.3 + depthFrac * 2.2) * s.mag;
+
+ // twinkle only matters for slow, distant stars
+ var twinkle = 1 - 0.25 * (1 - depthFrac) * (0.5 + 0.5 * Math.sin(s.tw += 0.15 * dt));
+ var alpha = Math.min(1, (0.14 + depthFrac * 0.95) * (0.6 + 0.4 * s.mag)) * twinkle;
+
+ // hotter / whiter at speed
+ var sat = Math.max(0, 72 - (speedFrac + boostAmt) * 50);
+ var light = 60 + depthFrac * 30 + Math.min(1, speedFrac + boostAmt) * 10;
+
+ ctx.strokeStyle = "hsla(" + s.hue + "," + sat + "%," + light + "%," + alpha.toFixed(3) + ")";
+ ctx.lineWidth = size;
+ ctx.beginPath();
+ ctx.moveTo(s.px, s.py);
+ ctx.lineTo(sx, sy);
+ ctx.stroke();
+
+ // bright core dot on the nearest stars
+ if (depthFrac > 0.72) {
+ ctx.fillStyle = "hsla(" + s.hue + ",30%,96%," + (alpha * 0.9).toFixed(3) + ")";
+ ctx.beginPath();
+ ctx.arc(sx, sy, size * 0.55, 0, 6.2832);
+ ctx.fill();
+ }
+ }
+
+ s.px = sx;
+ s.py = sy;
+ }
+
+ // central glow that intensifies with speed / boost
+ var glow = Math.min(1, speedFrac + boostAmt * 0.8);
+ if (glow > 0.05) {
+ var glowR = Math.max(60, Math.min(W, H) * (0.1 + glow * 0.4));
+ var g = ctx.createRadialGradient(vpx, vpy, 0, vpx, vpy, glowR);
+ var tint = boostAmt > 0.3 ? "255,170,200" : "150,190,255";
+ g.addColorStop(0, "rgba(" + tint + "," + (0.11 * glow).toFixed(3) + ")");
+ g.addColorStop(1, "rgba(" + tint + ",0)");
+ ctx.fillStyle = g;
+ ctx.fillRect(vpx - glowR, vpy - glowR, glowR * 2, glowR * 2);
+ }
+
+ // HUD readouts
+ speedfill.style.width = (Math.min(1, displayFrac) * 100).toFixed(1) + "%";
+ if (boostAmt > 0.15) speedfill.classList.add("boost");
+ else speedfill.classList.remove("boost");
+ spdEl.textContent = (curSpeed / 3).toFixed(1);
+
+ requestAnimationFrame(frame);
+ }
+
+ // If the user prefers reduced motion, start slow and don't auto-race.
+ if (prefersReduced) { speed = 6; curSpeed = 6; }
+
+ requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>
(oldest)
·
back to Warp Starfield
·
(newest)