diff --git a/index.html b/index.html
index ba70d97..57495ab 100644
--- a/index.html
+++ b/index.html
@@ -20,29 +20,29 @@
top: 0;
left: 0;
right: 0;
- height: 56px;
+ height: 48px;
background: #12121a;
border-bottom: 1px solid #2a2a3a;
display: flex;
align-items: center;
- padding: 0 16px;
- gap: 8px;
+ padding: 0 12px;
+ gap: 6px;
z-index: 100;
}
#toolbar .logo {
font-weight: 700;
- font-size: 16px;
+ font-size: 15px;
color: #00e599;
- margin-right: 16px;
+ margin-right: 12px;
}
.gate-btn {
- padding: 6px 14px;
+ padding: 5px 12px;
background: #1a1a2e;
border: 1px solid #2a2a3a;
border-radius: 6px;
color: #ccc;
cursor: pointer;
- font-size: 13px;
+ font-size: 12px;
font-weight: 600;
transition: all 0.15s;
user-select: none;
@@ -52,31 +52,121 @@
.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; }
+ .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: 6px 12px;
+ padding: 5px 10px;
background: transparent;
border: 1px solid #333;
border-radius: 6px;
color: #888;
cursor: pointer;
- font-size: 12px;
+ 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: 56px;
+ 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;
@@ -92,28 +182,29 @@
background: #12121a;
border: 1px solid #2a2a3a;
border-radius: 12px;
- padding: 32px;
+ padding: 24px;
max-width: 520px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
- #help-content h2 { color: #00e599; margin-bottom: 16px; }
+ #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: 14px; line-height: 1.6; margin-bottom: 8px;
+ 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: 2px 6px;
- font-size: 12px;
+ padding: 1px 5px;
+ font-size: 11px;
color: #ddd;
}
#help-close {
- margin-top: 16px;
- padding: 8px 20px;
+ margin-top: 12px;
+ padding: 6px 18px;
background: #00e599;
border: none;
border-radius: 6px;
@@ -121,6 +212,17 @@
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;
+ }
@@ -135,6 +237,8 @@
@@ -143,19 +247,45 @@
+
+
Logic Gate Simulator
- - Click a gate button in the toolbar, then click on the canvas to place it
+ - Click a gate button, then click on the canvas to place it
- Drag gates to move them around
- - Click on an output port (right side), then click an input port (left side) to connect them
- - Click on an INPUT gate to toggle its value (0/1)
- - Press Delete or Backspace while hovering a gate to delete it
- - Right-click a connection to delete it
- - The circuit evaluates in real-time
+ - Click on an output port (right), then an input port (left) to connect
+ - Click an INPUT gate to toggle its value (0/1)
+ - Delete while hovering a gate to remove it
+ - Right-click a port to delete its connections
+ - Escape to cancel placing/connecting
-
Built with ❤️ at MontLab
+
Waveform Viewer (GTKWave-style)
+
+ - Click 📊 Waveform to toggle the signal viewer
+ - ⏺ Record captures signal changes automatically
+ - Step ▶ manually advances one time step
+ - Toggle inputs to see signals change in real-time
+ - Drag the top border to resize the panel
+ - All INPUT, OUTPUT, and gate signals are tracked
+
+
Built with ❤️ at MontLab
@@ -170,7 +300,7 @@
let placingGate = null;
let dragging = null;
let dragOffset = { x: 0, y: 0 };
- let connecting = null; // { gate, portIndex, portType }
+ let connecting = null;
let hoveredGate = null;
let hoveredPort = null;
let mouseX = 0, mouseY = 0;
@@ -185,6 +315,22 @@
INPUT: '#3388ff', 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;
+
function gateInputCount(type) {
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1;
return 2;
@@ -196,10 +342,8 @@
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++) {
@@ -229,8 +373,212 @@
function evaluateAll() {
gates.forEach(g => { if (g.type !== 'INPUT') 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() {
+ // Order: inputs first, then gates, then outputs
+ 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];
+ }
+
+ function getGateLabel(gate) {
+ const sameType = gates.filter(g => g.type === gate.type);
+ const idx = sameType.indexOf(gate);
+ 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 = [];
@@ -252,7 +600,8 @@
function resize() {
canvas.width = window.innerWidth;
- canvas.height = window.innerHeight - 56;
+ const waveH = waveformVisible ? waveformHeight : 0;
+ canvas.height = window.innerHeight - 48 - waveH;
}
window.addEventListener('resize', resize);
resize();
@@ -262,20 +611,17 @@
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.roundRect(gate.x, gate.y, GATE_W, GATE_H, 8);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
@@ -285,21 +631,27 @@
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
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));
+ // 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') {
- ctx.font = 'bold 18px monospace';
+ 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 + 12);
+ ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 10);
}
- // Input ports
+ // 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');
@@ -309,10 +661,8 @@
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');
@@ -342,7 +692,6 @@
ctx.lineWidth = active ? 2.5 : 1.5;
ctx.stroke();
- // Glow effect for active wires
if (active) {
ctx.strokeStyle = '#00ff8844';
ctx.lineWidth = 6;
@@ -393,6 +742,12 @@
ctx.globalAlpha = 1;
}
+ // Draw waveform
+ if (waveformVisible) {
+ drawWaveLabels();
+ drawWaveforms();
+ }
+
requestAnimationFrame(draw);
}
@@ -403,19 +758,16 @@
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' };
- }
+ 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' };
- }
+ 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;
@@ -438,43 +790,23 @@
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
- });
+ 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
- });
+ 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
- });
+ connections.push({ from: port.gate.id, fromPort: port.index, to: connecting.gate.id, toPort: connecting.portIndex });
evaluateAll();
}
connecting = null;
@@ -484,13 +816,8 @@
return;
}
- // Cancel connecting
- if (connecting) {
- connecting = null;
- return;
- }
+ if (connecting) { connecting = null; return; }
- // Toggle input
const gate = findGateAt(x, y);
if (gate && gate.type === 'INPUT') {
gate.value = gate.value ? 0 : 1;
@@ -498,7 +825,6 @@
return;
}
- // Start drag
if (gate) {
dragging = gate;
dragOffset = { x: x - gate.x, y: y - gate.y };
@@ -506,50 +832,39 @@
}
});
- canvas.addEventListener('mouseup', () => {
- dragging = null;
- });
+ 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') {
+ } else 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);
+ 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;
- }
+ if (e.key === 'Escape') { placingGate = null; connecting = null; }
});
- // Toolbar buttons
+ // Toolbar
document.querySelectorAll('.gate-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- placingGate = btn.dataset.gate;
- });
+ btn.addEventListener('click', () => { placingGate = btn.dataset.gate; });
});
document.getElementById('clear-btn').addEventListener('click', () => {
@@ -558,9 +873,13 @@
connections = [];
connecting = null;
placingGate = null;
+ waveData = {};
+ timeStep = 0;
+ updateWaveInfo();
}
});
+ // Help
document.getElementById('help-btn').addEventListener('click', () => {
document.getElementById('help-modal').classList.add('visible');
});
@@ -568,10 +887,57 @@
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');
+ 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);
+ });
+
+ // 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();