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
- Angular momentum is real (rotation coasts)
- Drift is real (movement direction ≠ facing)
- Stabilization is an explicit action (Shift damp)
- Visual signals replace UI clutter (vectors + spin ring)
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>