Repository navigation
feat: add a gradient streaming animation and animation options - #1156
Closed
hryhoriiK97 wants to merge 11 commits into
Closed
hryhoriiK97 wants to merge 11 commits into
hryhoriiK97 wants to merge 11 commits into
Conversation
5 of 6 tasks
hryhoriiK97
force-pushed
the
feat/streaming-effects
branch
from
November 11, 2026 08:36
e520286 to
ccf3d9f
Compare
hryhoriiK97
force-pushed
the
feat/streaming-effects
branch
from
November 11, 2026 09:42
ccf3d9f to
68e4e23
Compare
This was referenced Nov 11, 2026
hryhoriiK97
force-pushed
the
feat/streaming-effects
branch
from
November 11, 2026 19:28
68e4e23 to
6ba9d29
Compare
`streamingConfig.animation` takes 'fade', 'gradient', 'none', or an
object with options: { type: 'fade', duration } and { type: 'gradient',
length, startColor, endColor } (#904). A type this version does not know
shows new content at once, with a warning in development.
How each new character looks is worked out in the shared core
(StreamingEffects): a fade from each chunk's own arrival time, so chunks
arriving every frame fade in fully instead of snapping, and a gradient
from each character's distance from the end of the text. The newest
character takes `endColor`, and the gradient settles to the text's own
colors over 200 ms when the stream ends. The last chunk, arriving with
the end of the stream, still animates before settling.
The hosts only draw: iOS draws the animated glyphs in the effect's
colors from the text tiles, with a display link only while the looks
change over time; Android applies one span per run of equal looks, read
in one JNI call per frame. A character's own alpha is kept, so hidden
spoiler text stays hidden, and the text itself never changes, so
selection, copy and accessibility are unaffected. This replaces
ENRMTailFadeInAnimator, TailFadeInAnimator and FadeInSpan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
streamingConfig.animation documents 'gradient' and the object forms with their options, and the streaming guide gains an Animations section on how fade and gradient behave and how to match a brand's colors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as no effect lookAt divided by the duration unchecked; configure now turns such a config into None, the one place the effects' options are checked. Adds tests for configure starting over, a gradient between two opaque colors and a tinted gradient settling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d test them The run computation (grapheme boundaries, runs of equal look) becomes StreamingEffectRuns, which keeps one BreakIterator instead of making one per frame. It uses android.icu's, which java.text's wraps on Android, so the JVM tests see the same grapheme rules: a surrogate pair and a ZWJ sequence the range starts inside, and plain runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nativeLooks filled three std::vectors each frame and copied them over; lookAt makes no JNI calls, so it now writes through critical array pointers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text effects read Reduce Motion once, when configured, so toggling it while a view was mounted changed the block fades but not the text. They now check it on each arrival, as the tail fade did, and start over when it changed. Android's StreamingEffects.configure takes the Config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishing a fade does nothing, so the hosts settle only gradients. With 'none' or Reduce Motion, Android no longer makes an animator and a core object for each streamed text segment. The block fade follows the parsed animation type on both platforms instead of a flag or the raw string, Android's config defaults live in one place, and a comment repeated in four files stays in one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Segment views the reconciler removed kept their core object until the deprecated finalize ran, as textEffects holds them weakly. onViewRemoved now releases them, so finalize goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e same processColor makes a new object for a PlatformColor each render, so the memoised streamingConfig was rebuilt every time. useStreamingAnimation memoises on the raw type, duration, length and colors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocks and table rows keep a fixed 200 ms fade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… effects left stale docs-md/MARKDOWN_STREAMING.md listed only 'fade' and 'none'; it now has 'gradient' and the preset options, as the guide and API reference do. The slot reveal's ease was still called the tail fade's, which this PR removed, and the web guide called streamingConfig block handling only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hryhoriiK97
force-pushed
the
feat/streaming-effects
branch
from
November 11, 2026 19:50
6ba9d29 to
169981d
Compare
6 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What/Why?
streamingConfig.animationhad two values,'fade'and'none'. #904 asks for a typewriter-style gradient in the app's own colors, and for chunks that arrive quickly not to snap in: each update cancelled the running fade and showed the previous chunk at once.This is the seventh PR of the streaming stack, stacked on #1151. It turns
animationinto a set of presets that can each take options, adds the'gradient'preset, and moves the effect math into the shared core.The streaming stack
streamingandstreamingConfig.animation.A later PR can add custom components per chunk (
renderers.chunk) on the same inputs.How
The API:
animationtakes a preset name or a preset object.'fade'(default) or{ type: 'fade', duration }: each chunk fades in overdurationms (200).'gradient'or{ type: 'gradient', length, startColor, endColor }: the lastlengthcharacters (20) run fromstartColortoendColor, following the end of the text. WithoutstartColor, each character keeps its own color (links, code, bold).endColordefaults to transparent and is the newest character's color.'none': new content appears at once, as does adurationorlengthof 0.A type this version doesn't know shows content at once, with a warning in development, so an app can use a newer preset without breaking on an older version. Adding a preset means one case in the core and one name in JS. Colors nested in the prop go through
processColorin JS.The look of each character is worked out in the shared core (
StreamingEffects, next toStreamingSession). It gives each character a tint, a tint amount and an opacity:The hosts only draw, and never change the text, so selection, copy and accessibility see it as it is:
ENRMTailFadeInAnimator,TailFadeInAnimatorandFadeInSpan, which faded by rewriting the text's colors.Edge cases:
githubflavor, only the text segment at the end of the document keeps a gradient; segments the end has left settle.Testing
Unit tests:
StreamingEffectsTests(doctest): fade timing and overlapping chunks, the gradient's position and colors, settling, and a render that adds nothing keeping a settled gradient settled.ENRMStreamingEffectsTests: colors from the core, hidden text staying hidden;ENRMTextViewportTests: an effect redraws the tile holding the end.StreamingEffectSpanTest: tint and opacity, hidden text, the underline color.streaming-animation.test.ts: normalizing presets and options, unknown types, zero duration and length.CPU on the iPhone 15 Pro (Release build), with the scratch chat screen from #1151: one answer streaming in through a stream, 60 characters per 16 ms. The table shows the main thread against no animation, averaged over 2 runs.
Galaxy A16 (Android 15): the main thread rose by 2–4 points with fade and 0–3 points with gradient, with 0.05% jank in every case and unchanged stream times. These runs were before the last review fixes, which only touch how Android spans split around characters.
Visual checks on the iPhone and on a Pixel 9 emulator (Android 16),
githubflavor, with #904's colors:'none'mid-stream and back applies at once to the text on screen;Docs
streamingConfig.animationdocuments the presets and their options.docs-md/API_REFERENCE.mdandllms.txtare updated;check-examplesandcheck-llmspass.Limits
Changes after review
StreamingEffectRuns, ICU grapheme boundaries), the looks are written into the Java arrays in place, and a dropped text segment frees its effects (no finalizer).'none'makes no text effects.durationapplies to text only, and docs-md lists'gradient'and its options.Checked on the whole stack after these changes: core suite and fuzzing, Jest (1,838), Android unit tests (134), iOS unit tests (118), ktlint; streams on an iPhone 15 Pro and a Galaxy A16 call
onStreamingEndonce and end at the static height, with CPU level or lower; the 106 Maestro flows give the same results and screenshots as before.PR Checklist
🤖 Generated with Claude Code