Demonstrates true space-style motion by separating rotation from translation. Rotation persists via angular momentum (release ←/→ and the ship keeps spinning), while the ship’s velocity continues drifting independently of where it’s facing. A prominent velocity vector shows the direction of travel, a heading vector shows facing direction, and a spin indicator shows angular velocity—so you can instantly verify drift, stabilization, and control feel.

What this proves


Code for Above

<div style="max-width:1500px;margin:0 auto;">
  <iframe
    title="Angular Momentum + Drift POC"
    scrolling="no"
    style="display:block;margin:0 auto;border:0;width:1500px;height:1500px;overflow:hidden;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.55);background:#000;"
    sandbox="allow-scripts allow-same-origin"
    srcdoc='<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Angular Momentum + Drift POC</title>
<style>
  :root { color-scheme: dark; }
  html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#000; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; }
  #app { width:1500px; height:1500px; margin:0 auto; position:relative; background:#000; }
  canvas { display:block; width:1500px; height:1500px; }

  .top {
    position:absolute; left:14px; top:14px; right:14px;
    display:flex; flex-direction:column; gap:6px;
    pointer-events:none;
  }
  .title {
    display:inline-flex; width:max-content;
    padding:8px 12px;
    border-radius:12px;
    background:rgba(0,0,0,.55);
    border:1px solid rgba(255,255,255,.10);
    box-shadow:0 10px 30px rgba(0,0,0,.35);
    font-weight:650;
    letter-spacing:.02em;
  }
  .desc {
    max-width:1040px;
    padding:8px 12px;
    border-radius:12px;
    background:rgba(0,0,0,.45);
    border:1px solid rgba(255,255,255,.08);
    color:rgba(255,255,255,.78);
    font-size:12px;
    line-height:1.35;
  }

  .hud {
    position:absolute; left:0; right:0; bottom:0;
    height:108px; padding:10px 14px;
    background:linear-gradient(to top, rgba(0,0,0,.88), rgba(0,0,0,.35));
    border-top:1px solid rgba(255,255,255,.08);
    display:flex; gap:14px; align-items:flex-start; justify-content:space-between;
    pointer-events:none;
  }
  .hud .left, .hud .right { display:flex; flex-direction:column; gap:6px; }
  .hud .row { display:flex; gap:10px; align-items:baseline; }
  .k { opacity:.7; font-size:12px; letter-spacing:.02em; }
  .v { font-size:14px; }
  .hint { opacity:.55; font-size:12px; max-width:860px; }
  .pill {
    display:inline-flex; align-items:center; gap:8px;
    padding:6px 10px; border:1px solid rgba(255,255,255,.10);
    border-radius:999px; background:rgba(255,255,255,.03);
  }
</style>
</head>
<body>
  <div id="app">
    <canvas id="c" width="1500" height="1500"></canvas>

    <div class="top">
      <div class="title">Angular Momentum + Drift POC</div>
      <div class="desc">
        Rotation persists via angular momentum, while translation drifts independently. Vectors show velocity vs facing.
        Hold <b>Shift</b> to stabilize rotation (RCS damp).
      </div>
    </div>

    <div class="hud">
      <div class="left">
        <div class="row">
          <span class="pill"><span class="k">Controls</span><span class="v">←/→ rotate (impulse) · ↑ thrust · Space brake · Shift stabilize · R reset</span></span>
        </div>
        <div class="hint">Spin tuning updated: lower torque impulse + tighter ang-vel cap + slightly more coasting damp.</div>
      </div>
      <div class="right">
        <div class="row"><span class="k">Speed</span><span class="v" id="speed">0.00</span></div>
        <div class="row"><span class="k">Ang Vel</span><span class="v" id="angvel">0.00</span></div>
        <div class="row"><span class="k">Heading</span><span class="v" id="heading">0°</span></div>
        <div class="row"><span class="k">Vel Dir</span><span class="v" id="veldir">—</span></div>
      </div>
    </div>
  </div>

<script>
(() => {
  const W = 1500, H = 1500;
  const canvas = document.getElementById("c");
  const ctx = canvas.getContext("2d");

  const uiSpeed = document.getElementById("speed");
  const uiAngVel = document.getElementById("angvel");
  const uiHeading = document.getElementById("heading");
  const uiVelDir = document.getElementById("veldir");

  // ======== Input ========
  const keys = new Set();
  addEventListener("keydown", (e) => {
    if (["ArrowUp","ArrowLeft","ArrowRight","Space","ShiftLeft","ShiftRight","KeyR"].includes(e.code)) e.preventDefault();
    keys.add(e.code);
    if (e.code === "KeyR") reset();
  }, { passive:false });
  addEventListener("keyup", (e) => keys.delete(e.code));

  // ======== World ========
  const stars = [];
  for (let i=0; i<520; i++){
    const big = Math.random() < 0.08;
    stars.push({
      x: Math.random()*W,
      y: Math.random()*H,
      r: big ? (2.0 + Math.random()*1.4) : (0.9 + Math.random()*0.8),
      a: big ? (0.35 + Math.random()*0.35) : (0.12 + Math.random()*0.35)
    });
  }

  function drawStars(){
    ctx.save();
    ctx.fillStyle = "#fff";
    for (const s of stars){
      ctx.globalAlpha = s.a;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.restore();
  }

  function drawGrid(){
    ctx.save();
    ctx.globalAlpha = 0.14;
    ctx.lineWidth = 1;

    for (let x=0; x<=W; x+=150){
      ctx.strokeStyle = "rgba(255,255,255,0.10)";
      ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke();
    }
    for (let y=0; y<=H; y+=150){
      ctx.strokeStyle = "rgba(255,255,255,0.10)";
      ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
    }

    ctx.globalAlpha = 0.09;
    for (let x=0; x<=W; x+=50){
      if (x%150===0) continue;
      ctx.strokeStyle = "rgba(255,255,255,0.07)";
      ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke();
    }
    for (let y=0; y<=H; y+=50){
      if (y%150===0) continue;
      ctx.strokeStyle = "rgba(255,255,255,0.07)";
      ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
    }
    ctx.restore();
  }

  // ======== Ship ========
  const ship = {
    x: W/2, y: H/2,
    vx: 0, vy: 0,
    angle: -Math.PI/2,
    angVel: 0
  };

  function reset(){
    ship.x = W/2; ship.y = H/2;
    ship.vx = 0; ship.vy = 0;
    ship.angle = -Math.PI/2;
    ship.angVel = 0;
    trail.length = 0;
  }

  // ======== Tuning (spin calmer) ========
  // Lower torque, lower cap, and slightly more damping while coasting.
  const TORQUE_IMPULSE = 0.010;   // was 0.020
  const ANG_DAMP_COAST = 0.985;   // was 0.997 (more damping = less runaway)
  const ANG_DAMP_STAB  = 0.78;    // was 0.88 (Shift stabilizes harder/faster)
  const ANG_VEL_MAX    = 0.090;   // was 0.22

  // Translation
  const THRUST     = 0.070;
  const LIN_DAMP   = 0.996;
  const BRAKE      = 0.90;
  const MAX_SPEED  = 18;

  // Trail
  const trail = [];
  const TRAIL_MAX = 28;

  function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
  function len(x,y){ return Math.hypot(x,y); }
  function radToDeg(r){ return (r * 180/Math.PI); }
  function normRad(a){
    while (a <= -Math.PI) a += Math.PI*2;
    while (a >  Math.PI) a -= Math.PI*2;
    return a;
  }

  // ======== Vectors ========
  function drawVelocityVector(){
    const speed = len(ship.vx, ship.vy);
    if (speed < 0.02){
      ctx.save();
      ctx.globalAlpha = 0.45;
      ctx.strokeStyle = "rgba(255,255,255,0.55)";
      ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(ship.x, ship.y, 8, 0, Math.PI*2); ctx.stroke();
      ctx.restore();
      return;
    }

    const nx = ship.vx / speed, ny = ship.vy / speed;
    const L = clamp(34 + speed * 38, 44, 720);
    const a = clamp(0.55 + speed / 10, 0.60, 0.98);

    const x2 = ship.x + nx * L;
    const y2 = ship.y + ny * L;

    ctx.save();
    ctx.globalAlpha = a * 0.55;
    ctx.lineWidth = 14;
    ctx.lineCap = "round";
    ctx.strokeStyle = "rgba(255,255,255,0.35)";
    ctx.beginPath(); ctx.moveTo(ship.x, ship.y); ctx.lineTo(x2, y2); ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.globalAlpha = a;
    ctx.lineWidth = 5;
    ctx.lineCap = "round";
    ctx.strokeStyle = "rgba(255,255,255,0.92)";
    ctx.beginPath(); ctx.moveTo(ship.x, ship.y); ctx.lineTo(x2, y2); ctx.stroke();

    const head = 24;
    const ang = Math.atan2(ny, nx);
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - Math.cos(ang - 0.52) * head, y2 - Math.sin(ang - 0.52) * head);
    ctx.lineTo(x2 - Math.cos(ang + 0.52) * head, y2 - Math.sin(ang + 0.52) * head);
    ctx.closePath();
    ctx.fill();

    ctx.restore();

    ctx.save();
    ctx.globalAlpha = a;
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.beginPath(); ctx.arc(ship.x, ship.y, 4, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }

  function drawHeadingVector(){
    ctx.save();
    ctx.globalAlpha = 0.30;
    ctx.lineWidth = 3;
    ctx.strokeStyle = "rgba(255,255,255,0.55)";
    const L = 110;
    const x2 = ship.x + Math.cos(ship.angle) * L;
    const y2 = ship.y + Math.sin(ship.angle) * L;
    ctx.beginPath(); ctx.moveTo(ship.x, ship.y); ctx.lineTo(x2, y2); ctx.stroke();
    ctx.globalAlpha = 0.25;
    ctx.fillStyle = "rgba(255,255,255,0.55)";
    ctx.beginPath(); ctx.arc(x2, y2, 3, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }

  function drawSpinIndicator(){
    const mag = Math.abs(ship.angVel);
    if (mag < 0.001) return;

    const r = 26;
    const a = clamp(0.12 + mag / 0.09, 0.18, 0.55); // scaled to new cap

    ctx.save();
    ctx.translate(ship.x, ship.y);
    ctx.globalAlpha = a;
    ctx.lineWidth = 2;
    ctx.strokeStyle = "rgba(255,255,255,0.45)";
    ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI*2); ctx.stroke();

    const t = performance.now() * 0.0016 * Math.sign(ship.angVel) * (0.35 + mag*10.0);
    const dx = Math.cos(t) * r;
    const dy = Math.sin(t) * r;
    ctx.fillStyle = "rgba(255,255,255,0.75)";
    ctx.beginPath(); ctx.arc(dx, dy, 3.2, 0, Math.PI*2); ctx.fill();

    ctx.restore();
  }

  // ======== Ship render ========
  function drawShip(){
    ctx.save();
    ctx.translate(ship.x, ship.y);
    ctx.rotate(ship.angle);

    ctx.globalAlpha = 0.95;
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.strokeStyle = "rgba(0,0,0,0.70)";
    ctx.lineWidth = 2;

    ctx.beginPath();
    ctx.moveTo(24, 0);
    ctx.lineTo(-16, 13);
    ctx.lineTo(-10, 0);
    ctx.lineTo(-16, -13);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.globalAlpha = 0.75;
    ctx.fillStyle = "rgba(0,0,0,0.65)";
    ctx.beginPath(); ctx.arc(-12, 0, 3, 0, Math.PI*2); ctx.fill();

    if (keys.has("ArrowUp")){
      ctx.globalAlpha = 0.70;
      ctx.strokeStyle = "rgba(255,255,255,0.60)";
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(-18, 0);
      ctx.lineTo(-36, 0);
      ctx.stroke();
    }

    ctx.restore();
  }

  function drawTrail(){
    ctx.save();
    for (let i=0; i<trail.length; i++){
      const p = trail[i];
      const t = i / trail.length;
      ctx.globalAlpha = (1 - t) * 0.16;
      ctx.fillStyle = "rgba(255,255,255,0.70)";
      ctx.beginPath();
      ctx.arc(p.x, p.y, 2.2, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.restore();
  }

  function wrap(){
    if (ship.x < -60) ship.x = W+60;
    if (ship.x > W+60) ship.x = -60;
    if (ship.y < -60) ship.y = H+60;
    if (ship.y > H+60) ship.y = -60;
  }

  // ======== Loop ========
  let last = performance.now();
  function frame(now){
    const dt = Math.min(32, now-last) / 16.6667;
    last = now;

    // torque impulses
    if (keys.has("ArrowLeft"))  ship.angVel -= TORQUE_IMPULSE * dt;
    if (keys.has("ArrowRight")) ship.angVel += TORQUE_IMPULSE * dt;

    ship.angVel = clamp(ship.angVel, -ANG_VEL_MAX, ANG_VEL_MAX);

    // coasting damping
    ship.angVel *= Math.pow(ANG_DAMP_COAST, dt);

    // stabilization
    if (keys.has("ShiftLeft") || keys.has("ShiftRight")){
      ship.angVel *= Math.pow(ANG_DAMP_STAB, dt);
    }

    ship.angle = normRad(ship.angle + ship.angVel * dt);

    // thrust
    if (keys.has("ArrowUp")){
      ship.vx += Math.cos(ship.angle) * THRUST * dt;
      ship.vy += Math.sin(ship.angle) * THRUST * dt;
    }

    // brake translation only
    if (keys.has("Space")){
      ship.vx *= Math.pow(BRAKE, dt);
      ship.vy *= Math.pow(BRAKE, dt);
    }

    // cap speed
    const sp = len(ship.vx, ship.vy);
    if (sp > MAX_SPEED){
      ship.vx = (ship.vx/sp) * MAX_SPEED;
      ship.vy = (ship.vy/sp) * MAX_SPEED;
    }

    ship.vx *= Math.pow(LIN_DAMP, dt);
    ship.vy *= Math.pow(LIN_DAMP, dt);

    // integrate
    ship.x += ship.vx * dt * 4;
    ship.y += ship.vy * dt * 4;
    wrap();

    // trail
    trail.push({ x: ship.x, y: ship.y });
    if (trail.length > TRAIL_MAX) trail.shift();

    // render
    ctx.clearRect(0,0,W,H);
    drawStars();
    drawGrid();
    drawTrail();
    drawVelocityVector();
    drawHeadingVector();
    drawSpinIndicator();
    drawShip();

    // HUD
    const speed = len(ship.vx, ship.vy);
    uiSpeed.textContent = speed.toFixed(2);
    uiAngVel.textContent = ship.angVel.toFixed(3);

    const headingDeg = (radToDeg(ship.angle) + 360) % 360;
    uiHeading.textContent = headingDeg.toFixed(0) + "°";

    const velAng = (radToDeg(Math.atan2(ship.vy, ship.vx)) + 360) % 360;
    uiVelDir.textContent = (speed < 0.02 ? "—" : velAng.toFixed(0) + "°");

    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>'>
  </iframe>
</div>