mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-24 12:45:18 +00:00
Extends VCR speed cycle to `[0.25, 0.5, 1, 2, 4, 8]` so users can watch
live paths in slow motion.
## Changes
- `vcrSpeedCycle()`: speed array extended to include `¼x` and `½x`;
saves preference to `localStorage('live-vcr-speed')`
- `speedLabel()`: new helper returning `¼x` / `½x` for sub-1x, used in
the speed button
- `drawAnimatedLine`: step interval scales with speed (`33 / VCR.speed`)
- `drawMatrixLine`: `DURATION_MS` scales with speed (`1100 / VCR.speed`)
- Speed preference restored from localStorage on page load
## Tests
3 new unit tests; 72 pass, 0 regressions.
Closes #771 (M1 of 3)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+17
-6
@@ -96,6 +96,8 @@
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const _savedSpeed = parseFloat(localStorage.getItem('live-vcr-speed'));
|
||||
const _initialSpeed = [0.25, 0.5, 1, 2, 4, 8].includes(_savedSpeed) ? _savedSpeed : 1;
|
||||
let rainCanvas = null, rainCtx = null, rainDrops = [], rainRAF = null;
|
||||
const propagationBuffer = new Map(); // hash -> {timer, packets[]}
|
||||
let _onResize = null;
|
||||
@@ -579,10 +581,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
function speedLabel(s) {
|
||||
if (s === 0.25) return '¼x';
|
||||
if (s === 0.5) return '½x';
|
||||
return s + 'x';
|
||||
}
|
||||
|
||||
function vcrSpeedCycle() {
|
||||
const speeds = [1, 2, 4, 8];
|
||||
const speeds = [0.25, 0.5, 1, 2, 4, 8];
|
||||
const idx = speeds.indexOf(VCR.speed);
|
||||
VCR.speed = speeds[(idx + 1) % speeds.length];
|
||||
localStorage.setItem('live-vcr-speed', VCR.speed);
|
||||
updateVCRUI();
|
||||
// If replaying, restart with new speed
|
||||
if (VCR.mode === 'REPLAY' && VCR.replayTimer) {
|
||||
@@ -718,7 +727,7 @@
|
||||
if (pauseBtn) { pauseBtn.textContent = '⏸'; pauseBtn.setAttribute('aria-label', 'Pause'); }
|
||||
if (missedEl) missedEl.classList.add('hidden');
|
||||
}
|
||||
if (speedBtn) { speedBtn.textContent = VCR.speed + 'x'; speedBtn.setAttribute('aria-label', 'Speed ' + VCR.speed + 'x'); }
|
||||
if (speedBtn) { speedBtn.textContent = speedLabel(VCR.speed); speedBtn.setAttribute('aria-label', 'Speed ' + speedLabel(VCR.speed)); }
|
||||
updateVCRLcd();
|
||||
}
|
||||
|
||||
@@ -2461,6 +2470,7 @@
|
||||
window._liveFormatLiveTimestampHtml = formatLiveTimestampHtml;
|
||||
window._liveResolveHopPositions = resolveHopPositions;
|
||||
window._liveVcrSpeedCycle = vcrSpeedCycle;
|
||||
window._liveSpeedLabel = speedLabel;
|
||||
window._liveVcrPause = vcrPause;
|
||||
window._liveVcrResumeLive = vcrResumeLive;
|
||||
window._liveVcrSetMode = vcrSetMode;
|
||||
@@ -3129,7 +3139,7 @@
|
||||
|
||||
const matrixGreen = '#00ff41';
|
||||
const TRAIL_LEN = Math.min(6, bytes.length);
|
||||
const DURATION_MS = 1100; // total hop duration
|
||||
const DURATION_MS = 1100 / VCR.speed;
|
||||
const CHAR_INTERVAL = 0.06; // spawn a char every 6% of progress
|
||||
const charMarkers = [];
|
||||
let nextCharAt = CHAR_INTERVAL;
|
||||
@@ -3261,8 +3271,9 @@
|
||||
return;
|
||||
}
|
||||
const elapsed = now - lastStep;
|
||||
if (elapsed >= 33) {
|
||||
const ticks = Math.min(Math.floor(elapsed / 33), 4);
|
||||
const stepMs = 33 / VCR.speed;
|
||||
if (elapsed >= stepMs) {
|
||||
const ticks = Math.min(Math.floor(elapsed / stepMs), 4);
|
||||
lastStep = now;
|
||||
for (let t = 0; t < ticks && step < steps; t++) {
|
||||
step++;
|
||||
@@ -3605,7 +3616,7 @@
|
||||
packetCount = 0; activeAnims = 0;
|
||||
nodeActivity = {}; pktTimestamps = [];
|
||||
feedDedup.clear();
|
||||
VCR.buffer = []; VCR.playhead = -1; VCR.mode = 'LIVE'; VCR.missedCount = 0; VCR.speed = 1; VCR.replayGen = 0;
|
||||
VCR.buffer = []; VCR.playhead = -1; VCR.mode = 'LIVE'; VCR.missedCount = 0; VCR.speed = _initialSpeed; VCR.replayGen = 0;
|
||||
}
|
||||
|
||||
let _themeRefreshHandler = null;
|
||||
|
||||
+26
-3
@@ -402,9 +402,13 @@ console.log('\n=== live.js: VCR state machine ===');
|
||||
assert.strictEqual(VCR().mode, 'PAUSED', 'mode should stay PAUSED after second call');
|
||||
});
|
||||
|
||||
test('vcrSpeedCycle cycles through 1,2,4,8', () => {
|
||||
test('vcrSpeedCycle cycles through 0.25, 0.5, 1, 2, 4, 8 and wraps', () => {
|
||||
vcrSetMode('LIVE');
|
||||
VCR().speed = 1;
|
||||
VCR().speed = 0.25;
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(VCR().speed, 0.5);
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(VCR().speed, 1);
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(VCR().speed, 2);
|
||||
vcrSpeedCycle();
|
||||
@@ -412,7 +416,26 @@ console.log('\n=== live.js: VCR state machine ===');
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(VCR().speed, 8);
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(VCR().speed, 1); // wraps around
|
||||
assert.strictEqual(VCR().speed, 0.25); // wraps around
|
||||
});
|
||||
|
||||
test('vcrSpeedCycle saves speed to localStorage', () => {
|
||||
VCR().speed = 1;
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(ctx.localStorage.getItem('live-vcr-speed'), '2');
|
||||
vcrSpeedCycle();
|
||||
assert.strictEqual(ctx.localStorage.getItem('live-vcr-speed'), '4');
|
||||
});
|
||||
|
||||
const speedLabel = ctx.window._liveSpeedLabel;
|
||||
assert.ok(speedLabel, '_liveSpeedLabel must be exposed');
|
||||
|
||||
test('speedLabel returns fraction strings for sub-1x speeds', () => {
|
||||
assert.strictEqual(speedLabel(0.25), '¼x');
|
||||
assert.strictEqual(speedLabel(0.5), '½x');
|
||||
assert.strictEqual(speedLabel(1), '1x');
|
||||
assert.strictEqual(speedLabel(2), '2x');
|
||||
assert.strictEqual(speedLabel(8), '8x');
|
||||
});
|
||||
|
||||
const vcrResumeLive = ctx.window._liveVcrResumeLive;
|
||||
|
||||
Reference in New Issue
Block a user