summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Harrison <[email protected]>2026-06-15 12:47:45 -0700
committerPaul Harrison <[email protected]>2026-06-15 12:47:45 -0700
commitd639e402cd8b27cf839017080ce9b995b5b80452 (patch)
tree02f82677184c7c2166ca6bc3165629fca5d4c2c4
logic simulator part 1main
-rw-r--r--gatesim.js534
-rw-r--r--index.html11
2 files changed, 545 insertions, 0 deletions
diff --git a/gatesim.js b/gatesim.js
new file mode 100644
index 0000000..45dbbe5
--- /dev/null
+++ b/gatesim.js
@@ -0,0 +1,534 @@
+const guiTools = Object.freeze({
+ FINGER: {
+ "key": "1",
+ "cursor": "pointer"
+ },
+ PAN: {
+ "key": "2",
+ "cursor": "move"
+ },
+ POINTER: {
+ "key": "3",
+ "cursor": "default"
+ }
+});
+
+function addPos(a, b) {
+ return {
+ x: a.x + b.x,
+ y: a.y + b.y
+ };
+}
+
+function mapRange(value, inMin, inMax, outMin, outMax) {
+ return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
+}
+
+function centerY(i, n, h, p) {
+ if (n === 1) return h / 2;
+ return p + (i * (h - 2 * p)) / (n - 1);
+}
+
+class Simulator {
+ constructor(canvas) {
+ this.steps = [];
+ this.components = [];
+
+ this.canvas = canvas;
+ this.dc = this.canvas.getContext("2d");
+ this.panpos = {x: 0, y: 0};
+ this.canvas.addEventListener("click", e => {
+ this.onClick(e);
+ });
+ this.canvas.addEventListener("keypress", e => {
+ this.onKeyPress(e);
+ });
+ this.changeTool("FINGER");
+ }
+
+ pushStep(step) {
+ this.steps.push(step);
+ }
+
+ executeStep() {
+ var step = this.steps.pop();
+ if (step != undefined) {
+ step();
+ }
+ }
+
+ addComponent(component) {
+ component.simulator = this;
+ this.components.push(component);
+ component.initCanvas();
+ }
+
+ addComponents(components) {
+ components.forEach(component => {
+ this.addComponent(component);
+ })
+ }
+
+ draw() {
+ var wiresToDraw = [];
+
+ this.components.forEach(component => {
+ // draw component
+ component.draw();
+ this.dc.drawImage(component.canvas, component.position.x, component.position.y);
+
+ // draw component ports
+ var ports = [component.inputPorts, component.outputPorts].map(Object.values).flat();
+ var leftPortCount = ports.filter(port => port.style.position.x == "left").length;
+ var rightPortCount = ports.filter(port => port.style.position.x == "right").length;
+ var leftPortCounter = 0;
+ var rightPortCounter = 0;
+
+ ports.forEach(port => {
+ var ppos = {
+ x: component.position.x,
+ y: component.position.y
+ };
+
+ if (typeof port.style.position.x == Number) {
+ ppos.x += port.style.position.x;
+ } else if (port.style.position.x == "left") {
+ ppos.x += 0;
+ } else if (port.style.position.x == "right") {
+ ppos.x += component.constructor.size.x;
+ }
+
+ if (typeof port.style.position.y == Number) {
+ ppos.y += port.style.position.y;
+ } else if (port.style.position.y == "auto") {
+ if (port.style.position.x == "left") {
+ ppos.y += centerY(leftPortCounter, leftPortCount, component.constructor.size.y, 5);
+ leftPortCounter++;
+ } else if (port.style.position.x == "right") {
+ ppos.y += centerY(rightPortCounter, rightPortCount, component.constructor.size.y, 5);
+ rightPortCounter++;
+ }
+ }
+
+ // collect wires
+ if (port.wires != undefined) {
+ port.wires.forEach(wire => {
+ var matching = wiresToDraw.filter(entry => entry.wire == port.wire)[0]
+ if (matching) {
+ matching.from = ppos;
+ } else {
+ wiresToDraw.push({
+ from: ppos,
+ to: null,
+ wire: wire
+ });
+ }
+ });
+ } else {
+ var matching = wiresToDraw.filter(entry => entry.wire == port.wire)[0]
+ if (matching) {
+ matching.to = ppos;
+ } else {
+ wiresToDraw.push({
+ from: null,
+ to: ppos,
+ wire: port.wire
+ });
+ }
+ }
+
+ // paint
+ this.dc.beginPath();
+ this.dc.strokeStyle = "black";
+ if (port.getValue() === true) {
+ this.dc.fillStyle = "green";
+ } else if (port.getValue() === false) {
+ this.dc.fillStyle = "red";
+ } else {
+ this.dc.fillStyle = "blue";
+ }
+ this.dc.arc(ppos.x, ppos.y, port.style.bigCircle ? 4 : 1, 0, Math.PI * 2);
+ this.dc.fill();
+ this.dc.stroke();
+ this.dc.closePath();
+ });
+ });
+
+ // draw wires
+ wiresToDraw.forEach(wire => {
+ this.dc.beginPath();
+ if (wire.wire.getValue() === true) {
+ this.dc.strokeStyle = "green";
+ } else if (wire.wire.getValue() === false) {
+ this.dc.strokeStyle = "red";
+ } else {
+ this.dc.strokeStyle = "blue";
+ }
+ this.dc.strokeStyle
+ this.dc.moveTo(wire.from.x, wire.from.y);
+ this.dc.quadraticCurveTo(wire.from.x, wire.from.y, wire.to.x, wire.to.y);
+ this.dc.stroke();
+ this.dc.closePath();
+ });
+ }
+
+ changeTool(tool) {
+ this.activeTool = tool;
+ this.canvas.style.cursor = guiTools[this.activeTool].cursor;
+ }
+
+ onClick(e) {
+ var rect = e.currentTarget.getBoundingClientRect();
+ var mousepos = {x: e.clientX - rect.left, y: e.clientY - rect.top};
+
+ this.components.forEach(component => {
+ if (mousepos.x >= component.position.x
+ && mousepos.y >= component.position.y
+ && mousepos.x < component.position.x + component.constructor.size.x
+ && mousepos.y < component.position.y + component.constructor.size.y) {
+ if (this.activeTool == "FINGER") {
+ component.onClick(mousepos);
+ }
+ }
+ })
+ }
+
+ onKeyPress(e) {
+ Object.entries(guiTools).forEach(([k, v]) => {
+ if (e.key == v.key) {
+ this.changeTool(k);
+ }
+ });
+ }
+}
+
+class Rule {
+ constructor(n) {
+ this.n = n;
+ }
+
+ change(n) {
+ this.n = n;
+ }
+
+ evaluate(a, b) {
+ var index = ((b & 1) << 1) | (a & 1);
+ return (this.n >> index) === 1;
+ }
+
+ toString() {
+ return this.n.toString();
+ }
+}
+
+class Port {
+ constructor(component) {
+ this.component = component;
+ this.style = {};
+ }
+}
+
+class InputPort extends Port {
+ constructor(component, style) {
+ super(component);
+ this.style = {
+ ...this.style,
+ ...{position: {x: "left", y: "auto"}, bigCircle: false},
+ ...style
+ };
+ this.wire = null;
+ this.value = 0;
+ }
+
+ attach(wire) {
+ // if there is an wire already connected to this input, remove it
+ if (this.wire) {
+ this.wire.destroy();
+ }
+ this.wire = wire;
+ }
+
+ update() {
+ if (this.wire != null) {
+ var oldValue = this.value;
+ var newValue = this.wire.getValue();
+ this.value = newValue;
+
+ // only update the component if the input value has changed
+ if (oldValue != newValue) {
+ this.component.portChanged();
+ }
+ }
+ }
+
+ getValue() {
+ return this.value;
+ }
+
+ removeWire() {
+ this.wire = null;
+ }
+}
+
+class OutputPort extends Port {
+ constructor(component, style) {
+ super(component);
+ this.style = {
+ ...this.style,
+ ...{position: {x: "right", y: "auto"}},
+ ...style
+ };
+ this.value = 0;
+ this.wires = [];
+ }
+
+ attach(wire) {
+ this.wires.push(wire);
+ }
+
+ change(newValue) {
+ this.value = newValue;
+ this.wires.forEach(wire => {
+ this.component.simulator.pushStep(wire.update.bind(wire));
+ });
+ }
+
+ getValue() {
+ return this.value;
+ }
+
+ removeWire(wire) {
+ this.wires.splice(this.wires.indexOf(wire), 1);
+ }
+}
+
+class Wire {
+ constructor(sourcePort, destPort) {
+ this.sourcePort = sourcePort;
+ this.destPort = destPort;
+ this.sourcePort.attach(this);
+ this.destPort.attach(this);
+ }
+
+ update() {
+ this.destPort.update();
+ }
+
+ getValue() {
+ return this.sourcePort.getValue();
+ }
+
+ destroy() {
+ this.sourcePort.removeWire();
+ this.destPort.removeWire(this);
+ }
+}
+
+class Component {
+ constructor() {
+ this.position = {
+ x: 0,
+ y: 0
+ };
+ this.inputPorts = {};
+ this.outputPorts = {};
+ this.label = "";
+ this.showLabel = false;
+ }
+
+ moveTo(x, y) {
+ this.position.x = x;
+ this.position.y = y;
+ }
+
+ initCanvas() {
+ this.canvas = new OffscreenCanvas(this.constructor.size.x, this.constructor.size.y);
+ this.dc = this.canvas.getContext("2d");
+ }
+
+ addInputPort(name, style = {}) {
+ style["name"] = name;
+ this.inputPorts[name] = new InputPort(this, style);
+ }
+
+ addOutputPort(name, style = {}) {
+ style["name"] = name;
+ this.outputPorts[name] = new OutputPort(this, style);
+ }
+}
+
+class Gate2 extends Component {
+ static size = {x: 32, y: 32};
+
+ constructor(rule = new Rule(0)) {
+ super();
+ this.rule = rule;
+ this.addInputPort("a");
+ this.addInputPort("b");
+ this.addOutputPort("y");
+ }
+
+ portChanged() {
+ var newValue = this.rule.evaluate(this.inputPorts.a.getValue(), this.inputPorts.b.getValue());
+ this.outputPorts.y.change(newValue);
+ }
+
+ draw() {
+ this.dc.beginPath();
+ this.dc.rect(0, 0, this.constructor.size.x, this.constructor.size.y);
+ this.dc.stroke();
+ }
+}
+
+class Switch extends Component {
+ static size = {x: 32, y: 32};
+
+ constructor() {
+ super();
+ this.addOutputPort("y");
+ this.state = false;
+ }
+
+ onClick() {
+ this.state = !this.state;
+ this.outputPorts.y.change(this.state);
+ }
+
+ draw() {
+ this.dc.beginPath();
+ this.dc.strokeStyle = "black";
+ this.dc.fillStyle = this.state ? "darkgrey" : "red";
+ this.dc.rect(0, 0, this.constructor.size.x, this.constructor.size.y / 2);
+ this.dc.fill();
+ this.dc.stroke();
+ this.dc.closePath();
+
+ this.dc.beginPath();
+ this.dc.fillStyle = this.state ? "green" : "darkgrey";
+ this.dc.rect(0, this.constructor.size.y / 2, this.constructor.size.x, this.constructor.size.y / 2);
+ this.dc.fill();
+ this.dc.stroke();
+ this.dc.closePath();
+ }
+}
+
+class Bulb extends Component {
+ static size = {x: 32, y: 32};
+
+ constructor() {
+ super();
+ this.addInputPort("a");
+ }
+
+ portChanged() {
+ var value = this.inputPorts.a.getValue();
+ console.log(`Bulb: ${value}`);
+ }
+
+ draw() {
+ this.dc.beginPath();
+ this.dc.strokeStyle = "black";
+ this.dc.fillStyle = this.inputPorts.a.getValue() ? "yellow" : "darkgrey";
+ this.dc.rect(0, 0, this.constructor.size.x, this.constructor.size.y);
+ this.dc.fill();
+ this.dc.stroke();
+ this.dc.closePath();
+ }
+}
+
+class Gate1 extends Component {
+ static size = {x: 32, y: 32};
+
+ constructor(invert) {
+ super();
+ this.invert = !!invert;
+ this.addInputPort("a");
+ this.addOutputPort("y", {bigCircle: invert});
+ }
+
+ portChanged() {
+ this.outputPorts.y.change(this.inputPorts.a.getValue() != this.invert);
+ }
+
+ draw() {
+ this.dc.beginPath();
+ this.dc.strokeStyle = "black";
+ this.dc.moveTo(0, 0);
+ this.dc.lineTo(0, Gate1.size.y);
+ this.dc.lineTo(Gate1.size.x, Gate1.size.y / 2);
+ this.dc.lineTo(0, 0);
+ this.dc.stroke();
+ this.dc.closePath();
+ }
+}
+
+class Buffer extends Gate1 {
+ constructor() {
+ super(false);
+ }
+}
+
+class NotGate extends Gate1 {
+ constructor() {
+ super(true);
+ }
+}
+
+class AndGate2 extends Gate2 {
+ static rule = new Rule(0b1000);
+
+ constructor() {
+ super(AndGate2.rule);
+ }
+}
+
+class OrGate2 extends Gate2 {
+ static rule = new Rule(0b1110);
+
+ constructor() {
+ super(OrGate2.rule);
+ }
+}
+
+class XorGate2 extends Gate2 {
+ static rule = new Rule(0b0110);
+
+ constructor() {
+ super(XorGate2.rule);
+ }
+}
+
+class NandGate2 extends Gate2 {
+ static rule = new Rule(0b0111);
+
+ constructor() {
+ super(NandGate2.rule);
+ this.outputPorts.y.style.bigCircle = true;
+ }
+}
+
+var canvas = document.getElementById("canvas");
+var simulator = new Simulator(canvas);
+var switch1 = new Switch();
+var switch2 = new Switch();
+var andGate = new AndGate2();
+var notGate = new NotGate();
+var bulb = new Bulb();
+var switch1ToAndGate = new Wire(switch1.outputPorts.y, andGate.inputPorts.a);
+var switch2ToAndGate = new Wire(switch2.outputPorts.y, andGate.inputPorts.b);
+var andGateToNotGate = new Wire(andGate.outputPorts.y, notGate.inputPorts.a);
+var notGateToBulb = new Wire(notGate.outputPorts.y, bulb.inputPorts.a)
+switch1.moveTo(0, 100);
+switch2.moveTo(0, 200);
+andGate.moveTo(200, 200);
+bulb.moveTo(300, 200);
+
+simulator.addComponents([switch1, switch2, andGate, notGate, bulb]);
+
+simulator.pushStep(switch1.onClick.bind(switch1));
+simulator.pushStep(switch2.onClick.bind(switch2));
+
+setInterval(() => {
+ simulator.executeStep();
+ simulator.draw();
+}, 10); \ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d0ee0e3
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Logic Gate Simulator</title>
+ </head>
+ <body>
+ <canvas id="canvas" width="500" height="500" tabindex="1"></canvas>
+ <script src="gatesim.js"></script>
+ </body>
+</html> \ No newline at end of file