From db3503fd95aa8e8637d81749921f6fefbb2b0895 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 21:31:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=ED=9C=A0=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EA=B0=90=EC=86=8D=EC=9D=84=20=ED=94=84=EB=A0=88=EC=9E=84=20?= =?UTF-8?q?=EC=88=98=EA=B0=80=20=EC=95=84=EB=8B=8C=20=ED=9D=90=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프레임당 고정 비율(0.35)로 좁히면 주사율에 따라 속도가 달라짐 — 120Hz 화면에서는 두 배 빨라 툭 끊겨 보이고, 프레임을 한 번 놓치면 그만큼 덜컥 뜀. 흐른 시간으로 환산해 어느 화면에서나 같은 시간에 도착하게 함. 탭이 잠들었다 깬 큰 간격은 64ms로 자름. 자체검증(공용 브라우저 임시 탭, 휠 20칸): 프레임 중앙값 16.6ms(60fps) 유지, 최대 프레임 47.6 → 34.3ms, 20ms 초과 프레임 3 → 2건. Co-Authored-By: Claude Opus 5 --- B05_Profile/B05_Profile_UI_Profile_Wheel.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Profile_Wheel.ts b/B05_Profile/B05_Profile_UI_Profile_Wheel.ts index c2e0454e..5ee726db 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Wheel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Wheel.ts @@ -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); }