6
Coriolis Effect: The Deflecting Force That Isn't
drag ↔ to set the spin rate · tap a panel to launch a puck
idle
144 lines · vanilla
view source
const MAXP = 9, TRL = 80, NDOTS = 46, NSTAR = 36;
let W, H, omega, theta, lastL, seed, cols;
let act, tb, pvx, pvy, trail, tcnt;
let dotR, dotP, starR, starP;
let lastMx, lastMy, downPrev, moved;
function init({ width, height }) {
W = width; H = height;
omega = 0.9; theta = 0; lastL = -10; seed = 0;
act = new Uint8Array(MAXP); tb = new Float32Array(MAXP);
pvx = new Float32Array(MAXP); pvy = new Float32Array(MAXP);
trail = new Float32Array(MAXP * TRL * 2); tcnt = new Int16Array(MAXP);
dotR = new Float32Array(NDOTS); dotP = new Float32Array(NDOTS);
for (let i = 0; i < NDOTS; i++) { dotR[i] = Math.sqrt(Math.random()) * 0.92; dotP[i] = Math.random() * 6.2832; }
starR = new Float32Array(NSTAR); starP = new Float32Array(NSTAR);
for (let i = 0; i < NSTAR; i++) { starR[i] = 1.12 + Math.random() * 1.3; starP[i] = Math.random() * 6.2832; }
cols = [];
for (let i = 0; i < MAXP; i++) cols.push("hsl(" + (i * 41 + 15) + ",90%,65%)");
lastMx = 0; lastMy = 0; downPrev = false; moved = 0;
launch(0.4, 0.45, 0); launch(2.5, 0.4, -0.8);
}
function launch(ang, sp, age) {
for (let i = 0; i < MAXP; i++) if (!act[i]) {
act[i] = 1; tb[i] = age; pvx[i] = Math.cos(ang) * sp; pvy[i] = Math.sin(ang) * sp; tcnt[i] = 0;
return;
}
}
function drawPanel(ctx, x0, y0, pw, ph, rot, time) {
const cx = x0 + pw / 2, cy = y0 + ph / 2, R = Math.min(pw, ph) * 0.40;
ctx.save();
ctx.beginPath(); ctx.rect(x0, y0, pw, ph); ctx.clip();
ctx.fillStyle = rot ? "#0a0d18" : "#070a12"; ctx.fillRect(x0, y0, pw, ph);
ctx.fillStyle = "rgba(255,255,255,0.5)";
const sOff = rot ? -theta : 0;
for (let i = 0; i < NSTAR; i++) {
const a = starP[i] + sOff;
ctx.fillRect(cx + Math.cos(a) * starR[i] * R - 1, cy + Math.sin(a) * starR[i] * R - 1, 2, 2);
}
ctx.fillStyle = "#141f38";
ctx.beginPath(); ctx.arc(cx, cy, R, 0, 6.2832); ctx.fill();
ctx.strokeStyle = "#3d5689"; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = "#5d7bb0";
const dOff = rot ? 0 : theta;
for (let i = 0; i < NDOTS; i++) {
const a = dotP[i] + dOff;
ctx.beginPath(); ctx.arc(cx + Math.cos(a) * dotR[i] * R, cy + Math.sin(a) * dotR[i] * R, 2, 0, 6.2832); ctx.fill();
}
const ct = Math.cos(-theta), st = Math.sin(-theta);
for (let p = 0; p < MAXP; p++) {
if (!act[p]) continue;
const t = time - tb[p], ix = pvx[p] * t, iy = pvy[p] * t;
if (rot) {
const n = tcnt[p], base = p * TRL * 2;
ctx.strokeStyle = cols[p]; ctx.lineWidth = 2;
for (let j = 1; j < n; j++) {
ctx.globalAlpha = 0.08 + 0.8 * (j / n);
ctx.beginPath();
ctx.moveTo(cx + trail[base + (j - 1) * 2] * R, cy + trail[base + (j - 1) * 2 + 1] * R);
ctx.lineTo(cx + trail[base + j * 2] * R, cy + trail[base + j * 2 + 1] * R);
ctx.stroke();
}
ctx.globalAlpha = 1;
const rx = ix * ct - iy * st, ry = ix * st + iy * ct;
ctx.fillStyle = cols[p];
ctx.beginPath(); ctx.arc(cx + rx * R, cy + ry * R, 4, 0, 6.2832); ctx.fill();
} else {
ctx.strokeStyle = "rgba(255,255,255,0.3)"; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + ix * R, cy + iy * R); ctx.stroke();
ctx.fillStyle = cols[p];
ctx.beginPath(); ctx.arc(cx + ix * R, cy + iy * R, 4, 0, 6.2832); ctx.fill();
}
}
ctx.fillStyle = "rgba(255,255,255,0.75)"; ctx.font = "bold 11px monospace";
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
ctx.fillText(rot ? "ROTATING FRAME (on the disc)" : "INERTIAL FRAME (lab view)", x0 + 8, y0 + ph - 10);
ctx.restore();
}
function tick({ ctx, dt, time, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
const vert = H > W;
const pw = vert ? W : W / 2, ph = vert ? H / 2 : H;
const p1x = vert ? 0 : W / 2, p1y = vert ? H / 2 : 0;
const mx = input.mouseX, my = input.mouseY, down = input.mouseDown;
if (down) {
if (downPrev) {
omega += (mx - lastMx) * 0.012;
moved += Math.abs(mx - lastMx) + Math.abs(my - lastMy);
} else moved = 0;
lastMx = mx; lastMy = my;
}
downPrev = down;
if (omega > 3) omega = 3; if (omega < -3) omega = -3;
theta += omega * dt;
for (const c of input.consumeClicks()) {
if (moved > 12) continue;
const inP1 = c.x >= p1x && c.y >= p1y;
const cx = (inP1 ? p1x : 0) + pw / 2, cy = (inP1 ? p1y : 0) + ph / 2, R = Math.min(pw, ph) * 0.40;
let dx = c.x - cx, dy = c.y - cy;
const len = Math.hypot(dx, dy);
if (len < 8) continue;
dx /= len; dy /= len;
if (inP1) {
const cth = Math.cos(theta), sth = Math.sin(theta);
const ndx = dx * cth - dy * sth; dy = dx * sth + dy * cth; dx = ndx;
}
const sp = 0.25 + 0.45 * Math.min(1, len / R);
launch(Math.atan2(dy, dx), sp, time);
}
if (time - lastL > 1.3) {
seed++;
launch(seed * 2.399, 0.3 + 0.2 * ((seed * 0.37) % 1), time);
lastL = time;
}
const ct = Math.cos(-theta), st = Math.sin(-theta);
for (let p = 0; p < MAXP; p++) {
if (!act[p]) continue;
const t = time - tb[p], ix = pvx[p] * t, iy = pvy[p] * t;
if (ix * ix + iy * iy > 1.1) { act[p] = 0; continue; }
const base = p * TRL * 2;
if (tcnt[p] < TRL) {
trail[base + tcnt[p] * 2] = ix * ct - iy * st;
trail[base + tcnt[p] * 2 + 1] = ix * st + iy * ct;
tcnt[p]++;
} else {
for (let j = 0; j < (TRL - 1) * 2; j++) trail[base + j] = trail[base + j + 2];
trail[base + (TRL - 1) * 2] = ix * ct - iy * st;
trail[base + (TRL - 1) * 2 + 1] = ix * st + iy * ct;
}
}
ctx.fillStyle = "#04060c"; ctx.fillRect(0, 0, W, H);
drawPanel(ctx, 0, 0, pw, ph, false, time);
drawPanel(ctx, p1x, p1y, pw, ph, true, time);
ctx.strokeStyle = "#1c2740"; ctx.lineWidth = 2;
ctx.beginPath();
if (vert) { ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2); } else { ctx.moveTo(W / 2, 0); ctx.lineTo(W / 2, H); }
ctx.stroke();
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(PADX(), 10, 252, 56);
ctx.fillStyle = "#fff"; ctx.font = "13px monospace"; ctx.textAlign = "left";
ctx.fillText("Ω = " + omega.toFixed(2) + " rad/s " + (Math.abs(omega) < 0.05 ? "" : omega > 0 ? "(CCW)" : "(CW)"), PADX() + 10, 28);
ctx.fillStyle = "#ffd24d";
ctx.fillText(Math.abs(omega) < 0.05 ? "no spin: no deflection" : omega > 0 ? "N hemisphere: deflects RIGHT" : "S hemisphere: deflects LEFT", PADX() + 10, 46);
ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.font = "10px monospace";
ctx.fillText("drag ↔ to spin · tap to launch", PADX() + 10, 61);
}
function PADX() { return 10; }
Comments (0)
Log in to comment.