Repository navigation
fix(text): stop DitherText blinking out in Safari - #10
leoisadev1 merged 3 commits into
Conversation
DitherText swaps a new canvas data URL into background-image on every frame, and its text is transparent with background-clip: text. Safari loads the new image asynchronously and can paint a frame without it, so the whole headline vanished for a frame every few updates. Decode each frame off-screen with Image.decode() and swap it in once it is ready, keeping the shown image referenced. Frames apply in order, and a slow decode never stalls the animation. Fixes DCN-56 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
[Low impact] The PR appears safe to merge; the previous promise-growth issue is fixed. Summary
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Draw frame] --> B[Replace newest waiting frame]
B --> C{Decode already running?}
C -->|Yes| D[Keep current frame visible]
C -->|No| E[Take waiting frame]
E --> F[Decode image]
F --> G[Apply image and size]
G --> H{Another frame waiting?}
H -->|Yes| E
H -->|No| I[Clear busy flag]
Reviews (3) · Last reviewed commit: "fix(text): don't chain DitherText decode..." · Reviewed by Greptile |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a contained fix to DitherText’s Safari frame-swapping behavior, adding off-screen decoding and bounded frame queueing within a single component. It preserves existing functionality while preventing visible blinking and introduces no broader API or infrastructure changes. You can add or adjust custom eligibility rules. Learn more. |
Each frame started its own decode, so on a device that decodes slower than the 12 fps loop, pending images and data URLs could pile up. Decode one frame at a time and keep only the newest waiting frame, so the animation still lands the latest frame and resizes stay correct. Refs DCN-56 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Awaiting the recursive showQueued call kept every earlier call pending while decoding stayed slower than the frame loop. Start the next decode without awaiting it, so each call settles once it hands off. Refs DCN-56 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
In Safari, animated
DitherTextvanished for one frame every few updates (reported on X). Each frame is a new canvas data URL set asbackground-image, and the letters are transparent withbackground-clip: text. Safari loads the new image asynchronously and can paint before it is ready, which leaves the text with no ink. Chrome swaps synchronously, so it never showed there.The component now decodes each frame off-screen and swaps it in only once it's ready:
The previous frame stays up until the next one is ready. Only one decode runs at a time, plus the newest waiting frame, so a device that decodes slower than the 12 fps loop skips frames instead of piling up images or promises. The newest frame always lands, so a resize during a decode still ends at the right size.
Evidence
Three consecutive frames from an iOS Simulator Safari (iOS 27) screen recording. The test page swaps a large canvas data URL under
background-clip: textat 12 fps, which widens the race so it loses every time:After: 0 of 133 frames dropped out with the shipped queue (0 of 134 with the first, unbounded version). The old code rerun beside it blanked 135 of 269.
/docs/text/dither-textin Chrome still renders the fill, reachesdata-state=ready, and swapped in a new frame on each of 20 samples taken 100 ms apart. At 390 px wide the background resizes to the new 294×162 box. No console errors.ultracite checkis clean on the file.tscreports no errors in it.Not covered: desktop Safari, where the bug was reported. At its real size the image doesn't lose the race on the fast Mac Mini, so the unmodified component showed 0 blanks before and after in Simulator Safari. Playwright WebKit didn't reproduce it either.
Merge Danger
Door: two-way. Reverting the commit restores the old swap.
Blast radius: every animated
DitherText. A failed decode still swaps its frame in, so the fill can't freeze on an error.Fixes DCN-56
Created with Claude Opus 5.5 in Claude Code.
Note
Fix
DitherTextblinking in Safari by queueing decoded framesAdds a frame swapper to dither-text.tsx. The draw callback now queues each canvas-generated frame instead of setting the background image directly. The swapper decodes one frame at a time via an image element, applies it, and keeps only the newest queued frame while a decode is active. Frames are still applied when decoding fails.
Macroscope summarized 34b1c60.