Add GTKWave-style waveform viewer
- Signal recording panel with per-gate waveforms - Record/pause, step, zoom, clear controls - Auto-scroll to latest signals - Resizable panel with drag handle - Color-coded signals matching gate types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
564
index.html
564
index.html
@@ -20,29 +20,29 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 56px;
|
height: 48px;
|
||||||
background: #12121a;
|
background: #12121a;
|
||||||
border-bottom: 1px solid #2a2a3a;
|
border-bottom: 1px solid #2a2a3a;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
#toolbar .logo {
|
#toolbar .logo {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
color: #00e599;
|
color: #00e599;
|
||||||
margin-right: 16px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
.gate-btn {
|
.gate-btn {
|
||||||
padding: 6px 14px;
|
padding: 5px 12px;
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border: 1px solid #2a2a3a;
|
border: 1px solid #2a2a3a;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -52,31 +52,121 @@
|
|||||||
.gate-btn.input-btn:hover { border-color: #55aaff; }
|
.gate-btn.input-btn:hover { border-color: #55aaff; }
|
||||||
.gate-btn.output-btn { border-color: #ff8833; }
|
.gate-btn.output-btn { border-color: #ff8833; }
|
||||||
.gate-btn.output-btn:hover { border-color: #ffaa55; }
|
.gate-btn.output-btn:hover { border-color: #ffaa55; }
|
||||||
.separator { width: 1px; height: 28px; background: #2a2a3a; margin: 0 8px; }
|
.separator { width: 1px; height: 24px; background: #2a2a3a; margin: 0 6px; }
|
||||||
.toolbar-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
.toolbar-right { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 6px 12px;
|
padding: 5px 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #888;
|
color: #888;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
.action-btn:hover { border-color: #ff4444; color: #ff4444; }
|
.action-btn:hover { border-color: #ff4444; color: #ff4444; }
|
||||||
.action-btn.help-btn:hover { border-color: #00e599; color: #00e599; }
|
.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 */
|
||||||
#canvas {
|
#canvas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 56px;
|
top: 48px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
cursor: default;
|
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 */
|
||||||
#help-modal {
|
#help-modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -92,28 +182,29 @@
|
|||||||
background: #12121a;
|
background: #12121a;
|
||||||
border: 1px solid #2a2a3a;
|
border: 1px solid #2a2a3a;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 32px;
|
padding: 24px;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
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 {
|
#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 ul { padding-left: 20px; }
|
||||||
#help-content kbd {
|
#help-content kbd {
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px 6px;
|
padding: 1px 5px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
#help-close {
|
#help-close {
|
||||||
margin-top: 16px;
|
margin-top: 12px;
|
||||||
padding: 8px 20px;
|
padding: 6px 18px;
|
||||||
background: #00e599;
|
background: #00e599;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -121,6 +212,17 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Resize handle */
|
||||||
|
#wave-resize {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 8px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
z-index: 91;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -135,6 +237,8 @@
|
|||||||
<button class="gate-btn" data-gate="NAND">NAND</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="NOR">NOR</button>
|
||||||
<button class="gate-btn" data-gate="XOR">XOR</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">
|
<div class="toolbar-right">
|
||||||
<button class="action-btn help-btn" id="help-btn">? Help</button>
|
<button class="action-btn help-btn" id="help-btn">? Help</button>
|
||||||
<button class="action-btn" id="clear-btn">Clear All</button>
|
<button class="action-btn" id="clear-btn">Clear All</button>
|
||||||
@@ -143,19 +247,45 @@
|
|||||||
|
|
||||||
<canvas id="canvas"></canvas>
|
<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>
|
||||||
|
<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-modal">
|
||||||
<div id="help-content">
|
<div id="help-content">
|
||||||
<h2>Logic Gate Simulator</h2>
|
<h2>Logic Gate Simulator</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Click a gate button in the toolbar, then click on the canvas to place it</li>
|
<li>Click a gate button, then click on the canvas to place it</li>
|
||||||
<li>Drag gates to move them around</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 <strong>output port</strong> (right), then an <strong>input port</strong> (left) to connect</li>
|
||||||
<li>Click on an <kbd>INPUT</kbd> gate to toggle its value (0/1)</li>
|
<li>Click 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><kbd>Delete</kbd> while hovering a gate to remove it</li>
|
||||||
<li>Right-click a connection to delete it</li>
|
<li>Right-click a port to delete its connections</li>
|
||||||
<li>The circuit evaluates in real-time</li>
|
<li><kbd>Escape</kbd> to cancel placing/connecting</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p style="margin-top: 16px; color: #666;">Built with ❤️ at MontLab</p>
|
<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 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>
|
<button id="help-close">Got it</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +300,7 @@
|
|||||||
let placingGate = null;
|
let placingGate = null;
|
||||||
let dragging = null;
|
let dragging = null;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
let connecting = null; // { gate, portIndex, portType }
|
let connecting = null;
|
||||||
let hoveredGate = null;
|
let hoveredGate = null;
|
||||||
let hoveredPort = null;
|
let hoveredPort = null;
|
||||||
let mouseX = 0, mouseY = 0;
|
let mouseX = 0, mouseY = 0;
|
||||||
@@ -185,6 +315,22 @@
|
|||||||
INPUT: '#3388ff', OUTPUT: '#ff8833'
|
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) {
|
function gateInputCount(type) {
|
||||||
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1;
|
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1;
|
||||||
return 2;
|
return 2;
|
||||||
@@ -196,10 +342,8 @@
|
|||||||
function evaluate(gate, visited = new Set()) {
|
function evaluate(gate, visited = new Set()) {
|
||||||
if (visited.has(gate.id)) return gate.value || 0;
|
if (visited.has(gate.id)) return gate.value || 0;
|
||||||
visited.add(gate.id);
|
visited.add(gate.id);
|
||||||
|
|
||||||
if (gate.type === 'INPUT') return gate.value;
|
if (gate.type === 'INPUT') return gate.value;
|
||||||
|
|
||||||
// Get input values
|
|
||||||
const inputCount = gateInputCount(gate.type);
|
const inputCount = gateInputCount(gate.type);
|
||||||
const inputs = [];
|
const inputs = [];
|
||||||
for (let i = 0; i < inputCount; i++) {
|
for (let i = 0; i < inputCount; i++) {
|
||||||
@@ -229,8 +373,212 @@
|
|||||||
function evaluateAll() {
|
function evaluateAll() {
|
||||||
gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; });
|
gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; });
|
||||||
gates.forEach(g => evaluate(g));
|
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) {
|
function getInputPorts(gate) {
|
||||||
const count = gateInputCount(gate.type);
|
const count = gateInputCount(gate.type);
|
||||||
const ports = [];
|
const ports = [];
|
||||||
@@ -252,7 +600,8 @@
|
|||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight - 56;
|
const waveH = waveformVisible ? waveformHeight : 0;
|
||||||
|
canvas.height = window.innerHeight - 48 - waveH;
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
resize();
|
resize();
|
||||||
@@ -262,20 +611,17 @@
|
|||||||
const isHovered = hoveredGate === gate;
|
const isHovered = hoveredGate === gate;
|
||||||
const isActive = gate.value === 1;
|
const isActive = gate.value === 1;
|
||||||
|
|
||||||
// Shadow for active gates
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
ctx.shadowColor = color;
|
ctx.shadowColor = color;
|
||||||
ctx.shadowBlur = 20;
|
ctx.shadowBlur = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body
|
|
||||||
ctx.fillStyle = isActive ? color + '22' : '#14141e';
|
ctx.fillStyle = isActive ? color + '22' : '#14141e';
|
||||||
ctx.strokeStyle = isHovered ? '#fff' : color;
|
ctx.strokeStyle = isHovered ? '#fff' : color;
|
||||||
ctx.lineWidth = isHovered ? 2.5 : 1.5;
|
ctx.lineWidth = isHovered ? 2.5 : 1.5;
|
||||||
|
|
||||||
const r = 8;
|
|
||||||
ctx.beginPath();
|
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.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
@@ -285,21 +631,27 @@
|
|||||||
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
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));
|
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
|
// Value for INPUT/OUTPUT
|
||||||
if (gate.type === 'INPUT' || gate.type === '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.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 => {
|
getInputPorts(gate).forEach(p => {
|
||||||
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'input';
|
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 conn = connections.find(c => c.to === gate.id && c.toPort === p.index);
|
||||||
const portActive = conn ? gates.find(g => g.id === conn.from)?.value : 0;
|
const portActive = conn ? gates.find(g => g.id === conn.from)?.value : 0;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
||||||
@@ -309,10 +661,8 @@
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Output ports
|
|
||||||
getOutputPorts(gate).forEach(p => {
|
getOutputPorts(gate).forEach(p => {
|
||||||
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'output';
|
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'output';
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
|
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
|
||||||
@@ -342,7 +692,6 @@
|
|||||||
ctx.lineWidth = active ? 2.5 : 1.5;
|
ctx.lineWidth = active ? 2.5 : 1.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Glow effect for active wires
|
|
||||||
if (active) {
|
if (active) {
|
||||||
ctx.strokeStyle = '#00ff8844';
|
ctx.strokeStyle = '#00ff8844';
|
||||||
ctx.lineWidth = 6;
|
ctx.lineWidth = 6;
|
||||||
@@ -393,6 +742,12 @@
|
|||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw waveform
|
||||||
|
if (waveformVisible) {
|
||||||
|
drawWaveLabels();
|
||||||
|
drawWaveforms();
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,19 +758,16 @@
|
|||||||
function findPortAt(x, y) {
|
function findPortAt(x, y) {
|
||||||
for (const gate of gates) {
|
for (const gate of gates) {
|
||||||
for (const p of getInputPorts(gate)) {
|
for (const p of getInputPorts(gate)) {
|
||||||
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
|
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'input' };
|
||||||
return { gate, index: p.index, type: 'input' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const p of getOutputPorts(gate)) {
|
for (const p of getOutputPorts(gate)) {
|
||||||
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
|
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'output' };
|
||||||
return { gate, index: p.index, type: 'output' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== EVENTS ====================
|
||||||
canvas.addEventListener('mousemove', e => {
|
canvas.addEventListener('mousemove', e => {
|
||||||
mouseX = e.offsetX;
|
mouseX = e.offsetX;
|
||||||
mouseY = e.offsetY;
|
mouseY = e.offsetY;
|
||||||
@@ -438,43 +790,23 @@
|
|||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
const x = e.offsetX, y = e.offsetY;
|
const x = e.offsetX, y = e.offsetY;
|
||||||
|
|
||||||
// Placing a gate
|
|
||||||
if (placingGate) {
|
if (placingGate) {
|
||||||
gates.push({
|
gates.push({ id: nextId++, type: placingGate, x: x - GATE_W / 2, y: y - GATE_H / 2, value: 0 });
|
||||||
id: nextId++,
|
|
||||||
type: placingGate,
|
|
||||||
x: x - GATE_W / 2,
|
|
||||||
y: y - GATE_H / 2,
|
|
||||||
value: 0
|
|
||||||
});
|
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
placingGate = null;
|
placingGate = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check port click
|
|
||||||
const port = findPortAt(x, y);
|
const port = findPortAt(x, y);
|
||||||
if (port) {
|
if (port) {
|
||||||
if (connecting) {
|
if (connecting) {
|
||||||
// Complete connection
|
|
||||||
if (connecting.portType === 'output' && port.type === 'input') {
|
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 = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
||||||
connections.push({
|
connections.push({ from: connecting.gate.id, fromPort: connecting.portIndex, to: port.gate.id, toPort: port.index });
|
||||||
from: connecting.gate.id,
|
|
||||||
fromPort: connecting.portIndex,
|
|
||||||
to: port.gate.id,
|
|
||||||
toPort: port.index
|
|
||||||
});
|
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
} else if (connecting.portType === 'input' && port.type === 'output') {
|
} else if (connecting.portType === 'input' && port.type === 'output') {
|
||||||
connections = connections.filter(c => !(c.to === connecting.gate.id && c.toPort === connecting.portIndex));
|
connections = connections.filter(c => !(c.to === connecting.gate.id && c.toPort === connecting.portIndex));
|
||||||
connections.push({
|
connections.push({ from: port.gate.id, fromPort: port.index, to: connecting.gate.id, toPort: connecting.portIndex });
|
||||||
from: port.gate.id,
|
|
||||||
fromPort: port.index,
|
|
||||||
to: connecting.gate.id,
|
|
||||||
toPort: connecting.portIndex
|
|
||||||
});
|
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
}
|
}
|
||||||
connecting = null;
|
connecting = null;
|
||||||
@@ -484,13 +816,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel connecting
|
if (connecting) { connecting = null; return; }
|
||||||
if (connecting) {
|
|
||||||
connecting = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle input
|
|
||||||
const gate = findGateAt(x, y);
|
const gate = findGateAt(x, y);
|
||||||
if (gate && gate.type === 'INPUT') {
|
if (gate && gate.type === 'INPUT') {
|
||||||
gate.value = gate.value ? 0 : 1;
|
gate.value = gate.value ? 0 : 1;
|
||||||
@@ -498,7 +825,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start drag
|
|
||||||
if (gate) {
|
if (gate) {
|
||||||
dragging = gate;
|
dragging = gate;
|
||||||
dragOffset = { x: x - gate.x, y: y - gate.y };
|
dragOffset = { x: x - gate.x, y: y - gate.y };
|
||||||
@@ -506,50 +832,39 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', () => {
|
canvas.addEventListener('mouseup', () => { dragging = null; });
|
||||||
dragging = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Right-click to delete connections
|
|
||||||
canvas.addEventListener('contextmenu', e => {
|
canvas.addEventListener('contextmenu', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const x = e.offsetX, y = e.offsetY;
|
const x = e.offsetX, y = e.offsetY;
|
||||||
|
|
||||||
// Check if clicking near a connection
|
|
||||||
const port = findPortAt(x, y);
|
const port = findPortAt(x, y);
|
||||||
if (port && port.type === 'input') {
|
if (port && port.type === 'input') {
|
||||||
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
return;
|
} else if (port && port.type === 'output') {
|
||||||
}
|
|
||||||
if (port && port.type === 'output') {
|
|
||||||
connections = connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
|
connections = connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete key
|
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
if (hoveredGate && document.activeElement === document.body) {
|
if (hoveredGate && document.activeElement === document.body) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
connections = connections.filter(c => c.from !== hoveredGate.id && c.to !== hoveredGate.id);
|
const gateId = hoveredGate.id;
|
||||||
gates = gates.filter(g => g !== hoveredGate);
|
connections = connections.filter(c => c.from !== gateId && c.to !== gateId);
|
||||||
|
gates = gates.filter(g => g.id !== gateId);
|
||||||
|
delete waveData[gateId];
|
||||||
hoveredGate = null;
|
hoveredGate = null;
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') { placingGate = null; connecting = null; }
|
||||||
placingGate = null;
|
|
||||||
connecting = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toolbar buttons
|
// Toolbar
|
||||||
document.querySelectorAll('.gate-btn').forEach(btn => {
|
document.querySelectorAll('.gate-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => { placingGate = btn.dataset.gate; });
|
||||||
placingGate = btn.dataset.gate;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('clear-btn').addEventListener('click', () => {
|
document.getElementById('clear-btn').addEventListener('click', () => {
|
||||||
@@ -558,9 +873,13 @@
|
|||||||
connections = [];
|
connections = [];
|
||||||
connecting = null;
|
connecting = null;
|
||||||
placingGate = null;
|
placingGate = null;
|
||||||
|
waveData = {};
|
||||||
|
timeStep = 0;
|
||||||
|
updateWaveInfo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Help
|
||||||
document.getElementById('help-btn').addEventListener('click', () => {
|
document.getElementById('help-btn').addEventListener('click', () => {
|
||||||
document.getElementById('help-modal').classList.add('visible');
|
document.getElementById('help-modal').classList.add('visible');
|
||||||
});
|
});
|
||||||
@@ -568,10 +887,57 @@
|
|||||||
document.getElementById('help-modal').classList.remove('visible');
|
document.getElementById('help-modal').classList.remove('visible');
|
||||||
});
|
});
|
||||||
document.getElementById('help-modal').addEventListener('click', e => {
|
document.getElementById('help-modal').addEventListener('click', e => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) document.getElementById('help-modal').classList.remove('visible');
|
||||||
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
|
// Start
|
||||||
draw();
|
draw();
|
||||||
|
|||||||
Reference in New Issue
Block a user