feat(speed): switch Playback Speed to 0x–4x forward (drop reverse)

Real-browser eyeball: below-0x reverse jittered because the base loop
clips aren't all-intra (the flagged risk). Narrow the slider to 0x–4x
forward (still continuous, step=any); 0x still freezes; OS reduced-motion
behavior unchanged. Removes the manual rAF reverse loop and the
HEFScrub.reverseStep helper + its unit tests.

Tests: node unit 10/10; a11y e2e 12/12 (Task 5 now 3 slider tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 12:48:14 -07:00
parent df8b245af3
commit e064a2e684
6 changed files with 36 additions and 115 deletions
-25
View File
@@ -59,31 +59,6 @@ test("integerCrossings: multiple crossings in travel order", () => {
assert.deepEqual(S.integerCrossings(3.2, 1.8), [{ index: 3, dir: -1 }, { index: 2, dir: -1 }]);
});
test("reverseStep: decrements within the loop region without wrapping", () => {
// region [loopStart=3, duration=8]; step back absSpeed*dt = 0.5
assert.deepEqual(S.reverseStep(5, 1, 0.5, 3, 8), { next: 4.5, wrapped: false });
// landing exactly on the bottom boundary is the region edge, not a wrap
assert.deepEqual(S.reverseStep(3.5, 1, 0.5, 3, 8), { next: 3, wrapped: false });
});
test("reverseStep: wraps past the bottom boundary back to the tail (duration)", () => {
// 3.2 - 0.5 = 2.7 < 3 -> overshoot 0.3 -> 8 - 0.3 = 7.7
const r = S.reverseStep(3.2, 1, 0.5, 3, 8);
assert.equal(r.wrapped, true);
assert.ok(Math.abs(r.next - 7.7) < 1e-9, `next=${r.next}`);
});
test("reverseStep: overshoot larger than the region length wraps modulo", () => {
// region length L = 5; 3 - 7 = -4 -> overshoot 7 -> 7 % 5 = 2 -> 8 - 2 = 6
const r = S.reverseStep(3, 1, 7, 3, 8);
assert.equal(r.wrapped, true);
assert.ok(Math.abs(r.next - 6) < 1e-9, `next=${r.next}`);
});
test("reverseStep: degenerate region (duration <= loopStart) holds at duration", () => {
assert.deepEqual(S.reverseStep(3, 1, 0.5, 3, 3), { next: 3, wrapped: false });
});
test("loopLandFrame: descend lands on the morph's tail frame, ascend on its head", () => {
// A morph spans src@0 (frac 0) -> dst@loopTailS (frac 1). Descending (dir>0) lands
// on dst at frac 1 -> the base loop must continue from loopTailS. Ascending (dir<0)