5
RC Circuit: Charging a Capacitor
Tap the switch; + / − buttons set R and C
idle
165 lines · vanilla
view source
const PAD = 12, BTN = 44, NE = 60, NS = 300, SDT = 1 / 30, TAU2 = Math.PI * 2;
const R_OPTS = [10, 20, 47, 100]; // kΩ
const C_OPTS = [47, 100, 220, 470]; // µF
const V0 = 9;
const BL = ["R−", "R+", "C−", "C+"];
let W, H, rIdx, cIdx, Vc, charging, lastToggle, chart, chartIdx, elec, sampleAcc, pos;
function tau() { return (R_OPTS[rIdx] * C_OPTS[cIdx]) / 1000; }
function hit(px, py, x, y, w, h) { return px >= x && px <= x + w && py >= y && py <= y + h; }
function yOf(v, chY, chH) { return chY + chH - 5 - (v / (V0 * 1.06)) * (chH - 14); }
function init({ ctx, width, height }) {
W = width; H = height;
rIdx = 1; cIdx = 1;
chart = new Float32Array(NS); chartIdx = 0; sampleAcc = 0;
elec = new Float32Array(NE);
for (let i = 0; i < NE; i++) elec[i] = i / NE;
pos = new Float32Array(2);
// prefill 10s of history (charge then auto-flip) so frame 1 shows the exponential
const tu = tau(), t0 = -NS * SDT;
let v = 0, ch = true, lt = t0;
for (let i = 0; i < NS; i++) {
const t = t0 + i * SDT;
if (t - lt >= 8) { ch = !ch; lt = t; }
v += ((ch ? V0 : 0) - v) * (1 - Math.exp(-SDT / tu));
chart[i] = v;
}
Vc = v; charging = ch; lastToggle = lt;
}
function drawButton(ctx, x, y, label, hot) {
ctx.fillStyle = hot ? "rgba(255,160,60,0.85)" : "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 15px monospace"; ctx.textAlign = "center";
ctx.fillText(label, x + BTN / 2, y + BTN / 2 + 5);
}
function tick({ ctx, dt, time, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
const by = H - PAD - BTN;
const bx0 = W - PAD - 4 * BTN - 3 * 8;
const chH = Math.min(96, (H * 0.2) | 0), chY = by - 14 - chH, chX = PAD, chW = W - 2 * PAD;
const x0 = 42, x1 = W - 46, y0 = PAD + 100, y1 = chY - 20;
const ym = ((y0 + y1) / 2) | 0;
const swX = x0 + (x1 - x0) * 0.32, rzX = x0 + (x1 - x0) * 0.74;
for (const c of input.consumeClicks()) {
if (hit(c.x, c.y, swX - 32, y0 - 36, 64, 64)) { charging = !charging; lastToggle = time; }
else for (let b = 0; b < 4; b++) {
if (hit(c.x, c.y, bx0 + b * (BTN + 8), by, BTN, BTN)) {
if (b === 0) rIdx = Math.max(0, rIdx - 1);
else if (b === 1) rIdx = Math.min(3, rIdx + 1);
else if (b === 2) cIdx = Math.max(0, cIdx - 1);
else cIdx = Math.min(3, cIdx + 1);
}
}
}
if (time - lastToggle > 8) { charging = !charging; lastToggle = time; }
const tu = tau();
const target = charging ? V0 : 0;
Vc += (target - Vc) * (1 - Math.exp(-dt / tu));
const ImA = (target - Vc) / R_OPTS[rIdx];
sampleAcc += dt;
while (sampleAcc >= SDT) {
sampleAcc -= SDT;
chart[chartIdx] = Vc;
chartIdx = (chartIdx + 1) % NS;
}
ctx.fillStyle = "#0b0f15"; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "#46566e"; ctx.lineWidth = 3;
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
// electrons slow exponentially as the capacitor charges; reverse on discharge
const frac = Math.abs(ImA) * R_OPTS[rIdx] / V0;
const per = 2 * ((x1 - x0) + (y1 - y0));
const ds = ((ImA >= 0 ? 1 : -1) * (8 + 240 * frac) * dt) / per;
for (let i = 0; i < NE; i++) { elec[i] += ds; elec[i] -= Math.floor(elec[i]); }
ctx.fillStyle = "#7fd4ff";
for (let i = 0; i < NE; i++) {
posOnLoop(elec[i], x0, y0, x1, y1);
const ex = pos[0], ey = pos[1];
if (ey === y0 && (Math.abs(ex - swX) < 28 || Math.abs(ex - rzX) < 32)) continue;
if (ex === x0 && Math.abs(ey - ym) < 18) continue;
if (ex === x1 && Math.abs(ey - ym) < 16) continue;
ctx.beginPath(); ctx.arc(ex, ey, 2.4, 0, TAU2); ctx.fill();
}
// resistor
ctx.strokeStyle = "#c08a50"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(rzX - 28, y0);
for (let i = 0; i < 6; i++) ctx.lineTo(rzX - 28 + (56 / 6) * (i + 0.5), y0 + (i % 2 ? 8 : -8));
ctx.lineTo(rzX + 28, y0); ctx.stroke();
ctx.fillStyle = "#9fb6d4"; ctx.font = "11px monospace"; ctx.textAlign = "center";
ctx.fillText(`R ${R_OPTS[rIdx]} kΩ`, rzX, y0 + 22);
// battery (left edge)
ctx.fillStyle = "#0b0f15"; 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 = "#9fb6d4"; ctx.font = "11px monospace";
ctx.fillText(`${V0} V`, x0 + 6, ym + 34);
// switch (top wire) with pulsing tap ring
ctx.fillStyle = "#0b0f15"; ctx.fillRect(swX - 26, y0 - 8, 52, 16);
ctx.fillStyle = "#ffd17a";
ctx.beginPath(); ctx.arc(swX - 20, y0, 4, 0, TAU2); ctx.fill();
ctx.beginPath(); ctx.arc(swX + 20, y0, 4, 0, TAU2); ctx.fill();
const ang = charging ? 0 : -0.55;
ctx.strokeStyle = "#ffd17a"; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(swX - 20, y0);
ctx.lineTo(swX - 20 + 40 * Math.cos(ang), y0 + 40 * Math.sin(ang)); ctx.stroke();
ctx.strokeStyle = `rgba(255,209,122,${(0.35 + 0.2 * Math.sin(time * 3)).toFixed(2)})`;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(swX, y0 - 6, 30, 0, TAU2); ctx.stroke();
ctx.fillStyle = charging ? "#9fe87a" : "#ffb347"; ctx.font = "bold 11px monospace";
ctx.fillText(charging ? "CHARGING" : "DISCHARGING", swX, y0 + 36);
// capacitor (right edge) with charge symbols filling in
ctx.fillStyle = "#0b0f15"; ctx.fillRect(x1 - 26, ym - 11, 52, 22);
ctx.strokeStyle = "#cfe0ff"; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(x1 - 22, ym - 6); ctx.lineTo(x1 + 22, ym - 6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1 - 22, ym + 6); ctx.lineTo(x1 + 22, ym + 6); ctx.stroke();
const q = Math.round(8 * Vc / V0);
ctx.font = "bold 11px monospace";
for (let i = 0; i < q; i++) {
const px = x1 - 18 + i * 5.2;
ctx.fillStyle = "#ffb347"; ctx.fillText("+", px, ym - 12);
ctx.fillStyle = "#7fd4ff"; ctx.fillText("−", px, ym + 21);
}
ctx.fillStyle = "#9fb6d4"; ctx.font = "11px monospace";
ctx.fillText(`C ${C_OPTS[cIdx]} µF`, x1 - 6, ym + 38);
// strip chart of V_C(t)
ctx.fillStyle = "rgba(0,0,0,0.55)"; ctx.fillRect(chX, chY, chW, chH);
const y632 = yOf(0.632 * V0, chY, chH);
ctx.setLineDash([5, 4]);
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(chX, y632); ctx.lineTo(chX + chW, y632); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "rgba(255,255,255,0.6)"; ctx.font = "10px monospace"; ctx.textAlign = "left";
ctx.fillText("63.2% of V0 (t = τ)", chX + 6, y632 - 4);
ctx.fillText("V_C(t) — last 10 s", chX + 6, chY + 12);
ctx.strokeStyle = "#9fe87a"; ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < NS; i++) {
const px = chX + (i / (NS - 1)) * chW;
const py = yOf(chart[(chartIdx + i) % NS], chY, chH);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// buttons
for (let b = 0; b < 4; b++) {
const bx = bx0 + b * (BTN + 8);
const hot = input.mouseDown && hit(input.mouseX, input.mouseY, bx, by, BTN, BTN);
drawButton(ctx, bx, by, BL[b], hot);
}
ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.font = "11px monospace"; ctx.textAlign = "right";
ctx.fillText("τ = RC", bx0 - 8, by + BTN / 2 + 4);
// HUD
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(PAD, PAD, 224, 84);
ctx.font = "12px monospace"; ctx.textAlign = "left";
ctx.fillStyle = "#9fe87a";
ctx.fillText(`Vc ${Vc.toFixed(2)} V I ${ImA >= 0 ? "+" : ""}${ImA.toFixed(2)} mA`, PAD + 10, PAD + 20);
ctx.fillStyle = "#fff";
ctx.fillText(`R ${R_OPTS[rIdx]} kΩ · C ${C_OPTS[cIdx]} µF`, PAD + 10, PAD + 38);
ctx.fillStyle = "#ffd17a";
ctx.fillText(`τ = RC = ${tu.toFixed(2)} s`, PAD + 10, PAD + 56);
ctx.fillStyle = "#9fb6d4";
ctx.fillText(`t since flip: ${((time - lastToggle) / tu).toFixed(1)} τ`, PAD + 10, PAD + 74);
}
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;
}
Comments (0)
Log in to comment.