Skip to content

fix(text): stop DitherText blinking out in Safari - #10

Merged
leoisadev1 merged 3 commits into
mainfrom
leoplayz317/dcn-56-dithertext-blinks-out-in-safari-while-it-animates
Oct 26, 2026
Merged

leoisadev1 merged 3 commits into
mainfrom
leoplayz317/dcn-56-dithertext-blinks-out-in-safari-while-it-animates

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Summary

In Safari, animated DitherText vanished for one frame every few updates (reported on X). Each frame is a new canvas data URL set as background-image, and the letters are transparent with background-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:

 draw(time)
   paint cells into the canvas
-  el.style.backgroundImage = url(canvas.toDataURL())
+  queued = { url, size }            // newest frame wins; older waiting ones drop
+  if no decode is running: showQueued()

+showQueued()
+  take queued; await new Image(url).decode()
+  el.style.backgroundImage = url(...)   // decoded, so Safari paints it at once
+  repeat while a newer frame is queued

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: text at 12 fps, which widens the race so it loses every time:

Before (old swap) After (decode, then swap)
Before: text, blank, text After: text in every frame
  • Before: the text disappeared on 135 of 270 recorded frames, once per update. The mean brightness of the text area fell from 48.8 to 31.0, which is the bare background.
    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.
  • The real component at /docs/text/dither-text in Chrome still renders the fill, reaches data-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 check is clean on the file. tsc reports 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 DitherText blinking in Safari by queueing decoded frames

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

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

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
dithercn Ready Ready Preview Oct 26, 2026 2:28pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge; the previous promise-growth issue is fixed.

Summary

DitherText decodes each generated image before applying it, keeping the previous frame visible while the next one loads.

  • Animated DitherText prepares each frame before swapping it into the text.

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]
Loading

Reviews (3) · Last reviewed commit: "fix(text): don't chain DitherText decode..." · Reviewed by Greptile

Comment thread components/dithercn/dither-text.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: Approved at 34b1c60

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>
Comment thread components/dithercn/dither-text.tsx Outdated
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>
@leoisadev1
leoisadev1 merged commit 235d134 into main Oct 26, 2026
7 checks passed
@leoisadev1
leoisadev1 deleted the leoplayz317/dcn-56-dithertext-blinks-out-in-safari-while-it-animates branch October 26, 2026 14:31

This branch was successfully deployed

1 active deployment
Preview — 34b1c608 Deployed Oct 26, 2026 by vercel[bot]
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