11
Ideal Gas Law: PV = nRT Piston Lab
drag the piston down · +/− change temperature and particle count
idle
152 lines · vanilla
view source
const BTN = 44, PAD = 12, MAXN = 320, T0 = 9000, GP = 900, FLN = 40, R = 2.5;
let W, H, cw, xL, xR, yTop, yFloor;
let pxA, pyA, vxA, vyA, N, Ttar, Mg, M, pistonY, pistonV, Psm, P0, V0, Tref;
let fx, fy, fa, fI, dragP, wasDown, grabDy, cols, nAcc;
function init({ width, height }) {
W = width; H = height; layout();
N = 150; Ttar = T0; Tref = T0;
const heq = (yFloor - yTop) * 0.55;
pistonY = yFloor - heq; pistonV = 0;
Mg = N * T0 / heq; M = Mg / GP;
P0 = Mg / cw; V0 = cw * heq; Psm = P0;
pxA = new Float32Array(MAXN); pyA = new Float32Array(MAXN);
vxA = new Float32Array(MAXN); vyA = new Float32Array(MAXN);
for (let i = 0; i < MAXN; i++) spawn(i);
fx = new Float32Array(FLN); fy = new Float32Array(FLN); fa = new Float32Array(FLN);
for (let i = 0; i < FLN; i++) fa[i] = 9;
fI = 0; dragP = false; wasDown = false; grabDy = 0; nAcc = 0;
cols = [];
for (let i = 0; i < 16; i++) {
const t = i / 15;
const r = t < 0.5 ? 70 + t * 2 * 185 : 255;
const g = t < 0.5 ? 130 + t * 2 * 125 : 255 - (t - 0.5) * 2 * 105;
const b = t < 0.5 ? 255 : 255 - (t - 0.5) * 2 * 205;
cols.push("rgb(" + (r | 0) + "," + (g | 0) + "," + (b | 0) + ")");
}
}
function layout() {
cw = Math.min(W * 0.72, 430); xL = (W - cw) / 2; xR = xL + cw;
yTop = 50; yFloor = H - 72;
}
function spawn(i) {
pxA[i] = xL + R + Math.random() * (cw - 2 * R);
pyA[i] = pistonY + 10 + Math.random() * (yFloor - pistonY - 20);
const sp = Math.sqrt(2 * Ttar) * (0.5 + Math.random()), a = Math.random() * 6.2832;
vxA[i] = Math.cos(a) * sp; vyA[i] = Math.sin(a) * sp;
}
function flash(x, y) { fx[fI] = x; fy[fI] = y; fa[fI] = 0; fI = (fI + 1) % FLN; }
function inRect(x, y, rx, ry, rw, rh) { return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh; }
function btnPos(i) { return i < 2 ? PAD + i * (BTN + 8) : W - PAD - BTN - (3 - i) * (BTN + 8); }
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) {
W = width; H = height; layout();
if (pistonY > yFloor - 40) pistonY = yFloor - 40;
if (pistonY < yTop + 20) pistonY = yTop + 20;
for (let i = 0; i < N; i++) {
if (pxA[i] < xL + R) pxA[i] = xL + R; if (pxA[i] > xR - R) pxA[i] = xR - R;
if (pyA[i] > yFloor - R) pyA[i] = yFloor - R;
}
}
const mx = input.mouseX, my = input.mouseY, down = input.mouseDown;
const dtf = Math.min(dt, 0.033) || 0.016, bY = H - PAD - BTN;
for (const c of input.consumeClicks()) {
if (inRect(c.x, c.y, btnPos(0), bY, BTN, BTN)) Ttar /= 1.3;
else if (inRect(c.x, c.y, btnPos(1), bY, BTN, BTN)) Ttar *= 1.3;
else if (inRect(c.x, c.y, btnPos(2), bY, BTN, BTN)) N = Math.max(10, N - 15);
else if (inRect(c.x, c.y, btnPos(3), bY, BTN, BTN)) { for (let k = 0; k < 15 && N < MAXN; k++) spawn(N++); }
}
if (down) {
if (inRect(mx, my, btnPos(0), bY, BTN, BTN)) Ttar *= Math.exp(-0.9 * dt);
if (inRect(mx, my, btnPos(1), bY, BTN, BTN)) Ttar *= Math.exp(0.9 * dt);
if (inRect(mx, my, btnPos(2), bY, BTN, BTN)) { nAcc -= 25 * dt; while (nAcc < -1 && N > 10) { N--; nAcc++; } }
if (inRect(mx, my, btnPos(3), bY, BTN, BTN)) { nAcc += 25 * dt; while (nAcc > 1 && N < MAXN) { spawn(N++); nAcc--; } }
}
Ttar = Math.max(T0 * 0.12, Math.min(T0 * 5, Ttar));
if (down && !wasDown && inRect(mx, my, xL - 10, pistonY - 30, cw + 20, 56)) { dragP = true; grabDy = my - pistonY; }
if (!down) dragP = false;
if (dragP) {
const ny = Math.max(yTop + 20, Math.min(yFloor - 40, my - grabDy));
pistonV = Math.max(-1500, Math.min(1500, (ny - pistonY) / dtf)); pistonY = ny;
}
wasDown = down;
let imp = 0, sv2 = 0;
for (let i = 0; i < N; i++) {
pxA[i] += vxA[i] * dtf; pyA[i] += vyA[i] * dtf;
if (pxA[i] < xL + R) { pxA[i] = xL + R; vxA[i] = Math.abs(vxA[i]); imp += 2 * vxA[i]; flash(xL, pyA[i]); }
else if (pxA[i] > xR - R) { pxA[i] = xR - R; vxA[i] = -Math.abs(vxA[i]); imp -= 2 * vxA[i]; flash(xR, pyA[i]); }
if (pyA[i] > yFloor - R) { pyA[i] = yFloor - R; vyA[i] = -Math.abs(vyA[i]); imp -= 2 * vyA[i]; flash(pxA[i], yFloor); }
if (pyA[i] < pistonY + R) {
pyA[i] = pistonY + R;
const old = vyA[i];
vyA[i] = Math.max(2 * pistonV - vyA[i], pistonV + 1);
imp += vyA[i] - old;
flash(pxA[i], pistonY);
}
sv2 += vxA[i] * vxA[i] + vyA[i] * vyA[i];
}
const Tn = sv2 / (2 * N);
const f = Math.pow(Ttar / Tn, 0.5 * Math.min(1, 2.5 * dtf));
for (let i = 0; i < N; i++) { vxA[i] *= f; vyA[i] *= f; }
// x/y mixing (stands in for particle collisions; keeps equipartition)
const mixN = Math.ceil(N * dtf * 2);
for (let k = 0; k < mixN; k++) {
const i = (Math.random() * N) | 0;
const sp = Math.hypot(vxA[i], vyA[i]), a = Math.random() * 6.2832;
vxA[i] = Math.cos(a) * sp; vyA[i] = Math.sin(a) * sp;
}
const hg0 = yFloor - pistonY;
Psm += (imp / dtf / (2 * (cw + hg0)) - Psm) * Math.min(1, 2 * dt);
if (!dragP) {
pistonV += (GP - Psm * cw / M) * dtf; pistonV *= Math.exp(-3 * dtf);
pistonY += pistonV * dtf;
if (pistonY > yFloor - 40) { pistonY = yFloor - 40; pistonV = 0; }
if (pistonY < yTop + 20) { pistonY = yTop + 20; pistonV = 0; }
}
const hg = yFloor - pistonY, Vc = cw * hg, ratio = Psm * Vc / (N * Tn);
ctx.fillStyle = "#070a12"; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#0d1424"; ctx.fillRect(xL, yTop, cw, yFloor - yTop);
const vref = Math.sqrt(2 * T0) * 2.2;
for (let i = 0; i < N; i++) {
const v = Math.sqrt(vxA[i] * vxA[i] + vyA[i] * vyA[i]);
ctx.fillStyle = cols[Math.min(15, (v / vref * 16) | 0)];
ctx.fillRect(pxA[i] - 2, pyA[i] - 2, 4, 4);
}
for (let i = 0; i < FLN; i++) {
if (fa[i] > 0.4) continue;
ctx.fillStyle = "rgba(255,170,60," + (0.7 * (1 - fa[i] / 0.4)).toFixed(2) + ")";
ctx.beginPath(); ctx.arc(fx[i], fy[i], 5, 0, 6.2832); ctx.fill();
fa[i] += dt;
}
ctx.fillStyle = "#67738c"; ctx.fillRect(xL - 2, pistonY - 24, cw + 4, 24);
ctx.fillStyle = "#46506a"; ctx.fillRect(xL - 2, pistonY - 6, cw + 4, 6);
ctx.fillStyle = dragP ? "#ffb84d" : "#8b97b5";
ctx.fillRect(xL + cw / 2 - 44, pistonY - 38, 88, 26);
ctx.fillStyle = "#10141c"; ctx.font = "bold 12px monospace";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText("≡ drag", xL + cw / 2, pistonY - 25);
ctx.strokeStyle = "#39476b"; ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(xL, yTop - 8); ctx.lineTo(xL, yFloor); ctx.lineTo(xR, yFloor); ctx.lineTo(xR, yTop - 8);
ctx.stroke();
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(PAD, PAD, 200, 100);
ctx.fillStyle = "#fff"; ctx.font = "13px monospace"; ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
ctx.fillText("P = " + (Psm / P0).toFixed(2) + " V = " + (Vc / V0).toFixed(2), PAD + 10, PAD + 20);
ctx.fillText("T = " + (Tn / Tref).toFixed(2) + " n = " + N, PAD + 10, PAD + 38);
ctx.fillStyle = "#ffd24d"; ctx.font = "bold 15px monospace";
ctx.fillText("PV/nRT = " + ratio.toFixed(2), PAD + 10, PAD + 62);
ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.font = "10px monospace";
ctx.fillText("pressure = drumbeat of hits", PAD + 10, PAD + 80);
ctx.fillText("drag piston · +/− T and n", PAD + 10, PAD + 94);
const labels = ["T−", "T+", "n−", "n+"];
for (let i = 0; i < 4; i++) {
const x = btnPos(i);
const hot = down && inRect(mx, my, x, bY, BTN, BTN);
ctx.fillStyle = hot ? "rgba(255,160,60,0.85)" : "rgba(0,0,0,0.65)";
ctx.fillRect(x, bY, BTN, BTN);
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, bY + 0.5, BTN - 1, BTN - 1);
ctx.fillStyle = "#fff"; ctx.font = "bold 17px ui-sans-serif, system-ui";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(labels[i], x + BTN / 2, bY + BTN / 2);
}
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
}
Comments (0)
Log in to comment.