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;
state.timeStep++;
// Manual toggles advance by simSpeed too for consistency
state.timeStep += state.simSpeed;
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
const arr = waveData[g.id];
@@ -43,7 +44,8 @@ export function recordSample() {
}
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 => {
if (!state.waveData[g.id]) state.waveData[g.id] = [];
state.waveData[g.id].push({ t: state.timeStep, value: g.value });
@@ -71,7 +73,10 @@ export function setEvaluateAll(fn) {
export function updateWaveInfo() {
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() {
@@ -121,32 +126,48 @@ export function drawWaveforms() {
return;
}
// Auto-scroll to show latest (only if we're already near the end)
const maxVisible = Math.floor(wc.width / state.waveZoom);
const isNearEnd = state.waveScroll >= state.timeStep - maxVisible - 2;
if (state.timeStep > maxVisible && isNearEnd) {
state.waveScroll = state.timeStep - maxVisible;
}
// Clamp scroll to valid range
state.waveScroll = Math.max(0, Math.min(state.timeStep - 1, state.waveScroll));
// pxPerMs: how many pixels per millisecond of simulation time
const pxPerMs = state.waveZoom / 100; // waveZoom=20 → 0.2 px/ms
// Total width in pixels for all recorded time
const totalPx = state.timeStep * pxPerMs;
// Visible width in ms
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.lineWidth = 1;
for (let t = Math.ceil(state.waveScroll); t <= state.timeStep; t++) {
const x = (t - state.waveScroll) * state.waveZoom;
const startT = Math.floor(state.waveScroll / gridMs) * gridMs;
for (let t = startT; t <= state.timeStep; t += gridMs) {
const x = tToX(t);
if (x < 0 || x > wc.width) continue;
wctx.beginPath();
wctx.moveTo(x, 0);
wctx.lineTo(x, wc.height);
wctx.stroke();
if (t % 5 === 0 || state.waveZoom > 30) {
wctx.fillStyle = '#333';
wctx.font = '9px monospace';
wctx.textAlign = 'center';
wctx.fillText(`${t}`, x, 10);
}
wctx.fillStyle = '#333';
wctx.font = '9px monospace';
wctx.textAlign = 'center';
const label = t >= 1000 ? `${(t/1000).toFixed(1)}s` : `${t}ms`;
wctx.fillText(label, x, 10);
}
// Row dividers
@@ -159,7 +180,7 @@ export function drawWaveforms() {
wctx.stroke();
});
// Draw signals
// Draw signals using actual timestamps
tracked.forEach((gate, i) => {
const data = state.waveData[gate.id] || [];
if (data.length === 0) return;
@@ -173,49 +194,50 @@ export function drawWaveforms() {
wctx.lineWidth = 1.5;
wctx.beginPath();
let lastVal = 0;
let started = false;
// Build complete signal timeline
const timeline = [];
for (let t = 1; t <= state.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 - state.waveScroll) * state.waveZoom;
const val = timeline[t];
const y = val ? yHigh : yLow;
for (let s = 0; s < data.length; s++) {
const sample = data[s];
const nextT = s < data.length - 1 ? data[s + 1].t : state.timeStep;
const x1 = tToX(sample.t);
const x2 = tToX(nextT);
const y = sample.value ? yHigh : yLow;
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;
} else {
if (val !== lastVal) {
wctx.lineTo(x, lastVal ? yHigh : yLow);
wctx.lineTo(x, y);
// Vertical transition from previous value
const prevVal = data[s - 1].value;
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();
// 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
const cursorX = (state.timeStep - state.waveScroll) * state.waveZoom;
const cursorX = tToX(state.timeStep);
if (cursorX >= 0 && cursorX <= wc.width) {
wctx.strokeStyle = '#00e59966';
wctx.lineWidth = 1;