<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Orb Space v4 — Heavenly States</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{background:#04040c;overflow:hidden;font-family:'Courier New',monospace;}
#wrap{width:100vw;height:100vh;position:relative;overflow:hidden;outline:none;}
#c{display:block;width:100%;height:100%;}
#lyric-layer{position:absolute;inset:0;pointer-events:none;overflow:hidden;}
.lyric{position:absolute;pointer-events:none;color:rgba(255,255,255,0.92);font-weight:500;letter-spacing:.04em;line-height:1.2;white-space:nowrap;transform-origin:center center;will-change:transform,opacity;text-shadow:0 0 30px rgba(26,210,155,0.4);}
#hud{position:absolute;top:14px;left:50%;transform:translateX(-50%);text-align:center;pointer-events:none;}
#gear-display{position:absolute;top:14px;right:18px;pointer-events:none;text-align:center;}
#gear-ring{width:40px;height:40px;border-radius:50%;border:1px solid rgba(232,158,28,0.35);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:border-color .25s;pointer-events:all;}
#hint{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);font-size:9px;color:rgba(255,255,255,0.1);letter-spacing:.07em;pointer-events:none;white-space:nowrap;}
</style>
</head>
<body>
<div id="wrap" tabindex="0">
<canvas id="c"></canvas>
<div id="lyric-layer"></div>
<div id="hud">
<div style="font-size:9px;letter-spacing:.14em;color:rgba(26,210,155,0.3);margin-bottom:3px;">ORB SPACE — HEAVENLY STATES</div>
<div id="vdisp" style="font-size:9px;color:rgba(255,255,255,0.12);letter-spacing:.05em;"></div>
</div>
<div id="gear-display">
<div id="gear-ring" onclick="shiftGear()">
<span id="gear-label" style="font-size:15px;font-weight:700;color:rgba(232,158,28,0.75);">1</span>
</div>
<div style="font-size:8px;color:rgba(255,255,255,0.1);margin-top:3px;letter-spacing:.06em;">G / gear</div>
</div>
<div id="hint">scroll · wasd · arrows · Q/E = up/down · G = gear shift</div>
</div>
<script>
const wrap=document.getElementById('wrap');
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
const lyricLayer=document.getElementById('lyric-layer');
let W,H,vx,vy;
// ---- GEAR SYSTEM ----
const GEARS=[
{label:'1',vel:18, color:'rgba(26,210,155,0.75)', name:'cruise'},
{label:'2',vel:45, color:'rgba(232,158,28,0.85)', name:'fast'},
{label:'3',vel:90, color:'rgba(200,80,220,0.85)', name:'fastest'},
{label:'4',vel:0, color:'rgba(255,255,255,0.35)', name:'coast'},
];
let gearIdx=0;
function currentGear(){return GEARS[gearIdx];}
function shiftGear(){
gearIdx=(gearIdx+1)%GEARS.length;
const g=currentGear();
document.getElementById('gear-label').textContent=g.label;
document.getElementById('gear-label').style.color=g.color;
document.getElementById('gear-ring').style.borderColor=g.color.replace(/[\d.]+\)$/,'0.4)');
}
// ---- VELOCITY ----
let vel=0,latVel=0,vertVel=0;
let lateral=0,vertical=0;
const DASH_RATE=0.0022;
let dashOff=0;
// ---- STAR WORLD — 4x wide, 3x tall ----
let SW,SH,camStarX=0,camStarY=0,stars=[];
// ---- ORB WORLD ----
const MAX_Z=900,MIN_Z=8,N_ORBS=180;
let orbs=[],camOrbX=0,camOrbY=0;
// ---- FLOATING VP ----
let vpOffX=0,vpOffY=0;
// ---- GLOW PULSE ----
let glowPhase=0;
// ---- LYRICS ----
const LYRIC_SPAWN_Z=1800,LYRIC_PEAK_Z=25,LYRIC_DIE_Z=-300;
const ANCHORS={
bottom:{x:0.5,y:0.88},top:{x:0.5,y:0.1},
left:{x:0.08,y:0.5},right:{x:0.92,y:0.5},center:{x:0.5,y:0.5}
};
const DEMO=[
{text:'I got power.',ts:2,pos:'bottom',size:20},
{text:'Power of no uptake ratio.',ts:5,pos:'top',size:28},
{text:'Most of you know me alone.',ts:9,pos:'right',size:14},
{text:'so bright',ts:13,pos:'center',size:38},
{text:'La la la la',ts:17,pos:'left',size:20},
{text:'No tears in a teacup.',ts:21,pos:'bottom',size:28},
{text:'These are the silent days.',ts:26,pos:'top',size:20},
{text:'You will be the one.',ts:31,pos:'right',size:28},
];
let lyricDefs=DEMO.map(l=>({...l,fired:false}));
let activeLyrics=[],demoTime=0;
function resize(){
W=wrap.clientWidth;H=wrap.clientHeight;
canvas.width=W;canvas.height=H;
vx=W/2;vy=H/2;
SW=W*4;SH=H*3;
camStarX=SW*0.25;camStarY=SH*0.33;
stars=Array.from({length:280},()=>({
wx:Math.random()*SW,wy:Math.random()*SH,
r:Math.random()<0.15?1.5:Math.random()<0.45?1:0.6,
a:Math.random()*0.4+0.08
}));
orbs=Array.from({length:N_ORBS},()=>mkOrb());
}
function mkOrb(z){
const p=Math.random();
const rgb=p<0.55?[255,255,255]:p<0.72?[26,212,160]:p<0.85?[232,158,28]:p<0.93?[120,48,210]:[210,75,28];
return{x:(Math.random()-0.5)*1800,y:(Math.random()-0.5)*1400,z:z||Math.random()*MAX_Z+50,base:Math.random()*2+0.4,r:rgb[0],g:rgb[1],b:rgb[2]};
}
// ---- CONTROLS ----
wrap.addEventListener('wheel',e=>{e.preventDefault();vel+=e.deltaY*0.65;latVel+=e.deltaX*0.28;},{passive:false});
wrap.addEventListener('keydown',e=>{
if(e.key==='ArrowUp'||e.key==='w'){e.preventDefault();vel-=60;}
if(e.key==='ArrowDown'||e.key==='s'){e.preventDefault();vel+=60;}
if(e.key==='ArrowLeft'||e.key==='a'){e.preventDefault();latVel-=32;}
if(e.key==='ArrowRight'||e.key==='d'){e.preventDefault();latVel+=32;}
if(e.key==='q'||e.key==='i'||e.key==='ArrowUp'&&e.shiftKey){e.preventDefault();vertVel-=32;}
if(e.key==='e'||e.key==='k'||e.key==='ArrowDown'&&e.shiftKey){e.preventDefault();vertVel+=32;}
if(e.key==='g'||e.key==='G'){shiftGear();}
});
wrap.focus();
// ---- LYRIC SPAWN ----
function spawnLyric(def){
const el=document.createElement('div');
el.className='lyric';el.textContent=def.text;
el.style.fontSize='1px';el.style.opacity='0';
lyricLayer.appendChild(el);
return{...def,el,anchor:ANCHORS[def.pos]||ANCHORS.bottom,z:LYRIC_SPAWN_Z,active:true};
}
function updateLyrics(eVel){
demoTime+=0.016;
lyricDefs.forEach(def=>{if(!def.fired&&demoTime>=def.ts){def.fired=true;activeLyrics.push(spawnLyric(def));}});
const dead=[];
const cvx=vx+vpOffX,cvy=vy+vpOffY;
activeLyrics.forEach(lyr=>{
lyr.z-=eVel*0.55;
if(lyr.z<=LYRIC_DIE_Z){dead.push(lyr);lyr.el.style.opacity='0';setTimeout(()=>lyr.el.remove(),300);return;}
const approach=Math.max(0,Math.min(1,(LYRIC_SPAWN_Z-lyr.z)/(LYRIC_SPAWN_Z-LYRIC_PEAK_Z)));
const zs=Math.max(lyr.z,LYRIC_PEAK_Z);
const fontSize=lyr.size*(600/zs)*0.55;
const ax=lyr.anchor.x*W,ay=lyr.anchor.y*H;
const ease=approach<0.5?2*approach*approach:1-Math.pow(-2*approach+2,2)/2;
const px=cvx+(ax-cvx)*ease,py=cvy+(ay-cvy)*ease;
let opacity;
if(lyr.z>LYRIC_PEAK_Z){opacity=Math.max(0,Math.min(1,(approach-0.55)/0.45));}
else{const pp=Math.abs(lyr.z-LYRIC_PEAK_Z)/Math.abs(LYRIC_DIE_Z-LYRIC_PEAK_Z);opacity=Math.max(0,1-pp*2.2);}
lyr.el.style.fontSize=Math.max(fontSize,0.5).toFixed(1)+'px';
lyr.el.style.opacity=opacity.toFixed(3);
lyr.el.style.left=px.toFixed(1)+'px';
lyr.el.style.top=py.toFixed(1)+'px';
lyr.el.style.transform='translate(-50%,-50%)';
});
dead.forEach(l=>{const i=activeLyrics.indexOf(l);if(i>-1)activeLyrics.splice(i,1);});
}
// ---- LOOP ----
function loop(){
const g=currentGear();
const defaultVel=g.vel;
vel*=0.88;latVel*=0.90;vertVel*=0.90;
lateral+=latVel*0.009;lateral*=0.955;
vertical+=vertVel*0.009;vertical*=0.955;
const eVel=Math.abs(vel)+defaultVel;
const spd=Math.abs(vel);
dashOff+=eVel*DASH_RATE;
// VP floats OPPOSITE to turn
vpOffX+=((-lateral*W*0.08)-vpOffX)*0.06;
vpOffY+=((-vertical*H*0.08)-vpOffY)*0.06;
// STARS pan opposite to movement (corrected)
camStarX-=lateral*eVel*0.20;
camStarX=((camStarX%SW)+SW)%SW;
camStarY-=vertical*eVel*0.20;
camStarY=((camStarY%SH)+SH)%SH;
camOrbX+=lateral*eVel*0.04;
camOrbY+=vertical*eVel*0.04;
glowPhase+=0.018+defaultVel*0.0003;
orbs.forEach(o=>{o.z-=eVel*0.55;if(o.z<=MIN_Z)Object.assign(o,mkOrb(MAX_Z));});
updateLyrics(eVel);
draw(eVel,spd,defaultVel,g);
document.getElementById('vdisp').textContent=
`${g.name} · vel ${Math.round(spd)} · lat ${lateral.toFixed(1)} · vert ${vertical.toFixed(1)}`;
requestAnimationFrame(loop);
}
// ---- DRAW ----
function draw(eVel,spd,defaultVel,g){
ctx.clearRect(0,0,W,H);
const cvx=vx+vpOffX,cvy=vy+vpOffY;
const PERSPECTIVE=600;
ctx.fillStyle='#04040c';ctx.fillRect(0,0,W,H);
// STARS
stars.forEach(s=>{
let sx=s.wx-camStarX,sy=s.wy-camStarY;
if(sx<-W*0.15)sx+=SW;if(sx>W*1.15)sx-=SW;
if(sy<-H*0.15)sy+=SH;if(sy>H*1.15)sy-=SH;
if(sx<-4||sx>W+4||sy<-4||sy>H+4)return;
ctx.beginPath();ctx.arc(sx,sy,s.r,0,Math.PI*2);
ctx.fillStyle=`rgba(255,255,255,${s.a})`;ctx.fill();
});
// GLOW — pulses, floats with VP
const pulse=1+Math.sin(glowPhase)*0.18;
const glowR=(55+defaultVel*1.1)*pulse;
const glow=ctx.createRadialGradient(cvx,cvy,0,cvx,cvy,glowR);
glow.addColorStop(0,'rgba(12,80,105,0.78)');
glow.addColorStop(0.35,'rgba(8,50,75,0.28)');
glow.addColorStop(1,'rgba(4,4,12,0)');
ctx.fillStyle=glow;ctx.fillRect(0,0,W,H);
// bright core
const coreR=2+Math.sin(glowPhase*1.3)*0.8;
ctx.beginPath();ctx.arc(cvx,cvy,coreR,0,Math.PI*2);
ctx.fillStyle=`rgba(180,230,255,${0.6+Math.sin(glowPhase)*0.3})`;ctx.fill();
// ORBS
[...orbs].sort((a,b)=>b.z-a.z).forEach(o=>{
const scale=PERSPECTIVE/o.z;
const sx=cvx+(o.x-camOrbX*o.z*0.001)*scale;
const sy=cvy+(o.y-camOrbY*o.z*0.001)*scale;
const r=Math.max(o.base*scale*2.5,0.3);
const alpha=Math.min((1-o.z/MAX_Z)*1.4,0.85);
if(sx<-r||sx>W+r||sy<-r||sy>H+r)return;
ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);
ctx.fillStyle=`rgba(${o.r},${o.g},${o.b},${alpha.toFixed(3)})`;ctx.fill();
});
// BENT LINES — CORRECTED direction, vertical bend added
const lineCount=24+Math.floor(spd*0.28);
const lineInt=0.16+Math.min(eVel/80,1)*0.58;
const bendX=-lateral*115; // FLIPPED — field moves against turn
const bendY=-vertical*80; // vertical bend
const baseLen=52+eVel*2.1;
for(let i=0;i<lineCount;i++){
const angle=(i/lineCount)*Math.PI*2;
const len=baseLen*(0.65+Math.random()*0.65);
const ex=cvx+Math.cos(angle)*len;
const ey=cvy+Math.sin(angle)*len;
const mx=(cvx+ex)/2+bendX;
const my=(cvy+ey)/2+bendY;
const pick=Math.floor((angle/(Math.PI*2))*4);
const col=pick===0?`rgba(26,210,155,${lineInt})`:
pick===1?`rgba(232,155,28,${lineInt*0.9})`:
pick===2?`rgba(105,38,200,${lineInt*0.85})`:`rgba(205,72,26,${lineInt*0.8})`;
ctx.beginPath();ctx.moveTo(cvx,cvy);
ctx.quadraticCurveTo(mx,my,ex,ey);
ctx.strokeStyle=col;
ctx.lineWidth=0.4+Math.min(eVel/60,1)*1.4;
ctx.stroke();
}
// TINT — corrected directions
if(Math.abs(lateral)>0.02){
const t=Math.min(Math.abs(lateral)*2.5,0.18);
ctx.fillStyle=lateral>0?`rgba(45,15,145,${t})`:`rgba(185,55,15,${t})`;
ctx.fillRect(0,0,W,H);
}
if(Math.abs(vertical)>0.02){
const t=Math.min(Math.abs(vertical)*2.5,0.12);
ctx.fillStyle=vertical>0?`rgba(15,80,45,${t})`:`rgba(145,15,80,${t})`;
ctx.fillRect(0,0,W,H);
}
// GEAR SPEED RING
if(spd>30){
const ri=Math.min((spd-30)/80,1);
ctx.beginPath();ctx.arc(cvx,cvy,18+ri*78,0,Math.PI*2);
ctx.strokeStyle=g.color.replace(/[\d.]+\)$/,`${ri*0.4})`);
ctx.lineWidth=1+ri*2;ctx.stroke();
}
}
resize();
window.addEventListener('resize',resize);
loop();
</script>
</body>
</html>