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:
112
js/waveform.js
112
js/waveform.js
@@ -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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
wctx.stroke();
|
||||
// Horizontal line to next transition
|
||||
wctx.lineTo(x2, y);
|
||||
|
||||
// Fill area under signal
|
||||
// Fill high regions
|
||||
if (sample.value) {
|
||||
wctx.save();
|
||||
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.fillRect(x1, yHigh, x2 - x1, sigH);
|
||||
wctx.restore();
|
||||
}
|
||||
}
|
||||
wctx.globalAlpha = 1;
|
||||
wctx.stroke();
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user