fix(audio): grabbing the playhead thumb no longer teleports it
A pointerdown on the scrub bar unconditionally mapped the click X to a position, so grabbing the handle itself made the playhead jump — worst with Shift, whose branch force-snapped to the nearest chapter line before the drag even started. The user's complaint: the handle click flowed through to the track-click behavior behind it. The bar now hit-tests the thumb first (GRAB_SLOP_PX, a pixel test like the chapter magnetism — a seconds threshold goes sub-pixel on long books). A grab enters a new "grab" drag mode: the position stays where it is until the pointer moves, then advances RELATIVELY via the new applyGrab helper (incremental like the touch cursor, fixed scale — the mouse is always on the bar, so Shift = quarter-speed fine crawl instead of a pull-away zoom). Track clicks keep today's behavior exactly: click = jump with chapter magnetism, Shift+click = nearest chapter line, Shift+drag = precision rail, touch = incremental pull-away. MOUSE_GUIDE gains the two grab rows so the dialog documents the gesture; new pure helpers are unit- tested alongside the existing scrub math.
This commit is contained in:
1 parent
c71ef4af85
commit
79f4d668f1
4 files changed
+153
-7
No files matched your search
@@ -73,6 +73,50 @@ export function snapToChapterPx(
|
||||
return { seconds, snapped: false };
|
||||
}
|
||||
|
||||
// ---- §9 handle grab: the thumb is not part of the track ----
|
||||
|
||||
// GRAB_SLOP_PX is how close a pointerdown must land to the playhead
|
||||
// thumb, in bar pixels, for it to count as grabbing the HANDLE rather
|
||||
// than clicking the track. A grab never teleports the position — not
|
||||
// to the click X (free-drag behavior) and not to a chapter line
|
||||
// (Shift+click behavior) — it drags RELATIVE to where the handle is.
|
||||
export const GRAB_SLOP_PX = 12;
|
||||
|
||||
// isHandleGrab reports whether a pointerdown at clientX lands on the
|
||||
// playhead thumb (within slopPx) instead of the bare track.
|
||||
export function isHandleGrab(
|
||||
clientX: number,
|
||||
rect: DOMRect,
|
||||
position: number,
|
||||
duration: number,
|
||||
slopPx = GRAB_SLOP_PX,
|
||||
): boolean {
|
||||
if (rect.width <= 0 || duration <= 0) return false;
|
||||
const clamped = Math.min(duration, Math.max(0, position));
|
||||
const thumbX = rect.left + (clamped / duration) * rect.width;
|
||||
return Math.abs(clientX - thumbX) <= slopPx;
|
||||
}
|
||||
|
||||
// GRAB_FINE_SCALE is the Shift+grab travel factor: the same relative
|
||||
// drag crawls at quarter speed for precise placement.
|
||||
export const GRAB_FINE_SCALE = 0.25;
|
||||
|
||||
// applyGrab advances a HANDLE drag by one pointermove: incremental
|
||||
// (like applyDrag, a scale change never teleports the cursor), but
|
||||
// with a FIXED scale instead of a pull-away zoom — the mouse is always
|
||||
// on the bar, so Shift is the precision control, not vertical offset.
|
||||
export function applyGrab(
|
||||
position: number,
|
||||
dxPx: number,
|
||||
pxPerSecond: number,
|
||||
duration: number,
|
||||
scale = 1,
|
||||
): number {
|
||||
const clamped = Math.min(duration, Math.max(0, position));
|
||||
if (pxPerSecond <= 0 || duration <= 0) return clamped;
|
||||
return Math.min(duration, Math.max(0, clamped + (dxPx / pxPerSecond) * scale));
|
||||
}
|
||||
|
||||
// ---- §9 touch precision: vertical pull-away = progressive zoom ----
|
||||
|
||||
// The scale factor decays an order of magnitude for every DRAG_REF_PX of
|
||||
|
||||
Reference in new issue
Block a user