Skip to content

Commit bc56dfd

Browse files
committed
fix(tooltip): make the flourish smoothing frame-rate independent
The velocity low-pass filter applied a fixed coefficient per pointer event, so how fast the squish settled depended on how fast the device emitted events — 233ms at 30Hz down to 29ms at 240Hz, an 8x spread for the same gesture. It was also far snappier than the 150ms CSS ease-out it replaced, so the flourish read as twitchier than before. Derive the coefficient from the real elapsed time instead (1 - exp(-dt / tau), tau = 50ms). Settling is now flat at ~150ms from 60Hz upward, matching the duration of the transition this stands in for. Also separates the smoothing delta from the velocity-normalization delta: the latter is still floored at one frame to keep a 1ms event from reporting an enormous velocity, but flooring the former was itself a source of frame-rate dependence below 16ms. Verified against Chrome's documented re-raster behavior: a layer is re-rastered at its new scale when the scale changes via script, but not when a declarative animation interpolates it, which is why the flourish must stay out of the transition list. https://developer.chrome.com/blog/re-rastering-composite
1 parent 82c28e8 commit bc56dfd

1 file changed

Lines changed: 23 additions & 13 deletions

File tree

packages/emcn/src/components/tooltip/tooltip.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ const EDGE_THRESHOLD = 360
1111
const MIN_FRAME_MS = 16
1212

1313
/**
14-
* How much of the gap between the smoothed and the instantaneous pointer velocity
15-
* is closed per pointer event. This is what softens the velocity flourish — the
16-
* transform itself is never handed to a CSS transition, because a compositor-
17-
* interpolated fractional scale forces the tooltip's rasterized text to be
18-
* resampled, which reads as a blur until the interpolation settles.
14+
* Exponential time constant for smoothing the pointer velocity that drives the
15+
* flourish, in ms. The flourish is deliberately never handed to a CSS transition:
16+
* Chrome only re-rasters a layer at its new scale when the scale changes via
17+
* script, not when a declarative animation interpolates it, so a transitioned
18+
* fractional scale leaves the tooltip's text resampled from a stale bitmap until
19+
* the animation settles — which is what read as a blur on every appear.
20+
*
21+
* Smoothing here replaces the smoothing that transition used to provide. ~3x the
22+
* time constant is where the value has effectively settled, so 50ms reproduces
23+
* the feel of the 150ms ease-out it stands in for.
1924
*/
20-
const VELOCITY_SMOOTHING = 0.35
25+
const VELOCITY_TIME_CONSTANT_MS = 50
2126

2227
/**
2328
* Resolved position and motion of a floating tooltip. `x`/`y` are whole-pixel
@@ -146,14 +151,19 @@ export function useFloatingTooltip(canShow: (target: HTMLElement) => boolean): {
146151
if (!canShowRef.current(event.currentTarget)) return
147152
const now = performance.now()
148153
const previous = lastPointerRef.current
149-
const elapsed = previous ? Math.max(now - previous.time, MIN_FRAME_MS) : MIN_FRAME_MS
150-
const instantX = previous ? ((event.clientX - previous.x) / elapsed) * MIN_FRAME_MS : 0
151-
const instantY = previous ? ((event.clientY - previous.y) / elapsed) * MIN_FRAME_MS : 0
152-
154+
const delta = previous ? Math.max(now - previous.time, 1) : MIN_FRAME_MS
155+
const perFrame = Math.max(delta, MIN_FRAME_MS)
156+
const instantX = previous ? ((event.clientX - previous.x) / perFrame) * MIN_FRAME_MS : 0
157+
const instantY = previous ? ((event.clientY - previous.y) / perFrame) * MIN_FRAME_MS : 0
158+
159+
/**
160+
* Derived from the real elapsed time rather than applied per event, so a
161+
* 120Hz pointer and a 60Hz one settle over the same wall-clock duration.
162+
*/
163+
const smoothing = 1 - Math.exp(-delta / VELOCITY_TIME_CONSTANT_MS)
153164
const velocity = velocityRef.current
154-
velocity.x += (instantX - velocity.x) * VELOCITY_SMOOTHING
155-
velocity.magnitude +=
156-
(Math.hypot(instantX, instantY) - velocity.magnitude) * VELOCITY_SMOOTHING
165+
velocity.x += (instantX - velocity.x) * smoothing
166+
velocity.magnitude += (Math.hypot(instantX, instantY) - velocity.magnitude) * smoothing
157167

158168
lastPointerRef.current = { x: event.clientX, y: event.clientY, time: now }
159169
apply(event.clientX, event.clientY, {

0 commit comments

Comments
 (0)