diff --git a/index.html b/index.html index 09097b2..bac0dd3 100644 --- a/index.html +++ b/index.html @@ -50,6 +50,8 @@ .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; } @@ -229,6 +231,7 @@
Built with ❤️ at MontLab
@@ -312,7 +327,7 @@ const GATE_COLORS = { AND: '#00e599', OR: '#3388ff', NOT: '#ff6644', NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa', - INPUT: '#3388ff', OUTPUT: '#ff8833' + INPUT: '#3388ff', CLOCK: '#ff44aa', OUTPUT: '#ff8833' }; const SIGNAL_COLORS = [ @@ -331,8 +346,14 @@ 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 === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1; + if (type === 'CLOCK' || type === 'INPUT') return 0; + if (type === 'NOT' || type === 'OUTPUT') return 1; return 2; } function gateOutputCount(type) { @@ -342,7 +363,7 @@ 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; + if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value; const inputCount = gateInputCount(gate.type); const inputs = []; @@ -371,7 +392,7 @@ } function evaluateAll() { - gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; }); + gates.forEach(g => { if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0; }); gates.forEach(g => evaluate(g)); if (recording && waveformVisible) recordSample(); } @@ -413,16 +434,17 @@ } function getTrackedGates() { - // Order: inputs first, then gates, then outputs + 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'); - return [...inputs, ...logic, ...outputs]; + 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}`; @@ -633,15 +655,16 @@ ctx.textBaseline = 'middle'; const label = getGateLabel(gate); - ctx.fillText(gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (gate.type === 'INPUT' || gate.type === 'OUTPUT' ? 8 : 0)); + 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 - if (gate.type === 'INPUT' || gate.type === 'OUTPUT') { + // 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); @@ -819,7 +842,7 @@ if (connecting) { connecting = null; return; } const gate = findGateAt(x, y); - if (gate && gate.type === 'INPUT') { + if (gate && (gate.type === 'INPUT' || gate.type === 'CLOCK')) { gate.value = gate.value ? 0 : 1; evaluateAll(); return; @@ -924,6 +947,77 @@ 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 => {