9

Logic Gate Simulator: Build Circuits Live

Tap the input switches; press NEXT to change circuits

An interactive logic gate simulator: AND, OR, NOT, XOR and NAND gates wired from input switches to output bulbs, with signals propagating as green pulses along the wires. Each gate evaluates with a short delay, so you can watch the truth ripple through the circuit one layer at a time — the HUD shows the live truth-table row for the current inputs. Tap the big switches to toggle inputs, and hit NEXT to cycle four classic circuits, including a half adder (sum is , carry is ) and XOR built from four NANDs.

idle
152 lines · vanilla
view source
const DELAY = 0.32;
let CIRCUITS, ci, sw, shown, targ, waveT, lastUser, lastAuto;
let nodes, outs, depths, wires, gdefs, nIn;
let W = 0, H = 0;

function makeCircuits() {
  return [
    { name: "HALF ADDER", nIn: 2, inL: ["A", "B"],
      inPos: [[0.10, 0.28], [0.10, 0.72]],
      gates: [["XOR", 0.50, 0.28, 0, 1], ["AND", 0.50, 0.72, 0, 1]],
      outs: [{ x: 0.88, y: 0.28, src: 2, label: "SUM" }, { x: 0.88, y: 0.72, src: 3, label: "CARRY" }] },
    { name: "(A AND B) OR C", nIn: 3, inL: ["A", "B", "C"],
      inPos: [[0.09, 0.18], [0.09, 0.48], [0.09, 0.80]],
      gates: [["AND", 0.44, 0.30, 0, 1], ["OR", 0.70, 0.52, 3, 2]],
      outs: [{ x: 0.91, y: 0.52, src: 4, label: "Q" }] },
    { name: "XOR FROM 4 NANDS", nIn: 2, inL: ["A", "B"],
      inPos: [[0.08, 0.24], [0.08, 0.76]],
      gates: [["NAND", 0.34, 0.50, 0, 1], ["NAND", 0.56, 0.26, 0, 2], ["NAND", 0.56, 0.74, 1, 2], ["NAND", 0.78, 0.50, 3, 4]],
      outs: [{ x: 0.93, y: 0.50, src: 5, label: "Q" }] },
    { name: "(A OR B) AND NOT C", nIn: 3, inL: ["A", "B", "C"],
      inPos: [[0.09, 0.16], [0.09, 0.42], [0.09, 0.80]],
      gates: [["OR", 0.42, 0.28, 0, 1], ["NOT", 0.42, 0.80, 2, -1], ["AND", 0.71, 0.52, 3, 4]],
      outs: [{ x: 0.93, y: 0.52, src: 5, label: "Q" }] },
  ];
}

function evalGate(t, a, b) {
  if (t === "AND") return a & b;
  if (t === "OR") return a | b;
  if (t === "NOT") return 1 - a;
  if (t === "XOR") return a ^ b;
  return 1 - (a & b); // NAND
}

function loadCircuit(k, time) {
  ci = k;
  const c = CIRCUITS[k];
  nIn = c.nIn; gdefs = c.gates; outs = c.outs;
  nodes = []; depths = []; wires = [];
  for (let i = 0; i < nIn; i++) { nodes.push({ x: c.inPos[i][0], y: c.inPos[i][1] }); depths.push(0); }
  for (let g = 0; g < gdefs.length; g++) {
    const gt = gdefs[g];
    nodes.push({ x: gt[1], y: gt[2] });
    depths.push(1 + Math.max(depths[gt[3]], gt[4] >= 0 ? depths[gt[4]] : 0));
    wires.push({ a: gt[3], bx: gt[1], by: gt[2], out: false, port: gt[4] >= 0 ? -1 : 0 });
    if (gt[4] >= 0) wires.push({ a: gt[4], bx: gt[1], by: gt[2], out: false, port: 1 });
  }
  for (const o of outs) wires.push({ a: o.src, bx: o.x, by: o.y, out: true, port: 0 });
  sw = new Array(nIn).fill(0); sw[0] = 1;
  targ = new Array(nodes.length).fill(0);
  shown = new Array(nodes.length).fill(0);
  waveT = time;
}

function wirePath(ctx, x1, y1, x2, y2, p) {
  const mx = (x1 + x2) / 2;
  ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(mx, y1); ctx.lineTo(mx, y2); ctx.lineTo(x2, y2); ctx.stroke();
  const l1 = Math.abs(mx - x1), l2 = Math.abs(y2 - y1), l3 = Math.abs(x2 - mx);
  const L = l1 + l2 + l3 || 1; const d = p * L; let qx, qy;
  if (d <= l1) { qx = x1 + Math.sign(mx - x1) * d; qy = y1; }
  else if (d <= l1 + l2) { qx = mx; qy = y1 + Math.sign(y2 - y1) * (d - l1); }
  else { qx = mx + Math.sign(x2 - mx) * (d - l1 - l2); qy = y2; }
  ctx.beginPath(); ctx.arc(qx, qy, 3.5, 0, Math.PI * 2); ctx.fill();
}

function init({ width, height }) {
  W = width; H = height;
  CIRCUITS = makeCircuits();
  lastUser = 0; lastAuto = 0;
  loadCircuit(0, 0);
}

function tick({ ctx, time, width, height, input }) {
  if (width !== W || height !== H) { W = width; H = height; }
  const pax = 14, pay = 60, paw = W - 28, pah = H - 60 - 74;
  const PX = (i) => pax + nodes[i].x * paw, PY = (i) => pay + nodes[i].y * pah;
  const gw = Math.min(60, paw * 0.18), gh = 34, SW = 48;
  const nbw = 104, nbx = W - nbw - 12, nby = H - 44 - 12;

  for (const c of input.consumeClicks()) {
    let hit = false;
    for (let i = 0; i < nIn; i++) {
      if (Math.abs(c.x - PX(i)) <= SW / 2 + 8 && Math.abs(c.y - PY(i)) <= SW / 2 + 8) {
        sw[i] ^= 1; waveT = time; lastUser = time; hit = true;
      }
    }
    if (!hit && c.x >= nbx && c.x <= nbx + nbw && c.y >= nby && c.y <= nby + 44) {
      loadCircuit((ci + 1) % CIRCUITS.length, time); lastUser = time;
    }
  }
  if (time - lastUser > 5 && time - lastAuto > 2.8) {
    sw[(Math.random() * nIn) | 0] ^= 1; waveT = time; lastAuto = time;
  }

  for (let i = 0; i < nIn; i++) targ[i] = sw[i];
  for (let g = 0; g < gdefs.length; g++) {
    const gt = gdefs[g];
    targ[nIn + g] = evalGate(gt[0], targ[gt[3]], gt[4] >= 0 ? targ[gt[4]] : 0);
  }
  for (let i = 0; i < targ.length; i++) if (time - waveT >= depths[i] * DELAY) shown[i] = targ[i];

  ctx.fillStyle = "#0c0f16"; ctx.fillRect(0, 0, W, H);

  ctx.lineWidth = 2;
  for (let j = 0; j < wires.length; j++) {
    const w = wires[j], on = shown[w.a];
    const x1 = PX(w.a) + (w.a < nIn ? SW / 2 : gw / 2), y1 = PY(w.a);
    const x2 = pax + w.bx * paw - (w.out ? 20 : gw / 2);
    const y2 = pay + w.by * pah + (w.port < 0 ? -8 : w.port > 0 ? 8 : 0);
    ctx.strokeStyle = on ? "#3dff7a" : "#3a4150";
    ctx.fillStyle = on ? "#c8ffdd" : "#596275";
    wirePath(ctx, x1, y1, x2, y2, (time * 0.8 + j * 0.19) % 1);
  }

  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  for (let g = 0; g < gdefs.length; g++) {
    const i = nIn + g, x = PX(i), y = PY(i), on = shown[i];
    ctx.fillStyle = "#151b28"; ctx.fillRect(x - gw / 2, y - gh / 2, gw, gh);
    ctx.lineWidth = 2; ctx.strokeStyle = on ? "#3dff7a" : "#5a6377";
    ctx.strokeRect(x - gw / 2, y - gh / 2, gw, gh);
    ctx.fillStyle = on ? "#aaffc8" : "#9aa3b5"; ctx.font = "bold 13px monospace";
    ctx.fillText(gdefs[g][0], x, y);
  }
  for (let i = 0; i < nIn; i++) {
    const x = PX(i), y = PY(i), on = sw[i];
    ctx.fillStyle = on ? "#1f8f4a" : "#222835";
    ctx.fillRect(x - SW / 2, y - SW / 2, SW, SW);
    ctx.lineWidth = 2; ctx.strokeStyle = on ? "#5dffa0" : "#4a5366";
    ctx.strokeRect(x - SW / 2, y - SW / 2, SW, SW);
    ctx.fillStyle = "#fff"; ctx.font = "bold 20px monospace";
    ctx.fillText(String(on), x, y + 6);
    ctx.font = "bold 11px monospace"; ctx.fillStyle = on ? "#d9ffe9" : "#8b93a6";
    ctx.fillText(CIRCUITS[ci].inL[i], x, y - SW / 2 + 10);
  }
  for (const o of outs) {
    const x = pax + o.x * paw, y = pay + o.y * pah, on = shown[o.src];
    if (on) { ctx.fillStyle = "rgba(255,211,77,0.25)"; ctx.beginPath(); ctx.arc(x, y, 26, 0, Math.PI * 2); ctx.fill(); }
    ctx.fillStyle = on ? "#ffd34d" : "#2a3144";
    ctx.beginPath(); ctx.arc(x, y, 15, 0, Math.PI * 2); ctx.fill();
    ctx.lineWidth = 2; ctx.strokeStyle = on ? "#ffe9a8" : "#4a5366"; ctx.stroke();
    ctx.fillStyle = "#cfd6e4"; ctx.font = "bold 11px monospace";
    ctx.fillText(o.label, x, y + 28);
  }

  ctx.fillStyle = "rgba(0,0,0,0.55)"; ctx.fillRect(0, 0, W, 52);
  ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
  ctx.fillStyle = "#9fb4ff"; ctx.font = "bold 13px monospace";
  ctx.fillText(CIRCUITS[ci].name + "  (" + (ci + 1) + "/" + CIRCUITS.length + ")", 12, 20);
  let row = "";
  for (let i = 0; i < nIn; i++) row += CIRCUITS[ci].inL[i] + "=" + sw[i] + " ";
  row += "→ ";
  for (const o of outs) row += o.label + "=" + shown[o.src] + " ";
  ctx.fillStyle = "#fff"; ctx.font = "12px monospace";
  ctx.fillText(row, 12, 40);

  ctx.fillStyle = "rgba(0,0,0,0.65)"; ctx.fillRect(nbx, nby, nbw, 44);
  ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 1;
  ctx.strokeRect(nbx + 0.5, nby + 0.5, nbw - 1, 43);
  ctx.fillStyle = "#fff"; ctx.font = "bold 14px monospace";
  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  ctx.fillText("NEXT ▸", nbx + nbw / 2, nby + 22);

  ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
  ctx.fillStyle = "#7d8699"; ctx.font = "11px monospace";
  ctx.fillText("tap the switches to toggle inputs", 12, H - 16);
}

Comments (0)

Log in to comment.