4
Buoyancy Lab: Float, Sink, Archimedes
drag blocks into the tank · +/− changes the fluid density
idle
133 lines · vanilla
view source
const BTN = 44, PAD = 12, NC = 96, GPX = 520, NB = 4, S = 48;
const MATS = [["wood", 0.60, "#b5854f"], ["ice", 0.92, "#cdeaf6"], ["plastic", 0.95, "#d95fa2"], ["aluminum", 2.70, "#a8b2bd"]];
const FLUIDS = [["fresh water", 1.00, "55,135,225", 0.45], ["salt water", 1.03, "45,160,200", 0.45],
["Dead Sea brine", 1.24, "70,175,185", 0.5], ["glycerin", 1.26, "175,150,75", 0.55], ["mercury", 13.6, "190,198,210", 0.88]];
let W, H, hgt, vel, fluidI, bx, by, bvx, bvy, pf, held, sel, wasDown, pmx, pmy;
let waterY, floorY, tankL, tankR;
function init({ width, height }) {
W = width; H = height; layout();
hgt = new Float32Array(NC); vel = new Float32Array(NC);
fluidI = 0; held = -1; sel = 0; wasDown = false; pmx = 0; pmy = 0;
bx = new Float32Array(NB); by = new Float32Array(NB);
bvx = new Float32Array(NB); bvy = new Float32Array(NB); pf = new Float32Array(NB);
for (let i = 0; i < NB; i++) {
bx[i] = W * (0.2 + 0.2 * i);
const fr = Math.min(1, MATS[i][1] / FLUIDS[0][1]);
by[i] = fr >= 1 ? floorY - S / 2 : waterY - S / 2 + fr * S;
bvx[i] = 0; bvy[i] = 0; pf[i] = fr;
}
for (let i = 0; i < NC; i++) vel[i] = (Math.random() - 0.5) * 8;
}
function layout() { waterY = H * 0.44; floorY = H - 16; tankL = 8; tankR = W - 8; }
function surfAt(x, time) {
let c = ((x - tankL) / (tankR - tankL) * NC) | 0;
if (c < 0) c = 0; if (c > NC - 1) c = NC - 1;
return waterY + hgt[c] + Math.sin(time * 1.4 + x * 0.025) * 2.5;
}
function splash(x, amt) {
const c = ((x - tankL) / (tankR - tankL) * NC) | 0;
for (let o = -3; o <= 3; o++) {
const j = c + o;
if (j >= 0 && j < NC) vel[j] += amt * (1 - Math.abs(o) / 4);
}
}
function arrow(ctx, x, y0, y1, color) {
const d = y1 > y0 ? 1 : -1;
ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(x, y0); ctx.lineTo(x, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x, y1 + d * 8); ctx.lineTo(x - 6, y1); ctx.lineTo(x + 6, y1); ctx.closePath(); ctx.fill();
}
function tick({ ctx, dt, time, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; layout(); }
const mx = input.mouseX, my = input.mouseY, down = input.mouseDown;
const dtc = Math.min(dt, 0.033) || 0.016;
const mX = W - PAD - BTN, pX = mX, mY = H - PAD - 2 * BTN - 8, pY = H - PAD - BTN;
for (const c of input.consumeClicks()) {
if (c.x >= mX && c.x <= mX + BTN && c.y >= mY && c.y <= mY + BTN) fluidI = Math.max(0, fluidI - 1);
else if (c.x >= pX && c.x <= pX + BTN && c.y >= pY && c.y <= pY + BTN) fluidI = Math.min(FLUIDS.length - 1, fluidI + 1);
}
if (down && !wasDown && held < 0)
for (let i = NB - 1; i >= 0; i--)
if (Math.abs(mx - bx[i]) < S / 2 + 10 && Math.abs(my - by[i]) < S / 2 + 10) { held = i; sel = i; break; }
if (!down) held = -1;
const rf = FLUIDS[fluidI][1];
for (let i = 0; i < NB; i++) {
if (i === held) {
bvx[i] = Math.max(-600, Math.min(600, (mx - pmx) / dtc));
bvy[i] = Math.max(-600, Math.min(600, (my - pmy) / dtc));
bx[i] = Math.max(tankL + S / 2, Math.min(tankR - S / 2, mx));
by[i] = Math.max(S / 2, Math.min(floorY - S / 2, my));
pf[i] = Math.max(0, Math.min(1, (by[i] + S / 2 - surfAt(bx[i], time)) / S));
continue;
}
const surf = surfAt(bx[i], time);
const fr = Math.max(0, Math.min(1, (by[i] + S / 2 - surf) / S));
if (pf[i] === 0 && fr > 0 && Math.abs(bvy[i]) > 60) splash(bx[i], bvy[i] * 0.05);
if (fr > 0 && fr < 1) splash(bx[i], bvy[i] * dtc * 0.6);
pf[i] = fr;
bvy[i] += GPX * (1 - rf * fr / MATS[i][1]) * dtc;
const damp = Math.exp(-(0.1 + fr * 4.5) * dtc);
bvy[i] *= damp; bvx[i] *= damp;
bx[i] += bvx[i] * dtc; by[i] += bvy[i] * dtc;
if (bx[i] < tankL + S / 2) { bx[i] = tankL + S / 2; bvx[i] = 0; }
if (bx[i] > tankR - S / 2) { bx[i] = tankR - S / 2; bvx[i] = 0; }
if (by[i] > floorY - S / 2) { by[i] = floorY - S / 2; bvy[i] = 0; }
if (by[i] < S / 2) { by[i] = S / 2; bvy[i] = 0; }
}
pmx = mx; pmy = my; wasDown = down;
for (let i = 0; i < NC; i++) {
const l = hgt[i > 0 ? i - 1 : i], r = hgt[i < NC - 1 ? i + 1 : i];
vel[i] += ((l + r) * 0.5 - hgt[i]) * 0.3; vel[i] *= 0.982;
}
for (let i = 0; i < NC; i++) { hgt[i] += vel[i]; if (hgt[i] > 30) hgt[i] = 30; if (hgt[i] < -30) hgt[i] = -30; }
ctx.fillStyle = "#0a0e18"; ctx.fillRect(0, 0, W, H);
ctx.textAlign = "center"; ctx.textBaseline = "middle";
for (let i = 0; i < NB; i++) {
ctx.fillStyle = MATS[i][2];
ctx.fillRect(bx[i] - S / 2, by[i] - S / 2, S, S);
ctx.strokeStyle = i === sel ? "#ffd24d" : "rgba(0,0,0,0.4)";
ctx.lineWidth = i === sel ? 3 : 1.5;
ctx.strokeRect(bx[i] - S / 2, by[i] - S / 2, S, S);
ctx.fillStyle = "#10141c"; ctx.font = "bold 12px monospace";
ctx.fillText(MATS[i][1].toFixed(2), bx[i], by[i] - 6);
ctx.font = "9px monospace"; ctx.fillText(MATS[i][0], bx[i], by[i] + 9);
}
const fc = FLUIDS[fluidI];
ctx.fillStyle = "rgba(" + fc[2] + "," + fc[3] + ")";
ctx.beginPath(); ctx.moveTo(tankL, floorY);
for (let i = 0; i < NC; i++)
ctx.lineTo(tankL + (i / (NC - 1)) * (tankR - tankL), surfAt(tankL + (i / (NC - 1)) * (tankR - tankL), time));
ctx.lineTo(tankR, floorY); ctx.closePath(); ctx.fill();
ctx.strokeStyle = "rgba(" + fc[2] + ",0.95)"; ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < NC; i++) {
const x = tankL + (i / (NC - 1)) * (tankR - tankL), y = surfAt(x, time);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.strokeStyle = "#39476b"; ctx.lineWidth = 4;
ctx.strokeRect(tankL, 4, tankR - tankL, floorY - 4);
const wF = 46, bF = wF * Math.min(2.2, rf * pf[sel] / MATS[sel][1]);
arrow(ctx, bx[sel] - 10, by[sel], by[sel] + wF, "#ff5560");
if (bF > 2) arrow(ctx, bx[sel] + 10, by[sel], by[sel] - bF, "#39e0e8");
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(PAD, PAD, 235, 70);
ctx.fillStyle = "#fff"; ctx.font = "13px monospace"; ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
ctx.fillText("fluid: " + fc[0] + " ρ=" + fc[1].toFixed(2), PAD + 10, PAD + 20);
ctx.fillText("block: " + MATS[sel][0] + " ρ=" + MATS[sel][1].toFixed(2), PAD + 10, PAD + 38);
ctx.fillText("submerged: " + (pf[sel] * 100).toFixed(0) + "%", PAD + 10, PAD + 56);
ctx.font = "10px monospace"; ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.fillText("drag a block · +/− change fluid", PAD, PAD + 84);
for (let b = 0; b < 2; b++) {
const x = b ? pX : mX, y = b ? pY : mY;
ctx.fillStyle = "rgba(0,0,0,0.65)"; ctx.fillRect(x, y, BTN, BTN);
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 0.5, BTN - 1, BTN - 1);
ctx.fillStyle = "#fff"; ctx.font = "bold 26px ui-sans-serif, system-ui";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(b ? "+" : "−", x + BTN / 2, y + BTN / 2);
}
ctx.font = "10px monospace"; ctx.textAlign = "right"; ctx.textBaseline = "alphabetic";
ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.fillText("fluid ρ", mX - 6, pY + 4);
ctx.textAlign = "left";
}
Comments (0)
Log in to comment.