5
Ohm's Law Circuit Simulator
Drag the V and R sliders
idle
142 lines · vanilla
view source
const PAD = 12, NE = 80, TAU = Math.PI * 2;
let W, H, V, R, elec, activeSlider, prevDown, pos;
function init({ ctx, width, height }) {
W = width; H = height;
V = 12; R = 20;
elec = new Float32Array(NE);
for (let i = 0; i < NE; i++) elec[i] = i / NE;
activeSlider = -1; prevDown = false;
pos = new Float32Array(2);
ctx.fillStyle = "#0a0e14"; ctx.fillRect(0, 0, W, H);
}
function posOnLoop(s, x0, y0, x1, y1) {
const w = x1 - x0, h = y1 - y0, P = 2 * (w + h);
let d = (s - Math.floor(s)) * P;
if (d < w) { pos[0] = x0 + d; pos[1] = y0; return; }
d -= w;
if (d < h) { pos[0] = x1; pos[1] = y0 + d; return; }
d -= h;
if (d < w) { pos[0] = x1 - d; pos[1] = y1; return; }
d -= w;
pos[0] = x0; pos[1] = y1 - d;
}
function fmtA(I) { return I >= 1 ? I.toFixed(2) + " A" : (I * 1000).toFixed(0) + " mA"; }
function fmtW(P) { return P >= 1 ? P.toFixed(1) + " W" : (P * 1000).toFixed(0) + " mW"; }
function drawResistor(ctx, cx, y, hw, t) {
ctx.save();
ctx.shadowColor = "rgba(255,150,40,0.95)";
ctx.shadowBlur = 4 + 28 * t;
ctx.strokeStyle = `rgb(${(90 + 165 * t) | 0},${(25 + 150 * t) | 0},${(15 + 45 * t) | 0})`;
ctx.lineWidth = 3.5;
ctx.beginPath();
ctx.moveTo(cx - hw, y);
const n = 6, seg = (2 * hw) / n;
for (let i = 0; i < n; i++) ctx.lineTo(cx - hw + seg * (i + 0.5), y + (i % 2 ? 9 : -9));
ctx.lineTo(cx + hw, y);
ctx.stroke();
ctx.restore();
}
function drawSlider(ctx, y, sh, name, valStr, frac, active, tx0, tx1) {
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(PAD, y, W - 2 * PAD, sh - 6);
ctx.fillStyle = "#fff"; ctx.font = "bold 14px monospace"; ctx.textAlign = "left";
ctx.fillText(name, PAD + 10, y + 20);
ctx.font = "11px monospace"; ctx.fillStyle = "#9fb6d4";
ctx.fillText(valStr, PAD + 10, y + 36);
ctx.fillStyle = "#26344a";
ctx.fillRect(tx0, y + sh / 2 - 7, tx1 - tx0, 8);
ctx.fillStyle = active ? "#ffb347" : "#5a8de0";
ctx.fillRect(tx0, y + sh / 2 - 7, (tx1 - tx0) * frac, 8);
ctx.fillStyle = active ? "#ffd17a" : "#cfe0ff";
ctx.beginPath();
ctx.arc(tx0 + (tx1 - tx0) * frac, y + sh / 2 - 3, 11, 0, TAU);
ctx.fill();
}
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
const sh = 52, ry1 = H - 2 * sh - 6, ry2 = H - sh - 2;
const md = input.mouseDown;
if (md && !prevDown) {
if (input.mouseY >= ry1 && input.mouseY < ry1 + sh - 4) activeSlider = 0;
else if (input.mouseY >= ry2 && input.mouseY < ry2 + sh) activeSlider = 1;
else activeSlider = -1;
}
if (!md) activeSlider = -1;
prevDown = md;
const tx0 = 96, tx1 = W - PAD - 16;
if (activeSlider >= 0) {
const f = Math.max(0, Math.min(1, (input.mouseX - tx0) / (tx1 - tx0)));
if (activeSlider === 0) V = 1 + f * 23; else R = 1 + f * 99;
}
input.consumeClicks();
const I = V / R, P = V * I;
const glow = Math.min(1, Math.log1p(P) / Math.log1p(580));
const gw = Math.min(180, W * 0.44), gh = Math.min(122, H * 0.24);
const gx = W - PAD - gw, gy = PAD;
const x0 = 38, x1 = W - 38;
const y0 = Math.max(118, gy + gh + 18), y1 = ry1 - 24;
const ym = ((y0 + y1) / 2) | 0, xm = (x0 + x1) / 2;
const rhw = Math.min(58, (x1 - x0) * 0.2);
ctx.fillStyle = "#0a0e14"; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "#46566e"; ctx.lineWidth = 3;
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
// electrons: count and speed both scale with I = V/R
const count = Math.min(NE, Math.round(8 + 26 * Math.sqrt(I)));
const per = 2 * ((x1 - x0) + (y1 - y0));
const ds = (Math.min(340, 26 + 64 * Math.sqrt(I)) * dt) / per;
for (let i = 0; i < NE; i++) { elec[i] += ds; if (elec[i] >= 1) elec[i] -= 1; }
ctx.fillStyle = "#7fd4ff";
const stride = NE / count;
for (let i = 0; i < count; i++) {
posOnLoop(elec[(i * stride) | 0], x0, y0, x1, y1);
const ex = pos[0], ey = pos[1];
if (ey === y0 && Math.abs(ex - xm) < rhw + 4) continue;
if (ex === x0 && Math.abs(ey - ym) < 18) continue;
if (ex === x1 && Math.abs(ey - ym) < 17) continue;
ctx.beginPath(); ctx.arc(ex, ey, 2.4, 0, TAU); ctx.fill();
}
drawResistor(ctx, xm, y0, rhw, glow);
ctx.fillStyle = "#9fb6d4"; ctx.font = "11px monospace"; ctx.textAlign = "center";
ctx.fillText(`R = ${R.toFixed(0)} Ω`, xm, y0 + 24);
// battery
ctx.fillStyle = "#0a0e14"; ctx.fillRect(x0 - 11, ym - 15, 22, 30);
ctx.strokeStyle = "#e8e8e8"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(x0 - 14, ym - 6); ctx.lineTo(x0 + 14, ym - 6); ctx.stroke();
ctx.lineWidth = 6;
ctx.beginPath(); ctx.moveTo(x0 - 7, ym + 6); ctx.lineTo(x0 + 7, ym + 6); ctx.stroke();
ctx.fillStyle = "#ffd17a"; ctx.font = "12px monospace"; ctx.textAlign = "left";
ctx.fillText("+", x0 + 18, ym - 9);
ctx.fillText("−", x0 + 18, ym + 15);
ctx.fillStyle = "#9fb6d4"; ctx.textAlign = "center"; ctx.font = "11px monospace";
ctx.fillText(`${V.toFixed(1)} V`, x0 + 6, ym + 34);
// ammeter
ctx.fillStyle = "#101826";
ctx.beginPath(); ctx.arc(x1, ym, 15, 0, TAU); ctx.fill();
ctx.strokeStyle = "#7fd4ff"; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = "#7fd4ff"; ctx.font = "bold 13px monospace";
ctx.fillText("A", x1, ym + 4);
ctx.fillStyle = "#9fb6d4"; ctx.font = "11px monospace";
ctx.fillText(fmtA(I), x1 - 6, ym + 33);
// V-I graph with sliding operating point
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(gx, gy, gw, gh);
ctx.strokeStyle = "rgba(255,255,255,0.25)"; ctx.lineWidth = 1;
ctx.strokeRect(gx + 0.5, gy + 0.5, gw - 1, gh - 1);
const lx = gx + 24, lyB = gy + gh - 16, lw = gw - 34, lh = gh - 32;
const ImaxG = (24 / R) * 1.12;
ctx.strokeStyle = "rgba(255,255,255,0.4)";
ctx.beginPath(); ctx.moveTo(lx, gy + 12); ctx.lineTo(lx, lyB); ctx.lineTo(lx + lw, lyB); ctx.stroke();
ctx.strokeStyle = "#5a8de0"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(lx, lyB);
ctx.lineTo(lx + lw, lyB - ((24 / R) / ImaxG) * lh); ctx.stroke();
ctx.fillStyle = "#ffb347";
ctx.beginPath(); ctx.arc(lx + (V / 24) * lw, lyB - (I / ImaxG) * lh, 4.5, 0, TAU); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.65)"; ctx.font = "10px monospace";
ctx.textAlign = "left"; ctx.fillText("I = V/R", lx + 6, gy + 14);
ctx.textAlign = "right"; ctx.fillText("V→24", gx + gw - 6, lyB + 12);
// HUD
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(PAD, PAD, 148, 92);
ctx.fillStyle = "#fff"; ctx.font = "13px monospace"; ctx.textAlign = "left";
ctx.fillText(`V = ${V.toFixed(1)} V`, PAD + 10, PAD + 22);
ctx.fillText(`R = ${R.toFixed(0)} Ω`, PAD + 10, PAD + 40);
ctx.fillStyle = "#7fd4ff";
ctx.fillText(`I = ${fmtA(I)}`, PAD + 10, PAD + 58);
ctx.fillStyle = "#ffb347";
ctx.fillText(`P = ${fmtW(P)}`, PAD + 10, PAD + 76);
drawSlider(ctx, ry1, sh, "V", `${V.toFixed(1)} V`, (V - 1) / 23, activeSlider === 0, tx0, tx1);
drawSlider(ctx, ry2, sh, "R", `${R.toFixed(0)} Ω`, (R - 1) / 99, activeSlider === 1, tx0, tx1);
}
Comments (0)
Log in to comment.