Skip to content

Instantly share code, notes, and snippets.

@jpatel3
Created March 27, 2026 17:41
Show Gist options
  • Select an option

  • Save jpatel3/c51adcdead70ee89b6964faa3a4b7548 to your computer and use it in GitHub Desktop.

Select an option

Save jpatel3/c51adcdead70ee89b6964faa3a4b7548 to your computer and use it in GitHub Desktop.
Astro-Drop Physics Lab Simulation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astro-Drop Physics Lab</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
overflow: hidden;
width: 100vw;
height: 100vh;
background: #000;
}
canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* --- Overlay UI --- */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.header {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: #fff;
text-shadow: 0 2px 12px rgba(0,0,0,0.6);
}
.header h1 {
font-size: 1.6em;
font-weight: 700;
letter-spacing: -0.02em;
}
.header p {
font-size: 0.85em;
opacity: 0.8;
margin-top: 4px;
font-weight: 400;
}
/* --- Control Panel (left side) --- */
.controls-panel {
position: absolute;
top: 50%;
left: 28px;
transform: translateY(-50%);
pointer-events: all;
background: rgba(15, 15, 25, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 24px;
width: 260px;
color: #fff;
}
.control-section {
margin-bottom: 20px;
}
.control-section:last-child {
margin-bottom: 0;
}
.section-label {
font-size: 0.7em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255,255,255,0.5);
margin-bottom: 10px;
}
/* --- Radio Buttons --- */
.radio-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.radio-option {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s;
font-size: 0.85em;
}
.radio-option:hover {
background: rgba(255,255,255,0.08);
}
.radio-option input[type="radio"] {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
flex-shrink: 0;
cursor: pointer;
position: relative;
transition: border-color 0.15s;
}
.radio-option input[type="radio"]:checked {
border-color: #60a5fa;
}
.radio-option input[type="radio"]:checked::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #60a5fa;
}
.radio-label {
display: flex;
flex-direction: column;
line-height: 1.3;
}
.radio-label .name {
font-weight: 500;
}
.radio-label .detail {
font-size: 0.75em;
color: rgba(255,255,255,0.4);
}
/* --- Checkboxes --- */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.checkbox-option {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s;
font-size: 0.85em;
}
.checkbox-option:hover {
background: rgba(255,255,255,0.08);
}
.checkbox-option input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 4px;
flex-shrink: 0;
cursor: pointer;
position: relative;
transition: all 0.15s;
}
.checkbox-option input[type="checkbox"]:checked {
background: #60a5fa;
border-color: #60a5fa;
}
.checkbox-option input[type="checkbox"]:checked::after {
content: '';
position: absolute;
top: 1px;
left: 4px;
width: 4px;
height: 8px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-option input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-option.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-option.disabled .force-tag {
background: rgba(255,255,255,0.1);
color: rgba(255,255,255,0.4);
}
.force-tag {
font-size: 0.65em;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
background: rgba(96,165,250,0.2);
color: #60a5fa;
margin-left: auto;
flex-shrink: 0;
}
.force-tag.always {
background: rgba(52,211,153,0.2);
color: #34d399;
}
/* --- Drop Button --- */
.drop-btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, #ef4444, #dc2626);
color: #fff;
font-family: 'Inter', sans-serif;
font-size: 1em;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
pointer-events: all;
transition: all 0.15s;
box-shadow: 0 4px 12px rgba(239,68,68,0.4);
}
.drop-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(239,68,68,0.5);
}
.drop-btn:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(239,68,68,0.3);
}
/* --- Stats HUD (bottom center) --- */
.stats-hud {
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
background: rgba(15, 15, 25, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 12px 28px;
color: #fff;
text-align: center;
font-size: 0.9em;
font-weight: 500;
white-space: nowrap;
}
.stats-hud .sub {
font-size: 0.8em;
font-weight: 400;
color: rgba(255,255,255,0.5);
margin-top: 2px;
}
</style>
</head>
<body>
<canvas id="simCanvas"></canvas>
<div class="overlay">
<div class="header">
<h1>Astro-Drop Physics Lab</h1>
<p>Drop objects across the solar system and observe the forces at play</p>
</div>
<div class="controls-panel">
<!-- Location -->
<div class="control-section">
<div class="section-label">Location</div>
<div class="radio-group" id="locationGroup">
<label class="radio-option">
<input type="radio" name="location" value="earth">
<span class="radio-label"><span class="name">Earth</span></span>
</label>
<label class="radio-option">
<input type="radio" name="location" value="moon" checked>
<span class="radio-label"><span class="name">Moon</span></span>
</label>
<label class="radio-option">
<input type="radio" name="location" value="mars">
<span class="radio-label"><span class="name">Mars</span></span>
</label>
</div>
</div>
<!-- Object -->
<div class="control-section">
<div class="section-label">Object</div>
<div class="radio-group" id="objectGroup">
<label class="radio-option">
<input type="radio" name="object" value="ball" checked>
<span class="radio-label"><span class="name">Soccer Ball</span></span>
</label>
<label class="radio-option">
<input type="radio" name="object" value="wood">
<span class="radio-label"><span class="name">Heavy Log</span></span>
</label>
<label class="radio-option">
<input type="radio" name="object" value="feather">
<span class="radio-label"><span class="name">Bird Feather</span></span>
</label>
<label class="radio-option">
<input type="radio" name="object" value="balloon">
<span class="radio-label"><span class="name">Helium Balloon</span></span>
</label>
</div>
</div>
<!-- Forces -->
<div class="control-section">
<div class="section-label">Forces</div>
<div class="checkbox-group">
<label class="checkbox-option disabled">
<input type="checkbox" id="forceGravity" checked disabled>
<span>Gravity</span>
<span class="force-tag always">always on</span>
</label>
<label class="checkbox-option">
<input type="checkbox" id="forceDrag" checked>
<span>Air Drag</span>
<span class="force-tag">optional</span>
</label>
<label class="checkbox-option">
<input type="checkbox" id="forceBuoyancy" checked>
<span>Buoyancy</span>
<span class="force-tag">optional</span>
</label>
</div>
</div>
<button class="drop-btn" onclick="startDrop()">DROP IT</button>
</div>
<div class="stats-hud" id="timerDisplay">
Ready to Drop
<div class="sub">Select your experiment and hit drop</div>
</div>
</div>
<script>
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const timerDisplay = document.getElementById('timerDisplay');
const G_SCALE = 40;
// Physics & Animation State
let lastTime = 0;
let elapsedSimTime = 0;
let isRunning = false;
let hasLanded = false; // true when object hit the ground or floated away
// Object coordinates (will be set relative to canvas size)
let x = 0, y = 0, vx = 0, vy = 0, angle = 0;
// Astronaut Animation State
let armAngle = Math.PI;
// Canvas sizing
let W = 0, H = 0;
let groundY = 0;
let scaleF = 1; // scale factor relative to original 500px design
function resizeCanvas() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
groundY = H - H * 0.1; // ground at bottom 10%
scaleF = Math.min(W, H) / 500;
if (!isRunning && !hasLanded) resetSim();
}
window.addEventListener('resize', resizeCanvas);
const data = {
locations: {
earth: { g: 9.81, density: 1.225, name: "Earth" },
moon: { g: 1.62, density: 0, name: "The Moon" },
mars: { g: 3.72, density: 0.020, name: "Mars" },
},
objects: {
ball: { mass: 0.43, volume: 0.0055, area: 0.038, dragCoeff: 0.47, shape: 'circle', radius: 15, color: '#fff' },
wood: { mass: 5.00, volume: 0.0080, area: 0.060, dragCoeff: 0.80, shape: 'rect', w: 30, h: 40, color: '#8e6e46' },
feather: { mass: 0.0005, volume: 0.00001, area: 0.005, dragCoeff: 1.50, shape: 'feather', color: '#ecf0f1' },
balloon: { mass: 0.0055, volume: 0.0140, area: 0.060, dragCoeff: 0.47, shape: 'balloon', color: '#e74c3c', groundOffset: 22 }
}
};
// --- Helpers to get selected values ---
function getLocation() {
return document.querySelector('input[name="location"]:checked').value;
}
function getObject() {
return document.querySelector('input[name="object"]:checked').value;
}
function isDragEnabled() {
return document.getElementById('forceDrag').checked;
}
function isBuoyancyEnabled() {
return document.getElementById('forceBuoyancy').checked;
}
// Listen for radio/checkbox changes — always reset (even if landed)
document.querySelectorAll('input[name="location"], input[name="object"]').forEach(el => {
el.addEventListener('change', () => { if (!isRunning) resetSim(); });
});
document.getElementById('forceDrag').addEventListener('change', () => { if (!isRunning) resetSim(); });
document.getElementById('forceBuoyancy').addEventListener('change', () => { if (!isRunning) resetSim(); });
// --- ASTRONAUT DRAWING (scaled to canvas) ---
function drawAstronaut() {
ctx.save();
const s = scaleF;
{
const cx = W * 0.6;
const cy = groundY;
ctx.translate(cx, cy);
ctx.scale(s, s);
ctx.fillStyle = '#bdc3c7'; ctx.fillRect(10, -180, 50, 100);
ctx.fillStyle = '#ecf0f1';
ctx.beginPath(); ctx.roundRect(-20, -80, 25, 80, 10); ctx.fill();
ctx.beginPath(); ctx.roundRect(15, -80, 25, 80, 10); ctx.fill();
ctx.fillStyle = '#95a5a6'; ctx.fillRect(-20, -45, 25, 10); ctx.fillRect(15, -45, 25, 10);
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.roundRect(-25, -180, 65, 110, 20); ctx.fill();
ctx.fillStyle = '#bdc3c7'; ctx.fillRect(-15, -150, 40, 30);
ctx.fillStyle = '#e74c3c'; ctx.fillRect(-10, -145, 10, 10);
ctx.fillStyle = '#3498db'; ctx.fillRect(5, -145, 10, 10);
ctx.fillStyle = '#ecf0f1'; ctx.beginPath(); ctx.roundRect(25, -170, 20, 80, 10); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(5, -195, 35, 0, Math.PI*2); ctx.fill();
let visorGrad = ctx.createLinearGradient(-15, -210, 15, -180);
visorGrad.addColorStop(0, '#f1c40f'); visorGrad.addColorStop(1, '#d35400');
ctx.fillStyle = visorGrad; ctx.beginPath(); ctx.arc(-5, -195, 25, 0, Math.PI*2); ctx.fill();
ctx.save();
ctx.translate(-15, -160);
ctx.rotate(armAngle);
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.roundRect(-12, -12, 90, 24, 12); ctx.fill();
ctx.fillStyle = '#95a5a6'; ctx.fillRect(35, -12, 10, 24);
ctx.fillStyle = '#7f8c8d'; ctx.beginPath(); ctx.arc(80, 0, 12, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawScene() {
const locKey = getLocation();
ctx.clearRect(0, 0, W, H);
if (locKey === 'earth') {
let skyGrad = ctx.createLinearGradient(0, 0, 0, groundY);
skyGrad.addColorStop(0, '#1a5276'); skyGrad.addColorStop(1, '#a9cce3');
ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, W, H);
let groundGrad = ctx.createLinearGradient(0, groundY, 0, H);
groundGrad.addColorStop(0, '#2ecc71'); groundGrad.addColorStop(0.7, '#27ae60'); groundGrad.addColorStop(1, '#5c4033');
ctx.fillStyle = groundGrad; ctx.fillRect(0, groundY, W, H - groundY);
} else if (locKey === 'moon') {
let spaceGrad = ctx.createRadialGradient(W/2, H/2, 50, W/2, H/2, Math.max(W,H)*0.8);
spaceGrad.addColorStop(0, '#0a0a1a'); spaceGrad.addColorStop(1, '#000000');
ctx.fillStyle = spaceGrad; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'white';
for(let i = 0; i < 80; i++) {
let sx = (i * 137.5 + 50) % W;
let sy = (i * 73.3 + 20) % (groundY - 20);
ctx.globalAlpha = 0.3 + (i % 3) * 0.25;
ctx.fillRect(sx, sy, 1 + (i % 2), 1 + (i % 2));
}
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#2980b9'; ctx.beginPath(); ctx.arc(W*0.8, H*0.15, 40*scaleF, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#27ae60'; ctx.beginPath(); ctx.arc(W*0.8 - 10*scaleF, H*0.15 - 10*scaleF, 15*scaleF, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#ecf0f1'; ctx.beginPath(); ctx.arc(W*0.8 + 10*scaleF, H*0.15 + 10*scaleF, 10*scaleF, 0, Math.PI*2); ctx.fill();
let groundGrad = ctx.createLinearGradient(0, groundY, 0, H);
groundGrad.addColorStop(0, '#bdc3c7'); groundGrad.addColorStop(1, '#7f8c8d');
ctx.fillStyle = groundGrad; ctx.fillRect(0, groundY, W, H - groundY);
ctx.fillStyle = '#95a5a6'; ctx.beginPath(); ctx.ellipse(W*0.2, groundY + (H-groundY)*0.4, 40*scaleF, 10*scaleF, 0, 0, Math.PI*2); ctx.fill();
} else if (locKey === 'mars') {
let skyGrad = ctx.createLinearGradient(0, 0, 0, groundY);
skyGrad.addColorStop(0, '#c0392b'); skyGrad.addColorStop(1, '#e67e22');
ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, W, H);
let groundGrad = ctx.createLinearGradient(0, groundY, 0, H);
groundGrad.addColorStop(0, '#d35400'); groundGrad.addColorStop(1, '#873600');
ctx.fillStyle = groundGrad; ctx.fillRect(0, groundY, W, H - groundY);
ctx.fillStyle = '#a04000';
ctx.beginPath(); ctx.moveTo(W*0.05, groundY+10); ctx.lineTo(W*0.1, groundY-25*scaleF); ctx.lineTo(W*0.18, groundY+10); ctx.fill();
ctx.fillStyle = '#ba4a00';
ctx.beginPath(); ctx.moveTo(W*0.8, groundY+15); ctx.lineTo(W*0.85, groundY-20*scaleF); ctx.lineTo(W*0.92, groundY+15); ctx.fill();
}
}
function drawObject(drawX, drawY, drawAngle, objKey) {
const obj = data.objects[objKey];
const s = scaleF;
ctx.save();
ctx.translate(drawX, drawY);
ctx.rotate(drawAngle);
ctx.scale(s, s);
ctx.fillStyle = obj.color;
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.beginPath();
if (obj.shape === 'circle') {
// Soccer ball base
const r = obj.radius;
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
// Pentagon pattern
ctx.fillStyle = '#333';
// Center pentagon
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const a = (i * 2 * Math.PI / 5) - Math.PI / 2;
const px = Math.cos(a) * 5;
const py = Math.sin(a) * 5;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.fill();
// Outer pentagons (partial, visible as dark patches)
for (let i = 0; i < 5; i++) {
const a = (i * 2 * Math.PI / 5) - Math.PI / 2;
const cx2 = Math.cos(a) * (r * 0.7);
const cy2 = Math.sin(a) * (r * 0.7);
ctx.beginPath();
for (let j = 0; j < 5; j++) {
const a2 = a + (j * 2 * Math.PI / 5) - Math.PI / 2;
const px = cx2 + Math.cos(a2) * 4;
const py = cy2 + Math.sin(a2) * 4;
if (j === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.fill();
}
// Seam lines from center to outer pentagons
ctx.strokeStyle = '#ccc'; ctx.lineWidth = 0.8;
for (let i = 0; i < 5; i++) {
const a = (i * 2 * Math.PI / 5) - Math.PI / 2;
ctx.beginPath();
ctx.moveTo(Math.cos(a) * 5, Math.sin(a) * 5);
ctx.lineTo(Math.cos(a) * (r * 0.7), Math.sin(a) * (r * 0.7));
ctx.stroke();
}
// Outer connecting seams
for (let i = 0; i < 5; i++) {
const a1 = (i * 2 * Math.PI / 5) - Math.PI / 2;
const a2 = ((i + 1) % 5 * 2 * Math.PI / 5) - Math.PI / 2;
const mid = (a1 + a2) / 2;
ctx.beginPath();
ctx.moveTo(Math.cos(a1) * (r * 0.7), Math.sin(a1) * (r * 0.7));
ctx.quadraticCurveTo(Math.cos(mid) * (r * 0.95), Math.sin(mid) * (r * 0.95),
Math.cos(a2) * (r * 0.7), Math.sin(a2) * (r * 0.7));
ctx.stroke();
}
} else if (obj.shape === 'rect') {
ctx.fillRect(-obj.w/2, -obj.h/2, obj.w, obj.h); ctx.strokeRect(-obj.w/2, -obj.h/2, obj.w, obj.h);
} else if (obj.shape === 'feather') {
ctx.moveTo(0, -25); ctx.bezierCurveTo(20, -10, 15, 20, 0, 35); ctx.bezierCurveTo(-15, 20, -20, -10, 0, -25); ctx.fill();
ctx.beginPath(); ctx.moveTo(0, -35); ctx.lineTo(0, 30); ctx.strokeStyle='#bdc3c7'; ctx.stroke();
} else if (obj.shape === 'balloon') {
// Teardrop balloon shape (no string)
ctx.moveTo(0, -22);
ctx.bezierCurveTo(22, -22, 24, 5, 0, 18);
ctx.bezierCurveTo(-24, 5, -22, -22, 0, -22);
ctx.fill();
ctx.strokeStyle = '#c0392b'; ctx.lineWidth = 1.5;
ctx.stroke();
// Knot at bottom
ctx.beginPath();
ctx.moveTo(-3, 18);
ctx.lineTo(0, 22);
ctx.lineTo(3, 18);
ctx.fillStyle = '#c0392b';
ctx.fill();
// Shine highlight
ctx.beginPath();
ctx.ellipse(-6, -8, 4, 7, -0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fill();
}
ctx.restore();
}
// Compute the world position of the astronaut's palm/glove
function getHandPosition() {
// Astronaut: origin at (W*0.6, groundY), arm pivot at (-15, -160), hand at 80px along armAngle
const cx = W * 0.6;
const cy = groundY;
const pivotX = -15, pivotY = -160;
const handX = pivotX + Math.cos(armAngle) * 80;
const handY = pivotY + Math.sin(armAngle) * 80;
return { x: cx + handX * scaleF, y: cy + handY * scaleF };
}
function resetSim() {
isRunning = false;
hasLanded = false;
elapsedSimTime = 0;
vx = 0; vy = 0; angle = 0;
armAngle = Math.PI;
const locKey = getLocation();
const objKey = getObject();
// Position object at the astronaut's palm
const hand = getHandPosition();
x = hand.x;
y = hand.y;
drawScene();
drawAstronaut();
let hoverY = y;
drawObject(x, hoverY, 0, objKey);
timerDisplay.innerHTML = 'Ready to Drop<div class="sub">Select your experiment and hit drop</div>';
// Reset button text
const btn = document.querySelector('.drop-btn');
btn.textContent = 'DROP IT';
btn.onclick = startDrop;
}
function startDrop() {
resetSim();
isRunning = true;
lastTime = performance.now();
timerDisplay.innerHTML = 'Dropping...<div class="sub">Observing forces in action</div>';
requestAnimationFrame(animate);
}
function onLanded() {
hasLanded = true;
const btn = document.querySelector('.drop-btn');
btn.textContent = 'RESET';
btn.onclick = resetSim;
}
function animate(currentTime) {
if (!isRunning) return;
let dt = (currentTime - lastTime) / 1000;
if (dt > 0.1) dt = 0.016;
lastTime = currentTime;
elapsedSimTime += dt;
const locKey = getLocation();
const objKey = getObject();
const loc = data.locations[locKey];
const obj = data.objects[objKey];
const dragOn = isDragEnabled();
const buoyancyOn = isBuoyancyEnabled();
// Arm Animation
if (armAngle > Math.PI / 2) {
armAngle -= 10 * dt;
if (armAngle < Math.PI / 2) armAngle = Math.PI / 2;
}
{
// Gravity (always on)
let netAccel = loc.g;
// Buoyancy (optional)
if (buoyancyOn) {
let buoyantAccel = (loc.density * obj.volume * loc.g) / obj.mass;
netAccel -= buoyantAccel;
}
// Drag (optional)
if (dragOn) {
let velocity_ms = vy / G_SCALE;
let dragForce = 0.5 * loc.density * (velocity_ms * velocity_ms) * obj.dragCoeff * obj.area;
let dragAccel = dragForce / obj.mass;
if (vy > 0) netAccel -= dragAccel;
else if (vy < 0) netAccel += dragAccel;
}
vy += (netAccel * G_SCALE) * dt;
// Feather aerodynamic behavior
if (objKey === 'feather' && loc.density > 0 && dragOn) {
let t = elapsedSimTime * 4;
angle = Math.cos(t) * 0.8 * (loc.density / 1.225);
vx = Math.sin(t) * 80 * (loc.density / 1.225);
let stallEffect = Math.abs(Math.sin(t)) * 1.5 * (loc.density / 1.225);
vy -= stallEffect * G_SCALE * dt;
} else { vx = 0; angle = 0; }
x += vx * dt;
y += vy * dt;
timerDisplay.innerHTML = `<span style="font-size:0.85em">&#9201;</span> ${elapsedSimTime.toFixed(2)}s`;
let objHalfH = (obj.groundOffset || obj.h / 2 || obj.radius || 30) * scaleF;
if (y >= groundY - objHalfH && vy > 0) {
isRunning = false;
y = groundY - objHalfH;
timerDisplay.innerHTML = `${elapsedSimTime.toFixed(2)}s<div class="sub">It hit the ground!</div>`;
onLanded();
} else if (y < -50) {
isRunning = false;
timerDisplay.innerHTML = `Floated away!`;
onLanded();
}
}
drawScene();
drawAstronaut();
drawObject(x, y, angle, objKey);
if (isRunning) requestAnimationFrame(animate);
}
// Keyboard shortcut: spacebar to drop or reset
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !e.repeat) {
e.preventDefault();
if (hasLanded) {
resetSim();
} else {
startDrop();
}
}
});
// Idle animation loop
let idleRAF;
function idleLoop() {
if (!isRunning && !hasLanded) {
elapsedSimTime += 0.016;
const locKey = getLocation();
const objKey = getObject();
// Recompute hand position for idle bob
const hand = getHandPosition();
x = hand.x;
y = hand.y;
drawScene();
drawAstronaut();
let hoverY = y;
drawObject(x, hoverY, 0, objKey);
} else if (hasLanded) {
// Keep drawing the scene with object on the ground
const locKey = getLocation();
const objKey = getObject();
drawScene();
drawAstronaut();
drawObject(x, y, angle, objKey);
}
idleRAF = requestAnimationFrame(idleLoop);
}
// Init
resizeCanvas();
idleLoop();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment