Skip to content

fix(tooltip): stop text blurring for ~150ms every time a tooltip appears - #6211

Merged
waleedlatif1 merged 4 commits into
stagingfrom
worktree-tooltip-blur-fix
Aug 3, 2026
Merged

fix(tooltip): stop text blurring for ~150ms every time a tooltip appears#6211
waleedlatif1 merged 4 commits into
stagingfrom
worktree-tooltip-blur-fix

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Every tooltip rendered blurry for ~150ms on appear, then snapped crisp
  • Cause: the bubble handed a fractional 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:1
  • It fired on every single 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
  • The flourish itself was never the problem — letting the compositor interpolate it was. So it's kept, just driven differently:
    • Split the transform across the individual translate, scale, and transform: skew() properties, and transition only translate. Position still eases toward the cursor; the flourish is a static value on every frame, so each frame rasterizes at its own scale
    • Smooth the pointer velocity in JS (low-pass filter) to replace the smoothing the CSS transition used to provide, so the squish still ramps instead of snapping between raw per-event velocities
    • Quantize the flourish to 3 decimals so sub-visible jitter settles instead of re-rendering every consumer
  • Round tooltip position to whole pixels — clientX/clientY are fractional on HiDPI/zoomed displays, parking the bubble on a subpixel boundary
  • Drop the dead filter from the transition list; nothing ever set a filter (leftover from an older blur-in animation)
  • Skip the state update when nothing resolved differently, so pointer jitter no longer re-renders every Tooltip.Trigger/Content consumer on each mouse move

Net effect: same cursor-following trail, same velocity squish, without the compositor resampling the text.

Type of Change

  • Bug fix

Testing

tsc clean on apps/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

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…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.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 3, 2026 6:31pm

Request Review

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
UI-only tooltip rendering and pointer-motion logic in emcn; no auth, data, or API surface changes beyond internal tooltip behavior.

Overview
Fixes ~150ms blurry tooltip text on every appear caused by Chrome compositor resampling when fractional scale/skew were animated via CSS transition on transform.

Motion model: Pointer velocity is low-pass filtered in JS (VELOCITY_TIME_CONSTANT_MS) so the velocity squish still ramps smoothly without compositor-interpolated scale. Flourish values are quantized to cut sub-pixel jitter re-renders; state updates are skipped when position/motion are unchanged.

Rendering: FloatingTooltip now uses translate + scale + transform: skew() with only translate transitioned (drops dead filter from the transition list). Tooltip anchor x/y are rounded to whole pixels. Focus reveal uses showFromElement so velocity is not seeded from the element box (avoids flourish spikes on the next pointermove).

Reviewed by Cursor Bugbot for commit bc56dfd. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR changes tooltip motion rendering to avoid transient text blurring while retaining cursor-following movement.

  • Separates translation, scale, and skew so only tooltip position is CSS-transitioned.
  • Adds time-based pointer-velocity smoothing and quantization.
  • Rounds tooltip coordinates and skips unchanged state updates.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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.
@waleedlatif1 waleedlatif1 changed the title fix(tooltip): remove velocity skew/scale that blurred text on every appear fix(tooltip): stop text blurring for ~150ms every time a tooltip appears Aug 3, 2026
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread packages/emcn/src/components/tooltip/tooltip.tsx Outdated
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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1
waleedlatif1 merged commit 806ea0c into staging Aug 3, 2026
27 checks passed
@waleedlatif1
waleedlatif1 deleted the worktree-tooltip-blur-fix branch August 3, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant