fix(tooltip): stop text blurring for ~150ms every time a tooltip appears - #6211
Conversation
…ppear 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryLow Risk Overview Motion model: Pointer velocity is low-pass filtered in JS ( Rendering: Reviewed by Cursor Bugbot for commit bc56dfd. Configure here. |
Greptile SummaryThe PR changes tooltip motion rendering to avoid transient text blurring while retaining cursor-following movement.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/emcn/src/components/tooltip/tooltip.tsx | Refactors tooltip positioning and velocity-driven effects to prevent compositor-resampled text while reducing redundant pointer-move renders. |
Reviews (4): Last reviewed commit: "fix(tooltip): make the flourish smoothin..." | Re-trigger Greptile
…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.
|
@cursor review |
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.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 82c28e8. Configure here.
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
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit bc56dfd. Configure here.
Summary
scale()+skew()to a 150ms CSS transition. Chrome promotes it to a compositor layer, rasterizes the text once at the pre-transition scale, then resamples that bitmap for the duration — text stays soft until the transition ends and the layer re-rasterizes at 1:1pointermoveafterpointerenteralways set a non-zero skew and a fractional scaletranslate,scale, andtransform: skew()properties, and transition onlytranslate. Position still eases toward the cursor; the flourish is a static value on every frame, so each frame rasterizes at its own scaleclientX/clientYare fractional on HiDPI/zoomed displays, parking the bubble on a subpixel boundaryfilterfrom the transition list; nothing ever set a filter (leftover from an older blur-in animation)Tooltip.Trigger/Contentconsumer on each mouse moveNet effect: same cursor-following trail, same velocity squish, without the compositor resampling the text.
Type of Change
Testing
tscclean onapps/sim,packages/emcn,packages/workflow-renderer,apps/desktop,apps/realtime. Existing tests that render tooltip consumers pass (19/19). No tests or stories cover the tooltip directly.Still needs a visual check on real GPU compositing — the artifact can't be captured headlessly, since screenshot capture forces the re-rasterization that makes the transient state look crisp by construction.
Checklist