Initial commit: logic gate simulator

This commit is contained in:
Jose Luis Montañes
2026-03-19 18:37:39 +01:00
commit 1f95b1a97a
2 changed files with 583 additions and 0 deletions

3
Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

580
index.html Normal file
View File

@@ -0,0 +1,580 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logic Gates — MontLab</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0a0a0f;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
}
/* Toolbar */
#toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: #12121a;
border-bottom: 1px solid #2a2a3a;
display: flex;
align-items: center;
padding: 0 16px;
gap: 8px;
z-index: 100;
}
#toolbar .logo {
font-weight: 700;
font-size: 16px;
color: #00e599;
margin-right: 16px;
}
.gate-btn {
padding: 6px 14px;
background: #1a1a2e;
border: 1px solid #2a2a3a;
border-radius: 6px;
color: #ccc;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.15s;
user-select: none;
}
.gate-btn:hover { background: #252540; border-color: #00e599; color: #fff; }
.gate-btn.input-btn { border-color: #3388ff; }
.gate-btn.input-btn:hover { border-color: #55aaff; }
.gate-btn.output-btn { border-color: #ff8833; }
.gate-btn.output-btn:hover { border-color: #ffaa55; }
.separator { width: 1px; height: 28px; background: #2a2a3a; margin: 0 8px; }
.toolbar-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.action-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid #333;
border-radius: 6px;
color: #888;
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
}
.action-btn:hover { border-color: #ff4444; color: #ff4444; }
.action-btn.help-btn:hover { border-color: #00e599; color: #00e599; }
/* Canvas */
#canvas {
position: fixed;
top: 56px;
left: 0;
right: 0;
bottom: 0;
cursor: default;
}
/* Help modal */
#help-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 200;
justify-content: center;
align-items: center;
}
#help-modal.visible { display: flex; }
#help-content {
background: #12121a;
border: 1px solid #2a2a3a;
border-radius: 12px;
padding: 32px;
max-width: 520px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
#help-content h2 { color: #00e599; margin-bottom: 16px; }
#help-content p, #help-content li {
color: #aaa; font-size: 14px; line-height: 1.6; margin-bottom: 8px;
}
#help-content ul { padding-left: 20px; }
#help-content kbd {
background: #1a1a2e;
border: 1px solid #333;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
color: #ddd;
}
#help-close {
margin-top: 16px;
padding: 8px 20px;
background: #00e599;
border: none;
border-radius: 6px;
color: #000;
font-weight: 600;
cursor: pointer;
}
</style>
</head>
<body>
<div id="toolbar">
<span class="logo">⚡ Logic Lab</span>
<button class="gate-btn input-btn" data-gate="INPUT">INPUT</button>
<button class="gate-btn output-btn" data-gate="OUTPUT">OUTPUT</button>
<div class="separator"></div>
<button class="gate-btn" data-gate="AND">AND</button>
<button class="gate-btn" data-gate="OR">OR</button>
<button class="gate-btn" data-gate="NOT">NOT</button>
<button class="gate-btn" data-gate="NAND">NAND</button>
<button class="gate-btn" data-gate="NOR">NOR</button>
<button class="gate-btn" data-gate="XOR">XOR</button>
<div class="toolbar-right">
<button class="action-btn help-btn" id="help-btn">? Help</button>
<button class="action-btn" id="clear-btn">Clear All</button>
</div>
</div>
<canvas id="canvas"></canvas>
<div id="help-modal">
<div id="help-content">
<h2>Logic Gate Simulator</h2>
<ul>
<li>Click a gate button in the toolbar, then click on the canvas to place it</li>
<li>Drag gates to move them around</li>
<li>Click on an <strong>output port</strong> (right side), then click an <strong>input port</strong> (left side) to connect them</li>
<li>Click on an <kbd>INPUT</kbd> gate to toggle its value (0/1)</li>
<li>Press <kbd>Delete</kbd> or <kbd>Backspace</kbd> while hovering a gate to delete it</li>
<li>Right-click a connection to delete it</li>
<li>The circuit evaluates in real-time</li>
</ul>
<p style="margin-top: 16px; color: #666;">Built with ❤️ at MontLab</p>
<button id="help-close">Got it</button>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// State
let gates = [];
let connections = [];
let placingGate = null;
let dragging = null;
let dragOffset = { x: 0, y: 0 };
let connecting = null; // { gate, portIndex, portType }
let hoveredGate = null;
let hoveredPort = null;
let mouseX = 0, mouseY = 0;
let nextId = 1;
// Gate definitions
const GATE_W = 100, GATE_H = 60;
const PORT_R = 7;
const GATE_COLORS = {
AND: '#00e599', OR: '#3388ff', NOT: '#ff6644',
NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa',
INPUT: '#3388ff', OUTPUT: '#ff8833'
};
function gateInputCount(type) {
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1;
return 2;
}
function gateOutputCount(type) {
return type === 'OUTPUT' ? 0 : 1;
}
function evaluate(gate, visited = new Set()) {
if (visited.has(gate.id)) return gate.value || 0;
visited.add(gate.id);
if (gate.type === 'INPUT') return gate.value;
// Get input values
const inputCount = gateInputCount(gate.type);
const inputs = [];
for (let i = 0; i < inputCount; i++) {
const conn = connections.find(c => c.to === gate.id && c.toPort === i);
if (conn) {
const srcGate = gates.find(g => g.id === conn.from);
inputs.push(srcGate ? evaluate(srcGate, visited) : 0);
} else {
inputs.push(0);
}
}
let result = 0;
switch (gate.type) {
case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break;
case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break;
case 'NOT': result = inputs[0] ? 0 : 1; break;
case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break;
case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break;
case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break;
case 'OUTPUT': result = inputs[0] || 0; break;
}
gate.value = result;
return result;
}
function evaluateAll() {
gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; });
gates.forEach(g => evaluate(g));
}
function getInputPorts(gate) {
const count = gateInputCount(gate.type);
const ports = [];
for (let i = 0; i < count; i++) {
const spacing = GATE_H / (count + 1);
ports.push({ x: gate.x, y: gate.y + spacing * (i + 1), index: i, type: 'input' });
}
return ports;
}
function getOutputPorts(gate) {
const count = gateOutputCount(gate.type);
const ports = [];
for (let i = 0; i < count; i++) {
ports.push({ x: gate.x + GATE_W, y: gate.y + GATE_H / 2, index: i, type: 'output' });
}
return ports;
}
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - 56;
}
window.addEventListener('resize', resize);
resize();
function drawGate(gate) {
const color = GATE_COLORS[gate.type];
const isHovered = hoveredGate === gate;
const isActive = gate.value === 1;
// Shadow for active gates
if (isActive) {
ctx.shadowColor = color;
ctx.shadowBlur = 20;
}
// Body
ctx.fillStyle = isActive ? color + '22' : '#14141e';
ctx.strokeStyle = isHovered ? '#fff' : color;
ctx.lineWidth = isHovered ? 2.5 : 1.5;
const r = 8;
ctx.beginPath();
ctx.roundRect(gate.x, gate.y, GATE_W, GATE_H, r);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Label
ctx.fillStyle = isActive ? '#fff' : color;
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (gate.type === 'INPUT' || gate.type === 'OUTPUT' ? 8 : 0));
// Value for INPUT/OUTPUT
if (gate.type === 'INPUT' || gate.type === 'OUTPUT') {
ctx.font = 'bold 18px monospace';
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 12);
}
// Input ports
getInputPorts(gate).forEach(p => {
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'input';
const conn = connections.find(c => c.to === gate.id && c.toPort === p.index);
const portActive = conn ? gates.find(g => g.id === conn.from)?.value : 0;
ctx.beginPath();
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
ctx.fill();
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
ctx.lineWidth = 1.5;
ctx.stroke();
});
// Output ports
getOutputPorts(gate).forEach(p => {
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'output';
ctx.beginPath();
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
ctx.fill();
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
ctx.lineWidth = 1.5;
ctx.stroke();
});
}
function drawConnection(conn) {
const fromGate = gates.find(g => g.id === conn.from);
const toGate = gates.find(g => g.id === conn.to);
if (!fromGate || !toGate) return;
const fromPort = getOutputPorts(fromGate)[conn.fromPort];
const toPort = getInputPorts(toGate)[conn.toPort];
if (!fromPort || !toPort) return;
const active = fromGate.value === 1;
const midX = (fromPort.x + toPort.x) / 2;
ctx.beginPath();
ctx.moveTo(fromPort.x, fromPort.y);
ctx.bezierCurveTo(midX, fromPort.y, midX, toPort.y, toPort.x, toPort.y);
ctx.strokeStyle = active ? '#00ff88' : '#333';
ctx.lineWidth = active ? 2.5 : 1.5;
ctx.stroke();
// Glow effect for active wires
if (active) {
ctx.strokeStyle = '#00ff8844';
ctx.lineWidth = 6;
ctx.stroke();
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Grid
ctx.strokeStyle = '#111118';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
connections.forEach(drawConnection);
gates.forEach(drawGate);
// Drawing connection wire
if (connecting) {
const gate = connecting.gate;
const port = connecting.portType === 'output'
? getOutputPorts(gate)[connecting.portIndex]
: getInputPorts(gate)[connecting.portIndex];
if (port) {
const midX = (port.x + mouseX) / 2;
ctx.beginPath();
ctx.moveTo(port.x, port.y);
ctx.bezierCurveTo(midX, port.y, midX, mouseY, mouseX, mouseY);
ctx.strokeStyle = '#00e59988';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
}
// Placing ghost
if (placingGate) {
ctx.globalAlpha = 0.5;
const ghost = { x: mouseX - GATE_W/2, y: mouseY - GATE_H/2, type: placingGate, value: 0, id: -1 };
drawGate(ghost);
ctx.globalAlpha = 1;
}
requestAnimationFrame(draw);
}
function findGateAt(x, y) {
return gates.find(g => x >= g.x && x <= g.x + GATE_W && y >= g.y && y <= g.y + GATE_H);
}
function findPortAt(x, y) {
for (const gate of gates) {
for (const p of getInputPorts(gate)) {
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
return { gate, index: p.index, type: 'input' };
}
}
for (const p of getOutputPorts(gate)) {
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
return { gate, index: p.index, type: 'output' };
}
}
}
return null;
}
canvas.addEventListener('mousemove', e => {
mouseX = e.offsetX;
mouseY = e.offsetY;
hoveredPort = findPortAt(mouseX, mouseY);
hoveredGate = hoveredPort ? hoveredPort.gate : findGateAt(mouseX, mouseY);
if (dragging) {
dragging.x = mouseX - dragOffset.x;
dragging.y = mouseY - dragOffset.y;
evaluateAll();
}
canvas.style.cursor = placingGate ? 'crosshair'
: hoveredPort ? 'pointer'
: hoveredGate ? 'grab'
: 'default';
});
canvas.addEventListener('mousedown', e => {
if (e.button !== 0) return;
const x = e.offsetX, y = e.offsetY;
// Placing a gate
if (placingGate) {
gates.push({
id: nextId++,
type: placingGate,
x: x - GATE_W / 2,
y: y - GATE_H / 2,
value: 0
});
evaluateAll();
placingGate = null;
return;
}
// Check port click
const port = findPortAt(x, y);
if (port) {
if (connecting) {
// Complete connection
if (connecting.portType === 'output' && port.type === 'input') {
// Remove existing connection to this input
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
connections.push({
from: connecting.gate.id,
fromPort: connecting.portIndex,
to: port.gate.id,
toPort: port.index
});
evaluateAll();
} else if (connecting.portType === 'input' && port.type === 'output') {
connections = connections.filter(c => !(c.to === connecting.gate.id && c.toPort === connecting.portIndex));
connections.push({
from: port.gate.id,
fromPort: port.index,
to: connecting.gate.id,
toPort: connecting.portIndex
});
evaluateAll();
}
connecting = null;
} else {
connecting = { gate: port.gate, portIndex: port.index, portType: port.type };
}
return;
}
// Cancel connecting
if (connecting) {
connecting = null;
return;
}
// Toggle input
const gate = findGateAt(x, y);
if (gate && gate.type === 'INPUT') {
gate.value = gate.value ? 0 : 1;
evaluateAll();
return;
}
// Start drag
if (gate) {
dragging = gate;
dragOffset = { x: x - gate.x, y: y - gate.y };
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mouseup', () => {
dragging = null;
});
// Right-click to delete connections
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
const x = e.offsetX, y = e.offsetY;
// Check if clicking near a connection
const port = findPortAt(x, y);
if (port && port.type === 'input') {
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
evaluateAll();
return;
}
if (port && port.type === 'output') {
connections = connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
evaluateAll();
}
});
// Delete key
document.addEventListener('keydown', e => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (hoveredGate && document.activeElement === document.body) {
e.preventDefault();
connections = connections.filter(c => c.from !== hoveredGate.id && c.to !== hoveredGate.id);
gates = gates.filter(g => g !== hoveredGate);
hoveredGate = null;
evaluateAll();
}
}
if (e.key === 'Escape') {
placingGate = null;
connecting = null;
}
});
// Toolbar buttons
document.querySelectorAll('.gate-btn').forEach(btn => {
btn.addEventListener('click', () => {
placingGate = btn.dataset.gate;
});
});
document.getElementById('clear-btn').addEventListener('click', () => {
if (gates.length === 0 || confirm('Clear all gates and connections?')) {
gates = [];
connections = [];
connecting = null;
placingGate = null;
}
});
document.getElementById('help-btn').addEventListener('click', () => {
document.getElementById('help-modal').classList.add('visible');
});
document.getElementById('help-close').addEventListener('click', () => {
document.getElementById('help-modal').classList.remove('visible');
});
document.getElementById('help-modal').addEventListener('click', e => {
if (e.target === e.currentTarget) {
document.getElementById('help-modal').classList.remove('visible');
}
});
// Start
draw();
</script>
</body>
</html>