@@ -10,13 +10,19 @@ const Sprites = @This();
1010const fly_anim_duration_us : i64 = 750_000 ;
1111
1212/// Cover-flow scrub momentum tuning (sprite-index units). See `fizzy.Fling`.
13+ /// Mouse/trackpad release velocity is measured over a position/time window
14+ /// (`releaseWindowed`), not a per-frame EMA — the EMA converged per frame, so a quick
15+ /// flick built up too little velocity at 60 Hz (e.g. Safari on a deployed build) even
16+ /// though it worked at 120 Hz. The window is wall-clock based, so it's refresh-independent.
1317const sprite_fling : fizzy.Fling.Tuning = .{
1418 .decay = 4.0 ,
1519 .min_start = 1.2 ,
1620 .stop = 0.6 ,
1721 .max = 50.0 ,
18- .idle_s = 0.08 ,
22+ .idle_s = 0.12 ,
1923};
24+ /// Window the mouse/trackpad release velocity is averaged over (s).
25+ const sprite_fling_window_s : f32 = 0.08 ;
2026/// Touch scrub: a finger flick is short and bursty, so start coasting at a lower
2127/// speed and tolerate the small gap the browser leaves before `touchend`. Velocity is
2228/// measured over a position/time window (`releaseWindowed`) rather than the last frame.
@@ -56,8 +62,6 @@ wheel_accum: f32 = 0.0,
5662drag_active : bool = false ,
5763/// Whether the pointer moved between press and release (drag vs. click).
5864moved_since_press : bool = false ,
59- /// Last frame's scrub delta (index units), kept for release when touch skips a final move.
60- last_drag_frame_dx : f32 = 0 ,
6165/// True when the active scrub began with a touch press (not mouse).
6266drag_was_touch : bool = false ,
6367/// Release momentum for the scrub: coasts the flow after a flick, then snaps.
@@ -719,7 +723,6 @@ fn handleInput(self: *Sprites, file: anytype, mode: ScrollMode, count: usize, px
719723 dvui .dragPreStart (me .p , .{ .name = "coverflow_drag" , .cursor = .hand });
720724 self .moved_since_press = false ;
721725 self .drag_was_touch = me .button .touch ();
722- self .last_drag_frame_dx = 0 ;
723726 self .wheel_accum = 0.0 ;
724727 // Grabbing again cancels any in-flight coast and its velocity.
725728 self .fling .begin ();
@@ -793,42 +796,24 @@ fn handleInput(self: *Sprites, file: anytype, mode: ScrollMode, count: usize, px
793796 }
794797
795798 if (! snap_scroll ) {
796- if (self .drag_was_touch ) {
797- // Touch path: record movement into the position/time history and, on
798- // release, coast from a windowed velocity. Kept fully separate from the
799- // mouse/trackpad path below so it can't change that proven behavior.
800- if (self .drag_active ) self .fling .sampleTimed (frame_dx );
801- if (released_moved ) {
802- // A finger flick usually delivers its last `touchmove` and `touchend`
803- // on the same frame, which leaves `drag_active` set. Clear it (after
804- // sampling that final move) so draw()'s `drag_active` branch doesn't
805- // immediately cancel the coast we're about to start — that race was
806- // eating the momentum on ~half of all flicks.
807- self .drag_active = false ;
808- if (! self .fling .releaseWindowed (sprite_fling_touch , sprite_fling_touch_window_s )) {
809- const snapped : i64 = @intFromFloat (@round (self .scroll_pos ));
810- self .goal = @floatFromInt (snapped );
811- dvui .refresh (null , @src (), id );
812- }
813- }
814- } else {
815- if (frame_dx != 0 ) self .last_drag_frame_dx = frame_dx ;
816- // Sample the flick velocity once per frame the drag moved.
817- if (self .drag_active ) self .fling .sample (frame_dx );
818-
819- // On release, coast with the built-up velocity — unless the pointer had
820- // paused or barely moved, in which case ease to the nearest sprite.
821- if (released_moved ) {
822- // Touch often lifts without a move on the same frame; re-sample the
823- // last delta so `release` sees fresh velocity and near-zero idle time.
824- const release_dx = if (frame_dx != 0 ) frame_dx else self .last_drag_frame_dx ;
825- if (release_dx != 0 ) self .fling .sample (release_dx );
826-
827- if (! self .fling .release (sprite_fling )) {
828- const snapped : i64 = @intFromFloat (@round (self .scroll_pos ));
829- self .goal = @floatFromInt (snapped );
830- dvui .refresh (null , @src (), id );
831- }
799+ // Touch and mouse/trackpad share one path: record each moved frame into the
800+ // position/time history and, on release, coast from a velocity averaged over a
801+ // wall-clock window. That window is refresh-independent, so momentum is reliable
802+ // at 60 Hz and 120 Hz alike — unlike the old per-frame EMA, which underread short
803+ // flicks at lower refresh rates. Only the feel tuning differs per input type.
804+ if (self .drag_active ) self .fling .sampleTimed (frame_dx );
805+ if (released_moved ) {
806+ // The last move and the release commonly land on the same frame (more so at
807+ // low refresh), which leaves `drag_active` set. Clear it after sampling that
808+ // final move so draw()'s `drag_active` branch doesn't cancel the coast we
809+ // start here — that race was eating momentum on a large share of flicks.
810+ self .drag_active = false ;
811+ const tuning = if (self .drag_was_touch ) sprite_fling_touch else sprite_fling ;
812+ const window_s = if (self .drag_was_touch ) sprite_fling_touch_window_s else sprite_fling_window_s ;
813+ if (! self .fling .releaseWindowed (tuning , window_s )) {
814+ const snapped : i64 = @intFromFloat (@round (self .scroll_pos ));
815+ self .goal = @floatFromInt (snapped );
816+ dvui .refresh (null , @src (), id );
832817 }
833818 }
834819 } else if (released_moved ) {
0 commit comments