Files
logic-gates/index.html
Jose Luis Montañes 6fbc6e4896 Add CLOCK gate with automatic simulation
- CLOCK gate auto-toggles 0→1→0 during simulation
- Run/Stop button in waveform toolbar
- Adjustable speed (50ms to 2000ms per tick)
- Click CLOCK to toggle manually when sim is stopped
- Waveform auto-opens when sim starts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 19:42:46 +01:00

1042 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 48px;
background: #12121a;
border-bottom: 1px solid #2a2a3a;
display: flex;
align-items: center;
padding: 0 12px;
gap: 6px;
z-index: 100;
}
#toolbar .logo {
font-weight: 700;
font-size: 15px;
color: #00e599;
margin-right: 12px;
}
.gate-btn {
padding: 5px 12px;
background: #1a1a2e;
border: 1px solid #2a2a3a;
border-radius: 6px;
color: #ccc;
cursor: pointer;
font-size: 12px;
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.clock-btn { border-color: #ff44aa; }
.gate-btn.clock-btn:hover { border-color: #ff66cc; }
.gate-btn.output-btn { border-color: #ff8833; }
.gate-btn.output-btn:hover { border-color: #ffaa55; }
.separator { width: 1px; height: 24px; background: #2a2a3a; margin: 0 6px; }
.toolbar-right { margin-left: auto; display: flex; gap: 6px; align-items: center; }
.action-btn {
padding: 5px 10px;
background: transparent;
border: 1px solid #333;
border-radius: 6px;
color: #888;
cursor: pointer;
font-size: 11px;
transition: all 0.15s;
}
.action-btn:hover { border-color: #ff4444; color: #ff4444; }
.action-btn.help-btn:hover { border-color: #00e599; color: #00e599; }
.action-btn.sim-btn { border-color: #ff44aa; color: #ff44aa; }
.action-btn.sim-btn:hover { background: #ff44aa22; }
.action-btn.sim-btn.active { background: #ff44aa33; border-color: #ff66cc; color: #ff66cc; }
/* Canvas */
#canvas {
position: fixed;
top: 48px;
left: 0;
right: 0;
bottom: 0;
cursor: default;
}
/* Waveform panel */
#waveform-panel {
display: none;
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 220px;
background: #0c0c14;
border-top: 2px solid #00e599;
z-index: 90;
flex-direction: column;
}
#waveform-panel.visible { display: flex; }
#wave-toolbar {
display: flex;
align-items: center;
padding: 4px 12px;
gap: 8px;
background: #10101a;
border-bottom: 1px solid #1a1a2a;
height: 32px;
flex-shrink: 0;
}
#wave-toolbar span.wave-title {
font-size: 12px;
font-weight: 700;
color: #00e599;
margin-right: 8px;
}
.wave-btn {
padding: 3px 10px;
background: #1a1a2e;
border: 1px solid #2a2a3a;
border-radius: 4px;
color: #aaa;
cursor: pointer;
font-size: 11px;
font-weight: 600;
transition: all 0.15s;
}
.wave-btn:hover { border-color: #00e599; color: #fff; }
.wave-btn.active { background: #00e59933; border-color: #00e599; color: #00e599; }
.wave-btn.record { border-color: #ff4444; }
.wave-btn.record.active { background: #ff444433; border-color: #ff4444; color: #ff4444; }
.wave-info {
margin-left: auto;
font-size: 11px;
color: #555;
}
#wave-container {
display: flex;
flex: 1;
overflow: hidden;
}
#wave-labels {
width: 100px;
flex-shrink: 0;
overflow-y: auto;
background: #0e0e18;
border-right: 1px solid #1a1a2a;
}
.wave-label {
height: 30px;
display: flex;
align-items: center;
padding: 0 8px;
font-size: 11px;
font-weight: 600;
border-bottom: 1px solid #111;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wave-label.input-label { color: #3388ff; }
.wave-label.output-label { color: #ff8833; }
.wave-label.gate-label { color: #00e599; }
#wave-canvas {
flex: 1;
cursor: crosshair;
}
/* 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: 24px;
max-width: 520px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
#help-content h2 { color: #00e599; margin-bottom: 12px; font-size: 18px; }
#help-content h3 { color: #ff44aa; margin: 12px 0 8px; font-size: 14px; }
#help-content p, #help-content li {
color: #aaa; font-size: 13px; line-height: 1.5; margin-bottom: 6px;
}
#help-content ul { padding-left: 20px; }
#help-content kbd {
background: #1a1a2e;
border: 1px solid #333;
border-radius: 4px;
padding: 1px 5px;
font-size: 11px;
color: #ddd;
}
#help-close {
margin-top: 12px;
padding: 6px 18px;
background: #00e599;
border: none;
border-radius: 6px;
color: #000;
font-weight: 600;
cursor: pointer;
}
/* Resize handle */
#wave-resize {
position: absolute;
top: -4px;
left: 0;
right: 0;
height: 8px;
cursor: ns-resize;
z-index: 91;
}
</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 clock-btn" data-gate="CLOCK">CLOCK</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="separator"></div>
<button class="action-btn sim-btn" id="sim-btn">📊 Waveform</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="waveform-panel">
<div id="wave-resize"></div>
<div id="wave-toolbar">
<span class="wave-title">📊 Waveform Viewer</span>
<button class="wave-btn record active" id="wave-record">⏺ Record</button>
<button class="wave-btn" id="wave-clear">Clear</button>
<button class="wave-btn" id="wave-step">Step ▶</button>
<button class="wave-btn" id="wave-zoom-in">Zoom +</button>
<button class="wave-btn" id="wave-zoom-out">Zoom -</button>
<div class="separator"></div>
<button class="wave-btn" id="sim-run-btn">▶ Run</button>
<button class="wave-btn" id="sim-slower"></button>
<span class="wave-info" id="sim-speed-label" style="margin-left:0">500ms</span>
<button class="wave-btn" id="sim-faster">+</button>
<span class="wave-info" id="wave-info">T=0 | 0 samples</span>
</div>
<div id="wave-container">
<div id="wave-labels"></div>
<canvas id="wave-canvas"></canvas>
</div>
</div>
<div id="help-modal">
<div id="help-content">
<h2>Logic Gate Simulator</h2>
<ul>
<li>Click a gate button, 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), then an <strong>input port</strong> (left) to connect</li>
<li>Click an <kbd>INPUT</kbd> gate to toggle its value (0/1)</li>
<li><kbd>Delete</kbd> while hovering a gate to remove it</li>
<li>Right-click a port to delete its connections</li>
<li><kbd>Escape</kbd> to cancel placing/connecting</li>
</ul>
<h3>Clock &amp; Simulation</h3>
<ul>
<li>Place a <kbd>CLOCK</kbd> gate — it auto-toggles 0/1 during simulation</li>
<li>Click <kbd>▶ Run</kbd> in the waveform bar to start the clock</li>
<li>Use <kbd></kbd> / <kbd>+</kbd> to adjust speed (50ms2000ms per tick)</li>
<li>Connect CLOCK to gates to see automatic signal propagation</li>
</ul>
<h3>Waveform Viewer (GTKWave-style)</h3>
<ul>
<li>Click <kbd>📊 Waveform</kbd> to toggle the signal viewer</li>
<li><kbd>⏺ Record</kbd> captures signal changes automatically</li>
<li><kbd>Step ▶</kbd> manually advances one time step</li>
<li>Toggle inputs to see signals change in real-time</li>
<li>Drag the top border to resize the panel</li>
<li>All CLOCK, INPUT, OUTPUT, and gate signals are tracked</li>
</ul>
<p style="margin-top: 12px; 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;
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', CLOCK: '#ff44aa', OUTPUT: '#ff8833'
};
const SIGNAL_COLORS = [
'#00e599', '#3388ff', '#ff6644', '#e5cc00',
'#cc44ff', '#ff44aa', '#ff8833', '#44ddff',
'#88ff44', '#ff4488', '#44ffaa', '#ffaa44'
];
// Waveform state
let waveformVisible = false;
let waveformHeight = 220;
let recording = true;
let waveData = {}; // { gateId: [{ t, value }] }
let timeStep = 0;
let waveZoom = 20; // pixels per time step
let waveScroll = 0;
let resizingWave = false;
// Clock / simulation state
let simRunning = false;
let simInterval = null;
let simSpeed = 500; // ms per tick
function gateInputCount(type) {
if (type === 'CLOCK' || type === 'INPUT') return 0;
if (type === 'NOT' || type === 'OUTPUT') return 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' || gate.type === 'CLOCK') return gate.value;
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.type !== 'CLOCK') g.value = 0; });
gates.forEach(g => evaluate(g));
if (recording && waveformVisible) recordSample();
}
// ==================== WAVEFORM ====================
function recordSample() {
const changed = gates.some(g => {
const data = waveData[g.id];
if (!data || data.length === 0) return true;
return data[data.length - 1].value !== g.value;
});
if (!changed && timeStep > 0) return;
timeStep++;
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
const arr = waveData[g.id];
// Only record if value changed or first sample
if (arr.length === 0 || arr[arr.length - 1].value !== g.value) {
arr.push({ t: timeStep, value: g.value });
}
});
updateWaveInfo();
}
function manualStep() {
timeStep++;
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
waveData[g.id].push({ t: timeStep, value: g.value });
});
updateWaveInfo();
}
function updateWaveInfo() {
const totalSamples = Object.values(waveData).reduce((sum, arr) => sum + arr.length, 0);
document.getElementById('wave-info').textContent = `T=${timeStep} | ${totalSamples} samples`;
}
function getTrackedGates() {
const clocks = gates.filter(g => g.type === 'CLOCK');
const inputs = gates.filter(g => g.type === 'INPUT');
const outputs = gates.filter(g => g.type === 'OUTPUT');
const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT' && g.type !== 'CLOCK');
return [...clocks, ...inputs, ...logic, ...outputs];
}
function getGateLabel(gate) {
const sameType = gates.filter(g => g.type === gate.type);
const idx = sameType.indexOf(gate);
if (gate.type === 'CLOCK') return `CLK_${idx}`;
if (gate.type === 'INPUT') return `IN_${idx}`;
if (gate.type === 'OUTPUT') return `OUT_${idx}`;
return `${gate.type}_${idx}`;
}
function drawWaveLabels() {
const labelsEl = document.getElementById('wave-labels');
labelsEl.innerHTML = '';
const tracked = getTrackedGates();
tracked.forEach((gate, i) => {
const div = document.createElement('div');
div.className = 'wave-label';
if (gate.type === 'INPUT') div.classList.add('input-label');
else if (gate.type === 'OUTPUT') div.classList.add('output-label');
else div.classList.add('gate-label');
div.textContent = getGateLabel(gate);
div.style.color = SIGNAL_COLORS[i % SIGNAL_COLORS.length];
labelsEl.appendChild(div);
});
}
function drawWaveforms() {
const wc = document.getElementById('wave-canvas');
const wctx = wc.getContext('2d');
const container = document.getElementById('wave-container');
wc.width = container.clientWidth - 100;
wc.height = container.clientHeight;
wctx.fillStyle = '#0c0c14';
wctx.fillRect(0, 0, wc.width, wc.height);
const tracked = getTrackedGates();
const rowH = 30;
const sigH = 20;
const margin = (rowH - sigH) / 2;
if (timeStep === 0) {
wctx.fillStyle = '#333';
wctx.font = '12px "Segoe UI", system-ui';
wctx.textAlign = 'center';
wctx.fillText('Toggle inputs to record signals...', wc.width / 2, wc.height / 2);
return;
}
// Auto-scroll to show latest
const maxVisible = Math.floor(wc.width / waveZoom);
if (timeStep > maxVisible) {
waveScroll = timeStep - maxVisible;
}
// Draw time grid
wctx.strokeStyle = '#151520';
wctx.lineWidth = 1;
for (let t = Math.ceil(waveScroll); t <= timeStep; t++) {
const x = (t - waveScroll) * waveZoom;
if (x < 0 || x > wc.width) continue;
wctx.beginPath();
wctx.moveTo(x, 0);
wctx.lineTo(x, wc.height);
wctx.stroke();
// Time labels
if (t % 5 === 0 || waveZoom > 30) {
wctx.fillStyle = '#333';
wctx.font = '9px monospace';
wctx.textAlign = 'center';
wctx.fillText(`${t}`, x, 10);
}
}
// Row dividers
tracked.forEach((_, i) => {
const y = i * rowH + rowH;
wctx.strokeStyle = '#111118';
wctx.beginPath();
wctx.moveTo(0, y);
wctx.lineTo(wc.width, y);
wctx.stroke();
});
// Draw signals
tracked.forEach((gate, i) => {
const data = waveData[gate.id] || [];
if (data.length === 0) return;
const color = SIGNAL_COLORS[i % SIGNAL_COLORS.length];
const y0 = i * rowH + margin;
const yHigh = y0 + 2;
const yLow = y0 + sigH;
wctx.strokeStyle = color;
wctx.lineWidth = 1.5;
wctx.beginPath();
let lastVal = 0;
let lastX = 0;
let started = false;
// Build complete signal timeline
const timeline = [];
for (let t = 1; t <= timeStep; t++) {
const sample = data.filter(s => s.t <= t).pop();
timeline.push(sample ? sample.value : 0);
}
for (let t = 0; t < timeline.length; t++) {
const x = (t + 1 - waveScroll) * waveZoom;
const val = timeline[t];
const y = val ? yHigh : yLow;
if (!started) {
wctx.moveTo(x, y);
started = true;
} else {
// Vertical transition
if (val !== lastVal) {
wctx.lineTo(x, lastVal ? yHigh : yLow);
wctx.lineTo(x, y);
}
wctx.lineTo(x + waveZoom, y);
}
lastVal = val;
lastX = x + waveZoom;
}
wctx.stroke();
// Fill area under signal
wctx.globalAlpha = 0.08;
wctx.fillStyle = color;
// Simple fill
for (let t = 0; t < timeline.length; t++) {
const x = (t + 1 - waveScroll) * waveZoom;
if (timeline[t]) {
wctx.fillRect(x, yHigh, waveZoom, sigH);
}
}
wctx.globalAlpha = 1;
});
// Cursor line at current time
const cursorX = (timeStep - waveScroll) * waveZoom;
if (cursorX >= 0 && cursorX <= wc.width) {
wctx.strokeStyle = '#00e59966';
wctx.lineWidth = 1;
wctx.setLineDash([4, 3]);
wctx.beginPath();
wctx.moveTo(cursorX, 0);
wctx.lineTo(cursorX, wc.height);
wctx.stroke();
wctx.setLineDash([]);
}
}
// ==================== DRAWING ====================
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;
const waveH = waveformVisible ? waveformHeight : 0;
canvas.height = window.innerHeight - 48 - waveH;
}
window.addEventListener('resize', resize);
resize();
function drawGate(gate) {
const color = GATE_COLORS[gate.type];
const isHovered = hoveredGate === gate;
const isActive = gate.value === 1;
if (isActive) {
ctx.shadowColor = color;
ctx.shadowBlur = 20;
}
ctx.fillStyle = isActive ? color + '22' : '#14141e';
ctx.strokeStyle = isHovered ? '#fff' : color;
ctx.lineWidth = isHovered ? 2.5 : 1.5;
ctx.beginPath();
ctx.roundRect(gate.x, gate.y, GATE_W, GATE_H, 8);
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';
const label = getGateLabel(gate);
const isIOType = gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK';
ctx.fillText(gate.type === 'CLOCK' ? '⏱ CLK' : gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (isIOType ? 8 : 0));
// Small ID label
ctx.font = '9px monospace';
ctx.fillStyle = '#444';
ctx.fillText(label, gate.x + GATE_W / 2, gate.y + GATE_H - 6);
// Value for INPUT/OUTPUT/CLOCK
if (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK') {
ctx.font = 'bold 16px monospace';
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 10);
}
// 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();
});
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();
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;
}
// Draw waveform
if (waveformVisible) {
drawWaveLabels();
drawWaveforms();
}
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;
}
// ==================== EVENTS ====================
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;
if (placingGate) {
gates.push({ id: nextId++, type: placingGate, x: x - GATE_W / 2, y: y - GATE_H / 2, value: 0 });
evaluateAll();
placingGate = null;
return;
}
const port = findPortAt(x, y);
if (port) {
if (connecting) {
if (connecting.portType === 'output' && port.type === '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;
}
if (connecting) { connecting = null; return; }
const gate = findGateAt(x, y);
if (gate && (gate.type === 'INPUT' || gate.type === 'CLOCK')) {
gate.value = gate.value ? 0 : 1;
evaluateAll();
return;
}
if (gate) {
dragging = gate;
dragOffset = { x: x - gate.x, y: y - gate.y };
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mouseup', () => { dragging = null; });
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
const x = e.offsetX, y = e.offsetY;
const port = findPortAt(x, y);
if (port && port.type === 'input') {
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
evaluateAll();
} else if (port && port.type === 'output') {
connections = connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
evaluateAll();
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (hoveredGate && document.activeElement === document.body) {
e.preventDefault();
const gateId = hoveredGate.id;
connections = connections.filter(c => c.from !== gateId && c.to !== gateId);
gates = gates.filter(g => g.id !== gateId);
delete waveData[gateId];
hoveredGate = null;
evaluateAll();
}
}
if (e.key === 'Escape') { placingGate = null; connecting = null; }
});
// Toolbar
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;
waveData = {};
timeStep = 0;
updateWaveInfo();
}
});
// Help
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');
});
// Waveform toggle
document.getElementById('sim-btn').addEventListener('click', () => {
waveformVisible = !waveformVisible;
document.getElementById('waveform-panel').classList.toggle('visible', waveformVisible);
document.getElementById('sim-btn').classList.toggle('active', waveformVisible);
resize();
});
// Waveform controls
document.getElementById('wave-record').addEventListener('click', function() {
recording = !recording;
this.classList.toggle('active', recording);
this.textContent = recording ? '⏺ Record' : '⏸ Paused';
});
document.getElementById('wave-clear').addEventListener('click', () => {
waveData = {};
timeStep = 0;
waveScroll = 0;
updateWaveInfo();
});
document.getElementById('wave-step').addEventListener('click', () => {
manualStep();
});
document.getElementById('wave-zoom-in').addEventListener('click', () => {
waveZoom = Math.min(60, waveZoom + 5);
});
document.getElementById('wave-zoom-out').addEventListener('click', () => {
waveZoom = Math.max(5, waveZoom - 5);
});
// ==================== CLOCK SIMULATION ====================
function simTick() {
// Toggle all CLOCK gates
gates.forEach(g => {
if (g.type === 'CLOCK') {
g.value = g.value ? 0 : 1;
}
});
evaluateAll();
// Force record even if evaluateAll didn't detect change
if (recording && waveformVisible) {
timeStep++;
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
waveData[g.id].push({ t: timeStep, value: g.value });
});
updateWaveInfo();
}
}
function startSim() {
if (simRunning) return;
const hasClocks = gates.some(g => g.type === 'CLOCK');
if (!hasClocks) { alert('Place a CLOCK gate first!'); return; }
simRunning = true;
// Auto-open waveform panel
if (!waveformVisible) {
waveformVisible = true;
document.getElementById('waveform-panel').classList.add('visible');
document.getElementById('sim-btn').classList.add('active');
resize();
}
simInterval = setInterval(simTick, simSpeed);
updateSimUI();
}
function stopSim() {
simRunning = false;
if (simInterval) clearInterval(simInterval);
simInterval = null;
updateSimUI();
}
function updateSimUI() {
const btn = document.getElementById('sim-run-btn');
if (simRunning) {
btn.textContent = '⏹ Stop';
btn.classList.add('active');
} else {
btn.textContent = '▶ Run';
btn.classList.remove('active');
}
document.getElementById('sim-speed-label').textContent = `${simSpeed}ms`;
}
document.getElementById('sim-run-btn').addEventListener('click', () => {
if (simRunning) stopSim(); else startSim();
});
document.getElementById('sim-faster').addEventListener('click', () => {
simSpeed = Math.max(50, simSpeed - 100);
if (simRunning) { clearInterval(simInterval); simInterval = setInterval(simTick, simSpeed); }
updateSimUI();
});
document.getElementById('sim-slower').addEventListener('click', () => {
simSpeed = Math.min(2000, simSpeed + 100);
if (simRunning) { clearInterval(simInterval); simInterval = setInterval(simTick, simSpeed); }
updateSimUI();
});
// Resize waveform panel
const resizeHandle = document.getElementById('wave-resize');
resizeHandle.addEventListener('mousedown', e => {
resizingWave = true;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (resizingWave) {
waveformHeight = Math.max(100, Math.min(500, window.innerHeight - e.clientY));
document.getElementById('waveform-panel').style.height = waveformHeight + 'px';
resize();
}
});
document.addEventListener('mouseup', () => { resizingWave = false; });
// Start
draw();
</script>
</body>
</html>
<!-- v2 -->