Skip to content

fix(streaming): stop flicker and make wave/blurIn distinct - #167

Merged
saminsohag merged 2 commits into
mainfrom
fix/streaming-animations
Nov 4, 2026
Merged

saminsohag merged 2 commits into
mainfrom
fix/streaming-animations

Conversation

@saminsohag

Copy link
Copy Markdown
Collaborator

Summary

Fixes the flicker seen while streaming and after a reply finishes, and makes the wave and blurIn character animations actually look different from fade.

Flicker

  • Blocks replayed their entrance when a reply went quiet. The incremental view swapped its root between a Semantics wrapper (text arriving) and the bare column (quiet, after 250 ms). The type change re-inflated the whole document, so every table, fence and formula played its block animation again (growIn collapsed and regrew), table scroll offsets reset, and SVGs blanked for a frame — also around every pause longer than 250 ms mid-stream. The wrapper now stays; only its arguments flip, and with container: false and no label it adds no semantics node, so the settled tree is unchanged.
  • Raw TeX flashed at the end of $$ blocks. When the closing $$ arrived split across chunks, the open formula was completed with a stray $ inside, failed to parse, and the error fallback drew the source for one chunk (and jumped the height 50 → 76 → 50 px in a probe).
  • A display formula dropped back to raw TeX as it closed. The inline hold scanned inside closed \(…\) / \[…\], read the _ in (z)_i as emphasis, and cut the tail mid-formula until the next paragraph began. Closed formulas are now opaque to the hold, like code spans.

Animations

  • wave — the crest uses the hue of ColorScheme.primary, made vivid and kept clearly lighter/darker than the text. It used to be a grey derived from the text colour, and Material 3's pastel dark primary alone (#BCC2FF on #E4E1E9) is still invisible.
  • blurIn — opacity now runs ahead of the blur, so words are visibly soft while readable; the blur scales with font size (0.2 em, clamped 1.5–8 px). Previously it was under 1 px by half opacity.
  • Fade window — entrances were cut off at 64 characters behind the head; the defaults (300 chars/s × 0.25 s = 75) already exceeded it. The window now grows with reveal speed, 64 → max 256.

Performance

  • Neighbouring characters at the same progress step (1/32) share one span.
  • The view rebuilds only segments with characters actually still fading (RevealEngine.fadingFrom, a binary search over the stamps) instead of a fixed window.

A/B, same session, debug VM, min of 3 runs: at defaults (300 chars/s, 0.25 s) fade/blurIn unchanged and wave ~7% faster; at 1200 chars/s fade +5%, blurIn +10% (the full entrance now plays instead of being clipped), wave unchanged. Not device measurements.

API

Additive only — no breaking changes:

  • revealStyleFor(..., {Color? accent, double? fontSize})
  • applyReveal(..., {Color? accent, double? fontSize})
  • RevealEngine.window, RevealEngine.fadingFrom, RevealEngine.maxFadeWindow

Without accent, revealStyleFor's wave is exactly the old colour (tested). fadeWindow stays at 64, now the starting size.

Also: a local demo/ folder is git-ignored and excluded from the root analyzer.

Tests

New regression tests, each confirmed to fail without its fix:

  • reveal_regressions_test.dart — a finished block does not enter again (with isStreaming left on and off)
  • math_streaming_test.dart — half a $$ closer never paints raw source; a closed formula is not held for the markup inside it
  • reveal_effect_test.dart (new) — wave lands in the accent; crest stands out with a pastel dark accent; blurIn soft while visible and scaled by font size
  • reveal_engine_test.dart — window grows / keeps stamps / is capped; fadingFrom is exact

Three existing span-shape / engine tests were updated where they pinned the fixed 64 window or used uniform progress (which now correctly merges into one span).

flutter test: 2041 passed, 39 skipped. flutter analyze: clean. Changelog entries are under ## Unreleased.

A scratch Flutter app for screen recordings lives in demo/. It is
git-ignored (so pub publish skips it too) and excluded from the root
analyzer, which would otherwise analyse it against the package's config.
Flicker:
- The incremental view swapped its root between a Semantics wrapper and
  the bare column when a reply went quiet. The type change re-inflated
  the document, so every block entrance replayed (tables faded in again,
  growIn blocks collapsed and regrew) and SVGs blanked for a frame. The
  wrapper now stays; only its arguments flip.
- A $$ block whose closer arrived split ($ then $) completed with a stray
  $ inside, failed to parse, and painted raw TeX for one chunk.
- The inline hold scanned inside closed \(…\) / \[…\] formulas, read
  the _ in (z)_i as emphasis, and cut the tail mid-formula, so it fell
  back to raw source as it closed. Closed formulas are now opaque.

Animations:
- wave's crest uses the hue of ColorScheme.primary, made vivid and kept
  clear of the text's lightness; it was a grey indistinguishable from fade.
- blurIn keeps its blur while the text is visible and scales it with the
  font size; the blur used to finish before the text could be seen.
- The fade window grows with reveal speed (64 up to 256) instead of
  cutting entrances short at 64 characters, which the defaults exceeded.
  Same-step neighbours share one span and only characters still fading
  are rebuilt, so the default settings cost no more per frame than before.

Additive API only: optional accent/fontSize on revealStyleFor and
applyReveal; RevealEngine.window, fadingFrom and maxFadeWindow.
@saminsohag
saminsohag merged commit 32f77f0 into main Nov 4, 2026
2 checks passed
@saminsohag
saminsohag deleted the fix/streaming-animations branch November 4, 2026 11:54
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