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);