* fix(tooltip): remove velocity skew/scale that blurred text on every appear
The tooltip animated a fractional scale() + skew() over 150ms on the element
containing its text. Chrome promotes the bubble to a compositor layer for the
transition, rasterizes the text once at the pre-transition scale, then
GPU-resamples that bitmap for the duration — so text rendered blurry until the
transition settled and the layer re-rasterized at 1:1.
It fired on every appear: a pointer entering a trigger is by definition moving,
so the first pointermove after pointerenter always set a non-zero skew and a
fractional scale.
- drop the velocity-reactive skew/scale flourish and the pointer-velocity
bookkeeping that existed only to feed it
- round tooltip position to whole pixels; clientX/clientY are fractional on
HiDPI/zoomed displays, leaving the bubble on a subpixel boundary
- drop the dead `filter` from the transition list — nothing ever set a filter
- skip the state update when the rounded position is unchanged, so pointer
jitter no longer re-renders every Tooltip.Trigger/Content consumer
The 150ms ease-out translate is kept, so the bubble still trails the cursor.
* improvement(tooltip): keep the velocity flourish, drive it without a CSS transition
Restores the velocity-reactive skew/scale removed in the previous commit. The
flourish was never the problem on its own — handing it to a CSS transition was.
An interpolated fractional scale makes the compositor rasterize the tooltip's
text once and resample that bitmap for the duration, which is what read as blur.
Applied as a static value per pointer event instead, so every frame is
rasterized at its own scale:
- split the transform across the individual `translate`, `scale`, and
`transform: skew()` properties, and transition only `translate` — position
still eases toward the cursor, the flourish no longer interpolates
- smooth the pointer velocity in JS (low-pass filter) to replace the smoothing
the CSS transition used to provide, so the squish still ramps rather than
snapping between raw per-event velocities
- quantize the flourish to 3 decimals so jitter below the visible threshold
settles instead of re-rendering every consumer
Whole-pixel position rounding and the redundant-update bail-out are unchanged.
* fix(tooltip): don't seed pointer velocity from the trigger box on focus
The previous commit routed `onFocus` through a shared reveal helper that seeds
`lastPointerRef` from the coordinates it is given. For focus those are the
trigger's box center, not the pointer — so if the pointer already happened to be
over the trigger, the next `pointermove` measured the box-to-cursor delta as
velocity and spiked the skew/scale flourish.
Split the helper in two: reveal-from-pointer seeds velocity tracking,
reveal-from-element leaves it cleared. Restores the pre-PR behavior, where focus
explicitly nulled the pointer snapshot.
Caught by Cursor Bugbot.
* 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