5

RC Circuit: Charging a Capacitor

Tap the switch; + / − buttons set R and C

An rc circuit charging simulation: flip the switch and the capacitor charges through the resistor following , then drains as on discharge. Charge symbols fill the capacitor plates while the electron flow in the wire slows exponentially, and the strip chart marks the time constant at the dashed 63.2% line. Tap the big switch to toggle charge/discharge (it auto-flips every 8 s if idle) and use the R and C buttons to stretch or shrink live.

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.