<!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>