feat: waveform uses real time (ms) - clock speed affects wave width

- Slow clock (1000ms) = wider pulses
- Fast clock (100ms) = narrower pulses
- Timeline shows ms/s labels
- Zoom controls scale the time axis
This commit is contained in:
Jose Luis Montañes
2026-03-19 22:13:41 +01:00
parent d471b0adb3
commit c162adb1df

View File

@@ -31,7 +31,8 @@ export function recordSample() {
if (!changed && state.timeStep > 0) return; if (!changed && state.timeStep > 0) return;
state.timeStep++; // Manual toggles advance by simSpeed too for consistency
state.timeStep += state.simSpeed;
gates.forEach(g => { gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = []; if (!waveData[g.id]) waveData[g.id] = [];
const arr = waveData[g.id]; const arr = waveData[g.id];
@@ -43,7 +44,8 @@ export function recordSample() {
} }
export function forceRecordSample() { export function forceRecordSample() {
state.timeStep++; // Advance time by the current simSpeed (in ms) to reflect real time
state.timeStep += state.simSpeed;
state.gates.forEach(g => { state.gates.forEach(g => {
if (!state.waveData[g.id]) state.waveData[g.id] = []; if (!state.waveData[g.id]) state.waveData[g.id] = [];
state.waveData[g.id].push({ t: state.timeStep, value: g.value }); state.waveData[g.id].push({ t: state.timeStep, value: g.value });
@@ -71,7 +73,10 @@ export function setEvaluateAll(fn) {
export function updateWaveInfo() { export function updateWaveInfo() {
const totalSamples = Object.values(state.waveData).reduce((sum, arr) => sum + arr.length, 0); const totalSamples = Object.values(state.waveData).reduce((sum, arr) => sum + arr.length, 0);
document.getElementById('wave-info').textContent = `T=${state.timeStep} | ${totalSamples} samples`; const timeLabel = state.timeStep >= 1000
? `${(state.timeStep/1000).toFixed(1)}s`
: `${state.timeStep}ms`;
document.getElementById('wave-info').textContent = `T=${timeLabel} | ${totalSamples} samples`;
} }
export function clearWaveData() { export function clearWaveData() {
@@ -121,32 +126,48 @@ export function drawWaveforms() {
return; return;
} }
// Auto-scroll to show latest (only if we're already near the end) // pxPerMs: how many pixels per millisecond of simulation time
const maxVisible = Math.floor(wc.width / state.waveZoom); const pxPerMs = state.waveZoom / 100; // waveZoom=20 → 0.2 px/ms
const isNearEnd = state.waveScroll >= state.timeStep - maxVisible - 2;
if (state.timeStep > maxVisible && isNearEnd) { // Total width in pixels for all recorded time
state.waveScroll = state.timeStep - maxVisible; const totalPx = state.timeStep * pxPerMs;
}
// Clamp scroll to valid range // Visible width in ms
state.waveScroll = Math.max(0, Math.min(state.timeStep - 1, state.waveScroll)); const visibleMs = wc.width / pxPerMs;
// Auto-scroll to show latest (only if near the end)
const isNearEnd = state.waveScroll >= state.timeStep - visibleMs - state.simSpeed * 2;
if (totalPx > wc.width && isNearEnd) {
state.waveScroll = state.timeStep - visibleMs;
}
state.waveScroll = Math.max(0, state.waveScroll);
// Helper: convert simulation time (ms) to pixel X
const tToX = (t) => (t - state.waveScroll) * pxPerMs;
// Draw time grid (every gridMs milliseconds)
let gridMs = 500;
if (pxPerMs * gridMs < 30) gridMs = 1000;
if (pxPerMs * gridMs < 30) gridMs = 2000;
if (pxPerMs * gridMs > 200) gridMs = 200;
if (pxPerMs * gridMs > 200) gridMs = 100;
// Draw time grid
wctx.strokeStyle = '#151520'; wctx.strokeStyle = '#151520';
wctx.lineWidth = 1; wctx.lineWidth = 1;
for (let t = Math.ceil(state.waveScroll); t <= state.timeStep; t++) { const startT = Math.floor(state.waveScroll / gridMs) * gridMs;
const x = (t - state.waveScroll) * state.waveZoom; for (let t = startT; t <= state.timeStep; t += gridMs) {
const x = tToX(t);
if (x < 0 || x > wc.width) continue; if (x < 0 || x > wc.width) continue;
wctx.beginPath(); wctx.beginPath();
wctx.moveTo(x, 0); wctx.moveTo(x, 0);
wctx.lineTo(x, wc.height); wctx.lineTo(x, wc.height);
wctx.stroke(); wctx.stroke();
if (t % 5 === 0 || state.waveZoom > 30) { wctx.fillStyle = '#333';
wctx.fillStyle = '#333'; wctx.font = '9px monospace';
wctx.font = '9px monospace'; wctx.textAlign = 'center';
wctx.textAlign = 'center'; const label = t >= 1000 ? `${(t/1000).toFixed(1)}s` : `${t}ms`;
wctx.fillText(`${t}`, x, 10); wctx.fillText(label, x, 10);
}
} }
// Row dividers // Row dividers
@@ -159,7 +180,7 @@ export function drawWaveforms() {
wctx.stroke(); wctx.stroke();
}); });
// Draw signals // Draw signals using actual timestamps
tracked.forEach((gate, i) => { tracked.forEach((gate, i) => {
const data = state.waveData[gate.id] || []; const data = state.waveData[gate.id] || [];
if (data.length === 0) return; if (data.length === 0) return;
@@ -173,49 +194,50 @@ export function drawWaveforms() {
wctx.lineWidth = 1.5; wctx.lineWidth = 1.5;
wctx.beginPath(); wctx.beginPath();
let lastVal = 0;
let started = false; let started = false;
// Build complete signal timeline for (let s = 0; s < data.length; s++) {
const timeline = []; const sample = data[s];
for (let t = 1; t <= state.timeStep; t++) { const nextT = s < data.length - 1 ? data[s + 1].t : state.timeStep;
const sample = data.filter(s => s.t <= t).pop(); const x1 = tToX(sample.t);
timeline.push(sample ? sample.value : 0); const x2 = tToX(nextT);
} const y = sample.value ? yHigh : yLow;
for (let t = 0; t < timeline.length; t++) {
const x = (t + 1 - state.waveScroll) * state.waveZoom;
const val = timeline[t];
const y = val ? yHigh : yLow;
if (!started) { if (!started) {
wctx.moveTo(x, y); // Draw from time 0 to first sample
const x0 = tToX(0);
const initY = yLow; // default low before first sample
if (x0 < wc.width) {
wctx.moveTo(Math.max(0, x0), initY);
if (sample.t > 0) wctx.lineTo(x1, initY);
wctx.lineTo(x1, y);
}
started = true; started = true;
} else { } else {
if (val !== lastVal) { // Vertical transition from previous value
wctx.lineTo(x, lastVal ? yHigh : yLow); const prevVal = data[s - 1].value;
wctx.lineTo(x, y); if (sample.value !== prevVal) {
wctx.lineTo(x1, prevVal ? yHigh : yLow);
wctx.lineTo(x1, y);
} }
wctx.lineTo(x + state.waveZoom, y);
} }
lastVal = val; // Horizontal line to next transition
wctx.lineTo(x2, y);
// Fill high regions
if (sample.value) {
wctx.save();
wctx.globalAlpha = 0.08;
wctx.fillStyle = color;
wctx.fillRect(x1, yHigh, x2 - x1, sigH);
wctx.restore();
}
} }
wctx.stroke(); wctx.stroke();
// Fill area under signal
wctx.globalAlpha = 0.08;
wctx.fillStyle = color;
for (let t = 0; t < timeline.length; t++) {
const x = (t + 1 - state.waveScroll) * state.waveZoom;
if (timeline[t]) {
wctx.fillRect(x, yHigh, state.waveZoom, sigH);
}
}
wctx.globalAlpha = 1;
}); });
// Cursor line at current time // Cursor line at current time
const cursorX = (state.timeStep - state.waveScroll) * state.waveZoom; const cursorX = tToX(state.timeStep);
if (cursorX >= 0 && cursorX <= wc.width) { if (cursorX >= 0 && cursorX <= wc.width) {
wctx.strokeStyle = '#00e59966'; wctx.strokeStyle = '#00e59966';
wctx.lineWidth = 1; wctx.lineWidth = 1;