diff options
| author | Paul Harrison <[email protected]> | 2026-06-15 12:47:45 -0700 |
|---|---|---|
| committer | Paul Harrison <[email protected]> | 2026-06-15 12:47:45 -0700 |
| commit | d639e402cd8b27cf839017080ce9b995b5b80452 (patch) | |
| tree | 02f82677184c7c2166ca6bc3165629fca5d4c2c4 | |
logic simulator part 1main
| -rw-r--r-- | gatesim.js | 534 | ||||
| -rw-r--r-- | index.html | 11 |
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 |
