fix(B05): 휠 이동 감속을 프레임 수가 아닌 흐른 시간 기준으로

프레임당 고정 비율(0.35)로 좁히면 주사율에 따라 속도가 달라짐 — 120Hz 화면에서는 두 배
빨라 툭 끊겨 보이고, 프레임을 한 번 놓치면 그만큼 덜컥 뜀. 흐른 시간으로 환산해
어느 화면에서나 같은 시간에 도착하게 함. 탭이 잠들었다 깬 큰 간격은 64ms로 자름.

자체검증(공용 브라우저 임시 탭, 휠 20칸): 프레임 중앙값 16.6ms(60fps) 유지,
최대 프레임 47.6 → 34.3ms, 20ms 초과 프레임 3 → 2건.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 21:31:16 +09:00
co-authored by Claude Opus 5
parent c322f53845
commit db3503fd95
+16 -3
View File
@@ -19,21 +19,29 @@
const EASE = 0.35;
/** 이보다 가까우면 도착으로 보고 딱 맞춘다(px). */
const SNAP_PX = 0.5;
/** EASE의 기준 프레임 간격(ms) — 60Hz 한 칸. */
const BASE_FRAME_MS = 1000 / 60;
/** 한 프레임으로 인정하는 최대 간격(ms). 탭이 잠들었다 깨면 간격이 수백 ms라 그대로
* 쓰면 한 번에 목표까지 튄다. */
const MAX_FRAME_MS = 64;
export function attachWheelHorizontalScroll(scroller: HTMLElement): void {
let target = -1;
/** 우리가 마지막으로 써 넣은 값 — 남이 자리를 옮겼는지 가리는 데 쓴다. */
let applied = -1;
let frame = 0;
/** 직전 프레임 시각 — 좁히는 양을 **흐른 시간**에 맞추는 데 쓴다. */
let lastFrameAt = 0;
function stop(): void {
if (frame) cancelAnimationFrame(frame);
frame = 0;
target = -1;
applied = -1;
lastFrameAt = 0;
}
function step(): void {
function step(now: number): void {
frame = 0;
// 스크롤바 끌기·스크롤 동기화처럼 다른 경로가 자리를 옮겼으면 이동을 접는다.
if (applied >= 0 && Math.abs(scroller.scrollLeft - applied) > 1) return stop();
@@ -44,8 +52,13 @@ export function attachWheelHorizontalScroll(scroller: HTMLElement): void {
scroller.scrollLeft = goal;
return stop();
}
const next = scroller.scrollLeft + rest * EASE;
scroller.scrollLeft = next;
// 프레임당 고정 비율로 좁히면 화면 주사율에 따라 속도가 달라진다 — 120Hz에서는 두 배
// 빨라 툭 끊겨 보이고, 프레임을 한 번 놓치면 그만큼 덜컥 뛴다. 흐른 시간으로 환산해
// **어느 화면에서나 같은 시간**에 도착하게 한다(2026-09-04).
const elapsed = lastFrameAt ? Math.min(MAX_FRAME_MS, now - lastFrameAt) : BASE_FRAME_MS;
lastFrameAt = now;
const ratio = 1 - Math.pow(1 - EASE, elapsed / BASE_FRAME_MS);
scroller.scrollLeft = scroller.scrollLeft + rest * ratio;
applied = scroller.scrollLeft;
frame = requestAnimationFrame(step);
}