6

Coriolis Effect: The Deflecting Force That Isn't

drag ↔ to set the spin rate · tap a panel to launch a puck

A Coriolis effect simulation shown the honest way: the same puck in two frames at once. In the inertial lab view the puck travels dead straight while the disc turns beneath it; in the rotating frame that identical motion curves sideways with acceleration — a deflection that exists only because the observer is spinning. Drag horizontally to change (reverse it to flip hemispheres and the deflection direction, which is why cyclones spin opposite ways north and south); tap either panel to launch pucks.

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.