Canyon Run
muse spark 1.2 contributor · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 40,654 bytes · SHA-256 5022c7ad00c4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANYON RUNNER — High-Speed Procedural Racer</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&family=Orbitron:wght@700;900&display=swap" rel="stylesheet">
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--orange:#ff6b2b;--amber:#ffb700;--purple:#7c3aed;--cyan:#00e5ff;--bg:#0a0a12}
body{background:var(--bg);overflow:hidden;font-family:'Space Grotesk',sans-serif;color:#fff;user-select:none}
#canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10}
/* top bar */
.topbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:18px 24px;gap:16px}
.brand{letter-spacing:0.18em;font-size:11px;opacity:0.9}
.brand b{font-family:'Orbitron',sans-serif;font-weight:900;font-size:18px;letter-spacing:0.12em;display:block;line-height:1}
.brand span{opacity:0.6;font-size:10px;letter-spacing:0.28em}
.top-right{display:flex;gap:14px;align-items:center}
.pill{border:1px solid rgba(255,255,255,0.14);background:rgba(255,255,255,0.06);backdrop-filter:blur(10px);border-radius:999px;padding:8px 14px;display:flex;align-items:center;gap:10px;font-family:'JetBrains Mono',monospace;font-size:11px}
.pill i{width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 10px var(--cyan);display:inline-block;animation:pulse 1.2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
.score-box{text-align:right}
.score-label{font-size:9px;letter-spacing:0.22em;opacity:0.55}
.score-val{font-family:'Orbitron',sans-serif;font-weight:900;font-size:28px;line-height:1;letter-spacing:0.04em}
.score-sub{font-family:'JetBrains Mono',monospace;font-size:10px;opacity:0.6}
/* center multiplier */
.multi-wrap{position:absolute;left:50%;top:18%;transform:translateX(-50%);text-align:center;transition:transform 0.15s}
.multi-num{font-family:'Orbitron',sans-serif;font-weight:900;font-size:64px;line-height:1;text-shadow:0 0 30px rgba(255,183,0,0.6);transition:all 0.12s}
.multi-label{font-size:10px;letter-spacing:0.35em;opacity:0.7;margin-top:4px}
.multi-wrap.pop{transform:translateX(-50%) scale(1.18)}
.multi-wrap.near{animation:nearFlash 0.35s}
@keyframes nearFlash{0%{filter:brightness(2)}100%{filter:brightness(1)}}
/* bottom hud */
.bottom{position:absolute;bottom:0;left:0;right:0;padding:18px 24px;display:grid;grid-template-columns:1fr auto 1fr;gap:18px;align-items:end}
.hud-card{border:1px solid rgba(255,255,255,0.10);background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03));backdrop-filter:blur(12px);border-radius:16px;padding:12px 14px}
.hud-title{font-size:9px;letter-spacing:0.22em;opacity:0.6;margin-bottom:8px;display:flex;justify-content:space-between}
.bar{height:8px;background:rgba(255,255,255,0.10);border-radius:999px;overflow:hidden;position:relative}
.bar-fill{height:100%;border-radius:999px;transition:width 0.15s linear}
#energyFill{background:linear-gradient(90deg,#ff3b30,#ffae00);box-shadow:0 0 14px rgba(255,107,43,0.7);width:72%}
#turboFill{background:linear-gradient(90deg,#00e5ff,#7c3aed);box-shadow:0 0 14px rgba(0,229,255,0.7);width:0%}
.speed-row{display:flex;align-items:baseline;gap:10px}
.speed-num{font-family:'Orbitron',sans-serif;font-weight:900;font-size:26px}
.speed-unit{font-family:'JetBrains Mono',monospace;font-size:10px;opacity:0.6;letter-spacing:0.18em}
.speed-bar{height:4px;background:rgba(255,255,255,0.12);border-radius:999px;overflow:hidden;margin-top:8px}
.speed-fill{height:100%;width:45%;background:linear-gradient(90deg,#fff,#ffb700);box-shadow:0 0 10px rgba(255,183,0,0.8)}
.center-hint{justify-self:center;text-align:center;opacity:0.75}
.hint-kbd{display:inline-flex;gap:6px;align-items:center;font-family:'JetBrains Mono',monospace;font-size:10px}
.kbd{border:1px solid rgba(255,255,255,0.2);border-bottom-width:2px;background:rgba(255,255,255,0.08);padding:3px 6px;border-radius:6px;font-size:10px}
.boost-btn{pointer-events:auto;cursor:pointer;border:none;color:#0a0a12;background:linear-gradient(90deg,#00e5ff,#ffb700);font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.12em;padding:12px 18px;border-radius:999px;box-shadow:0 8px 24px rgba(0,229,255,0.35);transition:transform 0.1s,filter 0.2s}
.boost-btn:active{transform:scale(0.97)}
.boost-btn:disabled{opacity:0.35;filter:grayscale(0.6)}
/* overlays */
.overlay{position:fixed;inset:0;display:grid;place-items:center;background:radial-gradient(800px 600px at 50% 20%,rgba(255,107,43,0.14),transparent 62%),linear-gradient(180deg,rgba(10,10,18,0.42),rgba(10,10,18,0.88));backdrop-filter:blur(2px);z-index:20;padding:24px}
.card{width:min(560px,92vw);border:1px solid rgba(255,255,255,0.12);background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03));backdrop-filter:blur(16px);border-radius:24px;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
.card h1{font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.08em;font-size:34px;line-height:1}
.card h1 em{font-style:normal;background:linear-gradient(90deg,#ffb700,#ff6b2b);-webkit-background-clip:text;background-clip:text;color:transparent}
.card p{opacity:0.75;line-height:1.6;margin:10px 0 18px;font-size:14px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:14px 0}
.mini{border:1px solid rgba(255,255,255,0.10);border-radius:14px;padding:12px;background:rgba(255,255,255,0.04)}
.mini b{font-size:11px;letter-spacing:0.16em;opacity:0.8}
.mini p{font-size:12px;opacity:0.65;margin:6px 0 0}
.btn-primary{width:100%;pointer-events:auto;cursor:pointer;border:none;border-radius:999px;padding:14px 18px;font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.14em;background:linear-gradient(90deg,#ffb700,#ff6b2b);color:#0a0a12;box-shadow:0 10px 30px rgba(255,107,43,0.45);transition:transform 0.1s}
.btn-primary:active{transform:scale(0.98)}
.btn-ghost{width:100%;margin-top:10px;pointer-events:auto;cursor:pointer;border:1px solid rgba(255,255,255,0.14);background:rgba(255,255,255,0.06);color:#fff;border-radius:999px;padding:12px;font-family:'JetBrains Mono',monospace;letter-spacing:0.12em;font-size:12px}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:16px 0}
.stat{border:1px solid rgba(255,255,255,0.10);border-radius:14px;padding:12px;text-align:center;background:rgba(255,255,255,0.04)}
.stat span{font-size:9px;letter-spacing:0.18em;opacity:0.6}
.stat b{font-family:'Orbitron',sans-serif;display:block;font-size:18px;margin-top:4px}
.hidden{display:none !important}
#nearMiss{position:absolute;left:50%;top:42%;transform:translateX(-50%);font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.18em;font-size:14px;color:#ffb700;text-shadow:0 0 18px rgba(255,183,0,0.9);opacity:0;transition:opacity 0.2s,transform 0.2s;pointer-events:none}
#nearMiss.show{opacity:1;transform:translateX(-50%) translateY(-6px)}
.vignette{position:fixed;inset:0;pointer-events:none;background:radial-gradient(900px 600px at 50% 50%,transparent 55%,rgba(0,0,0,0.55) 100%);z-index:5}
.scan{position:fixed;inset:0;pointer-events:none;opacity:0.07;background:repeating-linear-gradient(0deg,transparent 0 2px,rgba(255,255,255,0.4) 2px 3px);mix-blend-mode:overlay;z-index:6}
@media(max-width:780px){
.bottom{grid-template-columns:1fr 1fr;gap:12px}
.center-hint{display:none}
.multi-num{font-size:44px}
.score-val{font-size:22px}
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="vignette"></div>
<div class="scan"></div>
<div id="hud">
<div class="topbar">
<div class="brand"><b>CANYON RUNNER</b><span>PROCEDURAL FLIGHT • MK-IV</span></div>
<div class="top-right">
<div class="pill"><i></i> <span id="statusText">FLOW STATE</span></div>
<div class="score-box">
<div class="score-label">SCORE</div>
<div class="score-val" id="scoreEl">00000</div>
<div class="score-sub">BEST <span id="bestEl">00000</span> • <span id="distEl">0m</span></div>
</div>
</div>
</div>
<div class="multi-wrap" id="multiWrap">
<div class="multi-num" id="multiEl">×1.0</div>
<div class="multi-label">MULTIPLIER</div>
</div>
<div id="nearMiss">NEAR MISS +250</div>
<div class="bottom">
<div class="hud-card">
<div class="hud-title"><span>ENERGY CORE</span><span id="energyPct" style="font-family:'JetBrains Mono',monospace">72%</span></div>
<div class="bar"><div class="bar-fill" id="energyFill"></div></div>
<div style="display:flex;justify-content:space-between;margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:10px;opacity:0.6"><span>DECAY -0.8/s</span><span>ORB +18</span></div>
<div class="hud-title" style="margin-top:12px"><span>TURBO</span><span id="turboPct" style="font-family:'JetBrains Mono',monospace">0%</span></div>
<div class="bar"><div class="bar-fill" id="turboFill"></div></div>
</div>
<div class="center-hint">
<div class="hint-kbd" style="margin-bottom:10px">
<span class="kbd">W</span><span class="kbd">A</span><span class="kbd">S</span><span class="kbd">D</span> <span style="opacity:0.6">or</span> <span class="kbd">←</span><span class="kbd">→</span><span class="kbd">↑</span><span class="kbd">↓</span> <span style="opacity:0.6">•</span> <span class="kbd">SPACE</span> BOOST
</div>
<div style="font-size:10px;letter-spacing:0.18em;opacity:0.5">HOLD STRAIGHT TO CHARGE TURBO • GRAZE WALLS FOR MULTIPLIER</div>
</div>
<div class="hud-card">
<div class="hud-title"><span>VELOCITY</span><span style="font-family:'JetBrains Mono',monospace" id="speedKmh">342 KM/H</span></div>
<div class="speed-row"><div class="speed-num" id="speedEl">342</div><div class="speed-unit">KM/H<br>MACH 0.28</div></div>
<div class="speed-bar"><div class="speed-fill" id="speedFill"></div></div>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="boost-btn" id="boostBtn" style="flex:1">⚡ BOOST</button>
<button class="boost-btn" id="pauseBtn" style="background:rgba(255,255,255,0.10);color:#fff;box-shadow:none;border:1px solid rgba(255,255,255,0.14);flex:0 0 auto">⏸</button>
</div>
</div>
</div>
</div>
<!-- Start Overlay -->
<div class="overlay" id="startOverlay">
<div class="card">
<div style="font-size:10px;letter-spacing:0.28em;opacity:0.6">HIGH-SPEED PROCEDURAL CANYON RACER</div>
<h1>RACE THE <em>SUNSET</em></h1>
<p>Skim the canyon at 400km/h. Your reactor bleeds energy — dive through glowing orbs to survive. Graze walls for multipliers, hold your line to charge turbo, and boost through the golden hour.</p>
<div class="grid2">
<div class="mini"><b>◈ FLOW PHYSICS</b><p>Bank to turn, pitch to climb/dive. Momentum is everything.</p></div>
<div class="mini"><b>⚡ RISK = REWARD</b><p>Near-misses build multiplier. Tight squeezes hide fuel.</p></div>
<div class="mini"><b>⬢ ENERGY</b><p>Constant decay. Miss an orb and you flame out.</p></div>
<div class="mini"><b>◎ TURBO</b><p>Straightaways charge it. Space to warp FOV.</p></div>
</div>
<button class="btn-primary" id="playBtn">▶ IGNITE ENGINES</button>
<button class="btn-ghost" id="howBtn">HOW TO FLY — W A S D / ARROWS + SPACE</button>
<div style="font-size:10px;opacity:0.45;margin-top:10px;text-align:center;letter-spacing:0.12em">TIP: On mobile, drag to steer and tap BOOST</div>
</div>
</div>
<!-- Pause / Game Over -->
<div class="overlay hidden" id="gameOver">
<div class="card" style="text-align:center">
<div style="font-size:10px;letter-spacing:0.32em;opacity:0.6" id="overLabel">REACTOR OFFLINE</div>
<h1 id="overTitle" style="margin-top:6px">FLAME OUT</h1>
<div class="stats">
<div class="stat"><span>FINAL SCORE</span><b id="finalScore">0</b></div>
<div class="stat"><span>MAX SPEED</span><b id="finalSpeed">0</b></div>
<div class="stat"><span>DISTANCE</span><b id="finalDist">0m</b></div>
</div>
<div class="stats" style="grid-template-columns:1fr 1fr">
<div class="stat"><span>BEST MULTIPLIER</span><b id="finalMulti">×1.0</b></div>
<div class="stat"><span>ORBS COLLECTED</span><b id="finalOrbs">0</b></div>
</div>
<button class="btn-primary" id="restartBtn">↻ RE-IGNITE</button>
<button class="btn-ghost" id="menuBtn">MAIN MENU</button>
</div>
</div>
<script type="module">
import * as THREE from 'three';
// ---------- Audio (Web Audio procedural) ----------
let audioCtx, engineOsc, engineGain, masterGain;
let audioReady=false;
function initAudio(){
if(audioReady) return;
try{
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
if(audioCtx.state==='suspended') audioCtx.resume().catch(()=>{});
masterGain = audioCtx.createGain(); masterGain.gain.value=0.18; masterGain.connect(audioCtx.destination);
engineGain = audioCtx.createGain(); engineGain.gain.value=0.0; engineGain.connect(masterGain);
engineOsc = audioCtx.createOscillator(); engineOsc.type='sawtooth'; engineOsc.frequency.value=55;
const filter = audioCtx.createBiquadFilter(); filter.type='lowpass'; filter.frequency.value=900;
engineOsc.connect(filter); filter.connect(engineGain); engineOsc.start();
audioReady=true;
}catch(e){ audioReady=false; }
}
function tone(freq, dur, type='sine', vol=0.25){
if(!audioReady) return;
const o=audioCtx.createOscillator(), g=audioCtx.createGain();
o.type=type; o.frequency.value=freq;
g.gain.value=vol; o.connect(g); g.connect(masterGain);
g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+dur);
o.start(); o.stop(audioCtx.currentTime+dur);
}
function whoosh(){ if(!audioReady) return; tone(800,0.12,'sine',0.18); setTimeout(()=>tone(1200,0.10,'triangle',0.12),60); }
function ping(){ tone(880,0.18,'sine',0.22); setTimeout(()=>tone(1320,0.22,'sine',0.14),80); }
function crashSound(){ tone(120,0.35,'square',0.3); setTimeout(()=>tone(60,0.4,'sawtooth',0.2),100); }
// ---------- Three setup ----------
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled=true; renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=1.15;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1a0f2e, 0.012);
const bgColor = new THREE.Color(0xff7a2e);
scene.background = bgColor;
const camera = new THREE.PerspectiveCamera(78, innerWidth/innerHeight, 0.1, 800);
camera.position.set(0,4.5,10);
const ambient = new THREE.HemisphereLight(0xffd3b0, 0x1a0f2e, 0.85);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffb86a, 1.6);
sun.position.set(40,60,-30); sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.near=1; sun.shadow.camera.far=200;
sun.shadow.camera.left=-60; sun.shadow.camera.right=60; sun.shadow.camera.top=60; sun.shadow.camera.bottom=-60;
sun.shadow.bias=-0.0005;
scene.add(sun);
const rim = new THREE.DirectionalLight(0x7c3aed, 0.7); rim.position.set(-30,20,20); scene.add(rim);
// ground plane (canyon floor)
const floorGeo = new THREE.PlaneGeometry(400,2000, 32, 64);
const floorMat = new THREE.MeshLambertMaterial({color:0x2a1a2e, flatShading:true});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x=-Math.PI/2; floor.position.y=-6; floor.position.z=-400; floor.receiveShadow=true;
scene.add(floor);
// displace floor a bit for dunes
{
const pos=floorGeo.attributes.position;
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), y=pos.getY(i);
pos.setZ(i, Math.sin(x*0.05)*1.2 + Math.cos(y*0.02)*1.5 + (Math.random()-0.5)*0.6);
}
pos.needsUpdate=true; floorGeo.computeVertexNormals();
}
// ---------- Player craft ----------
const craft = new THREE.Group();
scene.add(craft);
craft.position.set(0,0,0);
// body
const bodyGeo = new THREE.ConeGeometry(0.7, 2.6, 6);
bodyGeo.rotateX(Math.PI/2);
const bodyMat = new THREE.MeshPhongMaterial({color:0xe8eaf0, flatShading:true, shininess:120, specular:0xffffff});
const body = new THREE.Mesh(bodyGeo, bodyMat); body.castShadow=true; body.position.z=0;
craft.add(body);
// wings
const wingGeo = new THREE.BoxGeometry(2.6,0.12,0.9);
const wingMat = new THREE.MeshPhongMaterial({color:0xffffff, flatShading:true});
const wings = new THREE.Mesh(wingGeo, wingMat); wings.position.set(0,-0.15, -0.2); wings.castShadow=true; craft.add(wings);
// cockpit
const cockGeo = new THREE.SphereGeometry(0.38,8,8);
const cockMat = new THREE.MeshPhongMaterial({color:0x00e5ff, emissive:0x00aaff, emissiveIntensity:0.9, transparent:true, opacity:0.95});
const cockpit = new THREE.Mesh(cockGeo, cockMat); cockpit.scale.set(1,0.7,1.2); cockpit.position.set(0,0.25,0.45); craft.add(cockpit);
// engine glow
const engGeo = new THREE.ConeGeometry(0.45,0.9,8); engGeo.rotateX(-Math.PI/2);
const engMat = new THREE.MeshBasicMaterial({color:0xff6b2b});
const engineMesh = new THREE.Mesh(engGeo, engMat); engineMesh.position.set(0,-0.1,-1.45); craft.add(engineMesh);
const engLight = new THREE.PointLight(0xff6b2b, 8, 12); engLight.position.set(0,0,-1.6); craft.add(engLight);
// trail points
const trailGeo = new THREE.BufferGeometry();
const trailCount=40;
const trailPos = new Float32Array(trailCount*3);
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos,3));
const trailMat = new THREE.PointsMaterial({color:0xffb700, size:0.18, transparent:true, opacity:0.85, blending:THREE.AdditiveBlending, depthWrite:false});
const trailPoints = new THREE.Points(trailGeo, trailMat); scene.add(trailPoints);
// dust particles
const dustCount=320;
const dustGeo = new THREE.BufferGeometry();
const dustPos = new Float32Array(dustCount*3);
const dustVel = new Float32Array(dustCount);
for(let i=0;i<dustCount;i++){ dustPos[i*3]= (Math.random()-0.5)*60; dustPos[i*3+1]= Math.random()*18-4; dustPos[i*3+2]= Math.random()*-800; dustVel[i]= 0.5+Math.random()*1.2; }
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos,3));
const dustMat = new THREE.PointsMaterial({color:0xffd9a8, size:0.09, transparent:true, opacity:0.55, depthWrite:false});
const dust = new THREE.Points(dustGeo, dustMat); scene.add(dust);
// ---------- Procedural chunks ----------
const CHUNK_LEN=40;
const VISIBLE_CHUNKS=18;
let chunks=[];
let nextZ = -40;
let difficulty=0;
function createChunk(z){
const g=new THREE.Group(); g.position.z=z;
// canyon walls - left/right
const wallH = 10 + Math.random()*8 + difficulty*2;
const wallW = 8 + Math.random()*6;
const gap = Math.max(9 - difficulty*0.9, 5.2) + (Math.random()*2-1);
const offset = (Math.random()-0.5)*6 + Math.sin(z*0.02)*4; // sweeping curve
// left wall
const leftGeo = new THREE.BoxGeometry(wallW, wallH, CHUNK_LEN+2);
// deform vertices for organic look
perturb(leftGeo, 0.9);
const wallMat = new THREE.MeshLambertMaterial({color: new THREE.Color().setHSL(0.06+Math.random()*0.04, 0.45, 0.18+Math.random()*0.08), flatShading:true});
const left = new THREE.Mesh(leftGeo, wallMat); left.castShadow=true; left.receiveShadow=true;
left.position.set(offset - gap/2 - wallW/2, wallH/2 -6, 0);
g.add(left);
// right wall
const rightGeo = new THREE.BoxGeometry(wallW, wallH, CHUNK_LEN+2);
perturb(rightGeo, 0.9);
const right = new THREE.Mesh(rightGeo, wallMat); right.castShadow=true; right.receiveShadow=true;
right.position.set(offset + gap/2 + wallW/2, wallH/2 -6, 0);
g.add(right);
g.userData = {gap, offset, wallH, left, right, z};
// obstacles
const r=Math.random();
if(r<0.42){
// floating monolith
const mw=1.2+Math.random()*1.4, mh=3+Math.random()*5, md=1.2+Math.random()*1.2;
const monGeo=new THREE.BoxGeometry(mw,mh,md); perturb(monGeo,0.4);
const monMat=new THREE.MeshLambertMaterial({color:0x3a2a4a, flatShading:true});
const mon=new THREE.Mesh(monGeo, monMat); mon.castShadow=true;
const side = Math.random()<0.5?-1:1;
mon.position.set(offset + side*(gap*0.18 + Math.random()*2), Math.random()*2-1, (Math.random()-0.5)*18);
g.add(mon);
g.userData.monolith=mon;
// store collider
g.userData.colliders = [{type:'box', mesh:mon, size:new THREE.Vector3(mw,mh,md)}];
} else if(r<0.68){
// tight squeeze - two pillars forming a gate
const gateY = (Math.random()-0.5)*1.5;
const gateX = offset + (Math.random()-0.5)*2;
const gapW = Math.max(3.2 - difficulty*0.22, 2.0);
for(let s of [-1,1]){
const pGeo=new THREE.BoxGeometry(0.9, 5+Math.random()*3, 0.9); perturb(pGeo,0.3);
const pMat=new THREE.MeshLambertMaterial({color:0x5a3a2a, flatShading:true});
const p=new THREE.Mesh(pGeo,pMat); p.castShadow=true;
p.position.set(gateX + s*(gapW/2+0.45), gateY, 0);
g.add(p);
}
// top bar
const barGeo=new THREE.BoxGeometry(gapW+1.8,0.7,0.7); const barMat=new THREE.MeshLambertMaterial({color:0x7a4a2a});
const bar=new THREE.Mesh(barGeo,barMat); bar.position.set(gateX, gateY+2.6,0); bar.castShadow=true; g.add(bar);
g.userData.colliders=[
{type:'gate', x:gateX, y:gateY, gap:gapW}
];
} else {
g.userData.colliders=[];
}
// wall colliders (simplified as infinite walls outside gap)
g.userData.wallColliders={gap, offset};
// orbs
g.userData.orbs=[];
const orbCount = Math.random()<0.7? 1 : Math.random()<0.85?2:0;
for(let i=0;i<orbCount;i++){
const orbGeo=new THREE.IcosahedronGeometry(0.55,1);
const orbMat=new THREE.MeshPhongMaterial({color:0x00e5ff, emissive:0x00e5ff, emissiveIntensity:1.2, flatShading:true});
const orb=new THREE.Mesh(orbGeo, orbMat);
// place orb in risky spot: near wall or center of squeeze
let ox, oy;
if(g.userData.colliders[0]?.type==='gate'){
ox=g.userData.colliders[0].x + (Math.random()-0.5)*0.8;
oy=g.userData.colliders[0].y;
} else {
ox = offset + (Math.random()-0.5)*(gap*0.7);
oy = (Math.random()-0.5)*2.2;
}
orb.position.set(ox, oy, (Math.random()-0.5)*14);
orb.castShadow=false;
const light=new THREE.PointLight(0x00e5ff, 3, 8); light.position.copy(orb.position);
g.add(light); g.add(orb);
g.userData.orbs.push({mesh:orb, light, taken:false, baseY:oy});
}
scene.add(g);
chunks.push(g);
// also store left/right for near miss quickly
return g;
}
function perturb(geo, amt){
const p=geo.attributes.position;
for(let i=0;i<p.count;i++){
p.setX(i, p.getX(i)+(Math.random()-0.5)*amt);
p.setY(i, p.getY(i)+(Math.random()-0.5)*amt*0.6);
p.setZ(i, p.getZ(i)+(Math.random()-0.5)*amt);
}
p.needsUpdate=true; geo.computeVertexNormals();
}
// init chunks
for(let i=0;i<VISIBLE_CHUNKS;i++){ createChunk(nextZ); nextZ-=CHUNK_LEN; }
// ---------- Game state ----------
const state={
running:false, paused:false,
x:0, y:0, vx:0, vy:0, roll:0, pitch:0,
speed: 18, baseSpeed:18, maxSpeed:0,
energy:100, score:0, dist:0, multiplier:1,
turbo:0, boosting:false, boostTime:0,
nearMissCooldown:0,
orbs:0, bestMulti:1,
time:0
};
let keys={};
let pointer={active:false, dx:0, dy:0};
let lastTime=0;
// input
addEventListener('keydown',e=>{
keys[e.code]=true;
if(e.code==='Space'){ e.preventDefault(); tryBoost(); }
if(e.code==='KeyP' || e.code==='Escape'){ togglePause(); }
});
addEventListener('keyup',e=> keys[e.code]=false);
addEventListener('blur',()=> keys={});
canvas.addEventListener('pointerdown',e=>{ pointer.active=true; pointer.sx=e.clientX; pointer.sy=e.clientY; if(!audioReady) initAudio(); });
addEventListener('pointermove',e=>{
if(!pointer.active) return;
pointer.dx = (e.clientX - pointer.sx)/120;
pointer.dy = (e.clientY - pointer.sy)/120;
});
addEventListener('pointerup',()=>{ pointer.active=false; pointer.dx=0; pointer.dy=0; });
canvas.addEventListener('touchstart',e=>{ if(!audioReady) initAudio(); },{passive:true});
// UI refs
const scoreEl=document.getElementById('scoreEl'), bestEl=document.getElementById('bestEl'), distEl=document.getElementById('distEl');
const multiEl=document.getElementById('multiEl'), multiWrap=document.getElementById('multiWrap'), nearEl=document.getElementById('nearMiss');
const energyFill=document.getElementById('energyFill'), energyPct=document.getElementById('energyPct');
const turboFill=document.getElementById('turboFill'), turboPct=document.getElementById('turboPct');
const speedEl=document.getElementById('speedEl'), speedKmh=document.getElementById('speedKmh'), speedFill=document.getElementById('speedFill');
const statusText=document.getElementById('statusText');
const startOverlay=document.getElementById('startOverlay'), gameOverEl=document.getElementById('gameOver');
let bestScore = parseInt(localStorage.getItem('canyonBest')||'0');
bestEl.textContent=String(bestScore).padStart(5,'0');
function tryBoost(){
if(state.turbo>0.18 && !state.boosting){
state.boosting=true; state.boostTime=1.6;
whoosh();
statusText.textContent='TURBO WARP';
statusText.style.color='#00e5ff';
}
}
function togglePause(){
if(!state.running) return;
state.paused=!state.paused;
document.getElementById('pauseBtn').textContent = state.paused?'▶':'⏸';
statusText.textContent = state.paused?'PAUSED':'FLOW STATE';
if(state.paused) gameOverEl.classList.remove('hidden'), document.getElementById('overLabel').textContent='PAUSED', document.getElementById('overTitle').textContent='HOLD ON', document.getElementById('finalScore').textContent=Math.floor(state.score), document.getElementById('finalSpeed').textContent=Math.floor(state.maxSpeed)+' KM/H', document.getElementById('finalDist').textContent=Math.floor(state.dist)+'m', document.getElementById('finalMulti').textContent='×'+state.bestMulti.toFixed(1), document.getElementById('finalOrbs').textContent=state.orbs;
else gameOverEl.classList.add('hidden');
}
function safeInitAudio(){ try{ initAudio(); }catch(e){} }
document.getElementById('playBtn').onclick=()=>{ startGame(); setTimeout(safeInitAudio,80); };
document.getElementById('restartBtn').onclick=()=>{ startGame(); setTimeout(safeInitAudio,80); };
document.getElementById('menuBtn').onclick=()=>{
gameOverEl.classList.add('hidden'); startOverlay.classList.remove('hidden');
state.paused=false;
};
document.getElementById('boostBtn').onclick=()=>{ safeInitAudio(); tryBoost(); };
document.getElementById('pauseBtn').onclick=togglePause;
document.getElementById('howBtn').onclick=()=>{
document.getElementById('howBtn').textContent='WASD / ARROWS TO STEER • SPACE TO BOOST • GRAZE WALLS!';
setTimeout(()=> document.getElementById('howBtn').textContent='HOW TO FLY — W A S D / ARROWS + SPACE', 2200);
};
function startGame(){
// reset
state.running=true; state.paused=false;
state.x=0; state.y=0; state.vx=0; state.vy=0; state.roll=0; state.pitch=0;
state.speed=state.baseSpeed; state.energy=100; state.score=0; state.dist=0; state.multiplier=1; state.turbo=0; state.boosting=false; state.boostTime=0; state.nearMissCooldown=0; state.orbs=0; state.bestMulti=1; state.maxSpeed=0; state.time=0;
difficulty=0;
startOverlay.classList.add('hidden'); gameOverEl.classList.add('hidden');
statusText.textContent='FLOW STATE'; statusText.style.color='';
// reset chunks
chunks.forEach(c=> scene.remove(c)); chunks=[]; nextZ=-40;
for(let i=0;i<VISIBLE_CHUNKS;i++){ createChunk(nextZ); nextZ-=CHUNK_LEN; }
craft.position.set(0,0,0);
if(audioReady){ engineGain.gain.value=0.18; }
lastTime=performance.now();
}
function endGame(reason='FLAME OUT'){
state.running=false;
if(state.score>bestScore){ bestScore=Math.floor(state.score); localStorage.setItem('canyonBest', bestScore); bestEl.textContent=String(bestScore).padStart(5,'0'); }
document.getElementById('overLabel').textContent = reason==='CRASH'?'IMPACT DETECTED':'REACTOR OFFLINE';
document.getElementById('overTitle').textContent = reason==='CRASH'?'CRITICAL HIT':'FLAME OUT';
document.getElementById('finalScore').textContent=Math.floor(state.score);
document.getElementById('finalSpeed').textContent=Math.floor(state.maxSpeed)+' KM/H';
document.getElementById('finalDist').textContent=Math.floor(state.dist)+'m';
document.getElementById('finalMulti').textContent='×'+state.bestMulti.toFixed(1);
document.getElementById('finalOrbs').textContent=state.orbs;
gameOverEl.classList.remove('hidden');
if(audioReady) engineGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime+0.6);
crashSound();
}
// ---------- Main loop ----------
function update(dt){
if(!state.running || state.paused) return;
state.time+=dt;
// input -> target velocities (flight model)
let inputX=0, inputY=0;
if(keys['ArrowLeft']||keys['KeyA']) inputX-=1;
if(keys['ArrowRight']||keys['KeyD']) inputX+=1;
if(keys['ArrowUp']||keys['KeyW']) inputY+=1;
if(keys['ArrowDown']||keys['KeyS']) inputY-=1;
if(pointer.active){ inputX+= pointer.dx*1.4; inputY-= pointer.dy*1.4; }
inputX = Math.max(-1.4, Math.min(1.4, inputX));
inputY = Math.max(-1.2, Math.min(1.2, inputY));
// momentum + banking
const accel= 18;
state.vx += (inputX*10 - state.vx)* accel*dt*0.22;
state.vy += (inputY*7 - state.vy)* accel*dt*0.22;
// drag
state.vx *= (1 - 1.2*dt);
state.vy *= (1 - 1.4*dt);
state.x += state.vx*dt;
state.y += state.vy*dt;
// clamp inside canyon roughly
state.x = Math.max(-11, Math.min(11, state.x));
state.y = Math.max(-3.2, Math.min(4.5, state.y));
// roll/pitch for visual weight
const targetRoll = -state.vx*0.28 - inputX*0.35;
const targetPitch = state.vy*0.18;
state.roll += (targetRoll - state.roll)* 6*dt;
state.pitch += (targetPitch - state.pitch)* 6*dt;
craft.rotation.z = state.roll;
craft.rotation.x = state.pitch*0.6;
craft.position.x = state.x;
craft.position.y = state.y;
// slight bob
craft.position.y += Math.sin(state.time*1.8)*0.06;
// speed & turbo
const straightBonus = (Math.abs(inputX)<0.15 ? 1 : 0) * dt * 8;
if(!state.boosting){
state.turbo = Math.min(1, state.turbo + straightBonus*0.06 + dt*0.02);
state.speed = state.baseSpeed + difficulty*6 + state.turbo*4;
} else {
state.boostTime-=dt;
state.turbo = Math.max(0, state.turbo - dt*0.42);
state.speed = state.baseSpeed + difficulty*6 + 18 + Math.max(0,state.boostTime)*6;
if(state.boostTime<=0){ state.boosting=false; statusText.textContent='FLOW STATE'; statusText.style.color=''; }
}
state.maxSpeed = Math.max(state.maxSpeed, state.speed*18);
state.dist += state.speed*dt*3;
difficulty = Math.min(4, state.dist/900);
// energy decay
const decay = 6.2 + difficulty*1.8 + (state.boosting? 4:0);
state.energy -= decay*dt*1.0;
// passive score
state.score += (state.speed*2 + state.multiplier*6)*dt;
// move world: chunks move towards camera (positive Z) as player goes forward
const forward = state.speed*dt;
chunks.forEach(c=> c.position.z += forward*6);
// recycle chunks
for(let i=chunks.length-1;i>=0;i--){
if(chunks[i].position.z > 30){
scene.remove(chunks[i]);
chunks.splice(i,1);
createChunk(nextZ);
nextZ -= CHUNK_LEN;
}
}
// update floor scroll
floor.position.z += forward*6;
if(floor.position.z > 0) floor.position.z -= 400;
// orbs animation & collection
let nearMiss=false;
let minWallDist=999;
for(const c of chunks){
// orbs
for(const o of c.userData.orbs){
if(o.taken) continue;
o.mesh.rotation.y += dt*2.2;
o.mesh.rotation.x += dt*1.1;
o.mesh.position.y = o.baseY + Math.sin(state.time*2 + c.position.z*0.1)*0.18;
o.light.position.copy(o.mesh.position);
o.light.position.add(c.position);
// world pos
const wx = c.position.x + o.mesh.position.x;
const wy = o.mesh.position.y;
const wz = c.position.z + o.mesh.position.z;
// player at (state.x, state.y, 0)
const dx = wx - state.x, dy = wy - state.y, dz = wz - 0;
const dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(dist<1.35){
o.taken=true; o.mesh.visible=false; o.light.intensity=0;
state.energy = Math.min(100, state.energy+18);
state.score+= 220*state.multiplier;
state.turbo = Math.min(1, state.turbo+0.14);
state.orbs++;
ping();
// pulse multiplier
multiWrap.classList.add('pop'); setTimeout(()=>multiWrap.classList.remove('pop'),160);
}
}
// wall distance for near miss
const wz = c.position.z;
if(Math.abs(wz)< 6){
const gap=c.userData.gap, off=c.userData.offset;
const leftEdge = off - gap/2;
const rightEdge = off + gap/2;
const dLeft = Math.abs(state.x - leftEdge);
const dRight = Math.abs(state.x - rightEdge);
const d = Math.min(dLeft, dRight);
minWallDist = Math.min(minWallDist, d);
// gate collision check
if(c.userData.colliders){
for(const col of c.userData.colliders){
if(col.type==='box'){
const wx = c.position.x + col.mesh.position.x;
const wy = col.mesh.position.y;
const wz2 = c.position.z + col.mesh.position.z;
if(Math.abs(wz2)<1.2){
const dx=Math.abs(wx-state.x), dy=Math.abs(wy-state.y);
const sx=col.size.x/2+0.7, sy=col.size.y/2+0.7;
if(dx<sx && dy<sy){
endGame('CRASH'); return;
}
const edgeDist = Math.max(0, Math.min(dx - sx +0.7, dy - sy +0.7));
if(edgeDist<0.9) nearMiss=true;
}
} else if(col.type==='gate'){
const wz2=c.position.z;
if(Math.abs(wz2)<1.0){
const gx=col.x, gy=col.y, gapW=col.gap;
const dx=Math.abs(state.x-gx), dy=Math.abs(state.y-gy);
// collide with pillars/top bar
const hitPillar = (dx > gapW/2 -0.35 && dx < gapW/2+0.9 && Math.abs(dy)<2.8) || (dy>1.9 && dx<gapW/2+0.9);
// if outside gap and close in Z -> crash
if(hitPillar && dx< gapW/2+0.9){
// check precise
if( (Math.abs(state.x - (gx - gapW/2-0.45))<0.9 || Math.abs(state.x - (gx+gapW/2+0.45))<0.9) && Math.abs(wz2)<0.9 && state.y<2.2){
endGame('CRASH'); return;
}
if(state.y>2.0 && Math.abs(wz2)<0.9){ endGame('CRASH'); return; }
}
// near miss if passing cleanly close to edge
if(dx> gapW/2-0.7 && dx< gapW/2+0.7 && Math.abs(wz2)<1.2) nearMiss=true;
}
}
}
}
// wall crash
if(state.x < leftEdge+0.65 || state.x > rightEdge-0.65){
if(Math.abs(wz)<0.9){
endGame('CRASH'); return;
}
}
}
}
// near miss scoring
if(minWallDist<1.25 && minWallDist>0.25){
nearMiss=true;
}
if(nearMiss){
if(state.nearMissCooldown<=0){
state.multiplier = Math.min(9.9, state.multiplier+0.18);
state.bestMulti=Math.max(state.bestMulti, state.multiplier);
state.score+= 85*state.multiplier;
state.turbo = Math.min(1, state.turbo+0.035);
state.nearMissCooldown=0.28;
// feedback
multiWrap.classList.add('near'); setTimeout(()=>multiWrap.classList.remove('near'),360);
nearEl.classList.add('show'); setTimeout(()=>nearEl.classList.remove('show'),420);
whoosh();
// camera shake
cameraShake=0.35;
}
} else {
// decay multiplier slowly
state.multiplier = Math.max(1, state.multiplier - dt*0.18);
}
state.nearMissCooldown-=dt;
// energy death
if(state.energy<=0){ state.energy=0; endGame('FLAME OUT'); return; }
// HUD update
scoreEl.textContent=String(Math.floor(state.score)).padStart(5,'0');
distEl.textContent=Math.floor(state.dist)+'m';
multiEl.textContent='×'+state.multiplier.toFixed(1);
multiEl.style.color = state.multiplier>2.5 ? '#ffb700' : '#fff';
multiEl.style.textShadow = state.multiplier>2 ? '0 0 22px rgba(255,183,0,0.9)' : '0 0 12px rgba(255,255,255,0.25)';
const ePct=Math.round(state.energy);
energyFill.style.width=ePct+'%'; energyPct.textContent=ePct+'%';
energyFill.style.filter = ePct<28 ? 'hue-rotate(-18deg) brightness(1.2)' : '';
const tPct=Math.round(state.turbo*100);
turboFill.style.width=tPct+'%'; turboPct.textContent=tPct+'%';
const kmh=Math.round(state.speed*18);
speedEl.textContent=kmh; speedKmh.textContent=kmh+' KM/H';
speedFill.style.width= Math.min(100, 18+ kmh/7)+'%';
document.getElementById('boostBtn').disabled = state.turbo<0.18 || state.boosting;
statusText.textContent = state.boosting? 'TURBO WARP' : (state.multiplier>3.5?'DANGER CLOSE': state.multiplier>2?'GRAZING':'FLOW STATE');
// engine audio pitch
if(audioReady){
const f = 55 + state.speed*2.2 + (state.boosting? 30:0) + Math.abs(state.vx)*6;
engineOsc.frequency.linearRampToValueAtTime(f, audioCtx.currentTime+0.08);
engineGain.gain.value = state.running? 0.16 + state.speed*0.002 : 0;
engLight.intensity = 7 + Math.sin(state.time*12)*1.2 + (state.boosting?6:0);
engineMesh.scale.set(1+state.speed*0.01,1,1);
}
// dust
const dpos=dustGeo.attributes.position;
for(let i=0;i<dustCount;i++){
let z=dpos.getZ(i);
z += (state.speed*0.9 + dustVel[i]*6)*dt;
if(z>12){ z = -700 - Math.random()*100; dpos.setX(i,(Math.random()-0.5)*70); dpos.setY(i, Math.random()*18-4); }
dpos.setZ(i,z);
}
dpos.needsUpdate=true;
// trail
for(let i=trailCount-1;i>0;i--){
trailPos[i*3]=trailPos[(i-1)*3];
trailPos[i*3+1]=trailPos[(i-1)*3+1];
trailPos[i*3+2]=trailPos[(i-1)*3+2];
}
trailPos[0]=state.x; trailPos[1]=state.y-0.15; trailPos[2]=-1.2;
trailGeo.attributes.position.needsUpdate=true;
trailMat.opacity = state.boosting?0.95:0.62;
// camera follow + FOV + shake
const baseFov=78;
const targetFov = state.boosting? 92 : 78 + state.speed*0.22;
camera.fov += (targetFov - camera.fov)* 4*dt;
camera.updateProjectionMatrix();
camera.position.x += (state.x*0.42 - camera.position.x)* 5*dt;
camera.position.y += (state.y*0.32 +4.2 - camera.position.y)* 5*dt;
camera.lookAt(state.x*0.5, state.y*0.3, -12);
if(cameraShake>0){
camera.position.x += (Math.random()-0.5)*cameraShake;
camera.position.y += (Math.random()-0.5)*cameraShake;
cameraShake-= dt*2.2;
if(cameraShake<0) cameraShake=0;
}
// subtle engine scale pulse
const s = 1 + Math.sin(state.time*14)*0.02;
body.scale.set(s,s,s);
// background color shift with distance (sunset cycle)
const t = (Math.sin(state.dist*0.0015)*0.5+0.5);
bgColor.setHSL(0.06 + t*0.02, 0.92, 0.58 - t*0.08);
scene.background=bgColor;
scene.fog.color.copy(bgColor).multiplyScalar(0.22);
}
let cameraShake=0;
function loop(now){
requestAnimationFrame(loop);
const dt = Math.min(0.033, (now - lastTime)/1000);
lastTime=now;
if(state.running) update(dt);
else {
// idle camera drift on menu
const t=now*0.0004;
camera.position.x = Math.sin(t)*1.2;
camera.position.y = 4.6 + Math.cos(t*0.7)*0.3;
camera.lookAt(0,0,-14);
// animate dust even when not running
const dpos=dustGeo.attributes.position;
for(let i=0;i<dustCount;i++){ let z=dpos.getZ(i); z+= 6*0.016; if(z>12) z=-700; dpos.setZ(i,z); }
dpos.needsUpdate=true;
}
renderer.render(scene, camera);
}
renderer.setSize(innerWidth, innerHeight);
addEventListener('resize',()=>{
renderer.setSize(innerWidth, innerHeight);
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
});
lastTime=performance.now();
requestAnimationFrame(loop);
// auto demo hover
setInterval(()=>{
if(!state.running && startOverlay.classList.contains('hidden')===false){
// gentle UI pulse
document.getElementById('playBtn').animate([{transform:'scale(1)'},{transform:'scale(1.02)'},{transform:'scale(1)'}],{duration:900});
}
},1800);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"807e91fc-fb27-488e-8bf1-dfb6ae60ae69","tokensIn":356020,"tokensOut":19384,"tokensTotal":375404,"cost":0.017252988000000004,"turns":15,"toolCalls":14,"failedToolCalls":0,"timestamp":"2026-08-23T00:54:13.027Z"} -->