diff --git a/apps/website/src/lib/stage-beats.spec.ts b/apps/website/src/lib/stage-beats.spec.ts index 47e4e84e6..c3bf5a303 100644 --- a/apps/website/src/lib/stage-beats.spec.ts +++ b/apps/website/src/lib/stage-beats.spec.ts @@ -80,9 +80,10 @@ describe('timeAt', () => { it('pins time at the interrupt through the hold and resumes past the threshold', () => { const a = beatWindows()[2]; const at = (f: number) => timeAt(a.from + (a.to - a.from) * f, READY); - expect(at(APPROVE_HOLD.from)).toBe(READY.hold.startMs); - expect(at(0.5)).toBe(READY.hold.startMs); - expect(at(APPROVE_HOLD.to - 1e-6)).toBe(READY.hold.startMs); + // Pinned one millisecond inside the hold, so the frame reports `pause`. + expect(at(APPROVE_HOLD.from)).toBe(READY.hold.startMs + 1); + expect(at(0.5)).toBe(READY.hold.startMs + 1); + expect(at(APPROVE_HOLD.to - 1e-6)).toBe(READY.hold.startMs + 1); expect(at(APPROVE_HOLD.to)).toBe(READY.hold.endMs); expect(at(1)).toBe(READY.beats[2].endMs); }); diff --git a/apps/website/src/lib/stage-beats.ts b/apps/website/src/lib/stage-beats.ts index 7afeaa852..e661945e2 100644 --- a/apps/website/src/lib/stage-beats.ts +++ b/apps/website/src/lib/stage-beats.ts @@ -110,11 +110,13 @@ export function timeAt(p: number, ready: StageReadyMessage): number { : Math.round(lerp(mid, b.endMs, (f - 0.5) / 0.5)); } case 'approve': { + const holdPin = Math.min(ready.hold.startMs + 1, ready.hold.endMs); if (f < APPROVE_HOLD.from) - return Math.round( - lerp(b.startMs, ready.hold.startMs, f / APPROVE_HOLD.from) - ); - if (f < APPROVE_HOLD.to) return ready.hold.startMs; + return Math.round(lerp(b.startMs, holdPin, f / APPROVE_HOLD.from)); + // One millisecond INSIDE the hold: the frame treats a boundary instant as + // belonging to the outgoing run (phaseReachedAt subtracts an epsilon), so + // pinning at hold.startMs exactly would report `stream`, not `pause`. + if (f < APPROVE_HOLD.to) return holdPin; return Math.round( lerp( ready.hold.endMs,