Skip to content

feat(chat): blur and fade the transcript at the top edge - #1428

Merged
bmc08gt merged 6 commits into
code/cashfrom
feat/chat-top-edge-blur
Sep 8, 2026
Merged

feat(chat): blur and fade the transcript at the top edge#1428
bmc08gt merged 6 commits into
code/cashfrom
feat/chat-top-edge-blur

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

The chat bar drew a two-stop fade that was already ~70% transparent where the status bar sits, so a cash bubble scrolling under it showed through behind the clock and cut at a hard line at the bar's edge.

The top edge now works the way iOS's does: the transcript passing under the bar is blurred, and a gradient over that takes it to the background colour. Neither half stands alone. iOS's fade is weak — alpha near a third behind the title, clear before the bar's bottom edge — and only reads because topEdgeEffect.style = .soft blurs underneath; without that blur the fade has to be strong enough to hide content outright, and without the fade a cash card's amount stays legible over the title.

Compose 1.12 has no backdrop API, so the blur is a RenderEffect chain: a blurred copy of the transcript and the sharp original, each masked against complementary gradients and added together, so every pixel is weighted between the two exactly once. Masking both halves is the part that matters. Compositing a blurred copy over an untouched original does not replace it — the transcript's background is transparent, and so is the spread of a blurred glyph — so the original showed through its own blurred copy, and white text on a dark ground came back as a crisp glyph wearing a halo.

Shape and distance, which took the most iteration:

  • The blur holds full strength across the bar, then eases off over 64dp starting 16dp above the bar's bottom edge. A mask that ramps from the very top is two-thirds spent by the time it reaches the title, which is where the collision was; one that holds to the bar's bottom edge starts its falloff exactly where a bubble emerges, and the two coincide into a single hard boundary.
  • Both ramps sample a smoothstep rather than interpolating linearly. A straight line turns a corner at each end, and those corners read as the edges of a band however long the ramp between them is.
  • The fade runs 36dp past the bar and the blur radius is 28dp. iOS's literal numbers left an amount readable over the title.
  • The blurred copy's brightness is scaled to 0.6. A blur spreads a pixel's light over its radius, so it hands back more lit area than it was given; that is a different correction from the fade, which mixes towards the background evenly rather than only where the blur lit up.

The fade is drawn, not laid out. Reaching past the bar as a sibling would mean being taller than it, and the scaffold turns the top bar slot's height into the transcript's content padding — a purely visual tail would have pushed every message down.

RenderEffect needs API 31. Below that the fade stands alone, which still keeps an amount out of the status bar.

The bar reports its measured height up so the transcript can size the blur to it, since the selection and editing modes change that height. The transcript keeps its hazeSource — the bottom bar still blurs through it.

Scroll cost is nothing measurable: p50 18ms and p90 32ms with the blur, against 18ms and 32ms without it on the same transcript.

The chat bar drew a two-stop fade that was already ~70% transparent where
the status bar sits, so a cash bubble scrolling under it showed through
behind the clock with a hard cut at the bar's edge.

Match iOS, which does this in two parts for a reason it documents: the soft
scroll-edge effect blurs what passes under the bar but does not darken it,
and a cash card's amount is large white text that still reads over the title
once blurred. So the bar now applies a progressive `hazeBlur` (intensity 1 at
the status bar, 0 by the tail's end) under a background gradient that holds
fully opaque to `statusBars - 12.dp` before fading out — the same hold iOS
uses for `TranscriptTopFade`.

The blur style carries no tint of its own; the gradient painted over it is
the whole of the darkening, and a material's wash on top would double the
scrim where the two overlap.
@bmc08gt bmc08gt self-assigned this Sep 8, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 8, 2026
Drop the `hazeBlur` from the top edge and let the gradient carry it. The
transcript keeps its `hazeSource` for the bottom bar, so this removes the
second blur pass over a scrolling list rather than the dependency.

Compose 1.12.0 has no first-party backdrop API — there is no `Backdrop` type
in `ui`, `ui-graphics`, `foundation` or `material3` — so the alternative to
Haze was hand-rolling the same record-and-replay against `RenderEffect`,
which is API 31+ and would fall back to this gradient on minSdk 29 anyway.

Without the blur the fade has to do all of the hiding, so its stops are keyed
to layout landmarks rather than to fractions of the region: opaque to
`statusBars - 12.dp`, still at 0.9 alpha where the bar's own bottom edge is,
and clear by the end of a 40dp tail. Keying the middle stop to the measured
bar height means the taller selection and editing bars darken over their whole
height instead of running out partway down, and spending the whole ramp on the
tail keeps its end from reading as a band across whatever bubble it lands on.
@bmc08gt bmc08gt changed the title feat(chat): blur and fade the transcript at the top edge feat(chat): fade the transcript at the top edge Sep 8, 2026
The fade-only version had to be strong enough to hide content outright, because
a cash card's amount is large white text that stays legible through anything
short of an opaque scrim. iOS keeps its fade weak — alpha lands near a third
behind the title and is spent before the bar's bottom edge — and gets away with
it because `topEdgeEffect.style = .soft` blurs what scrolls underneath.

So take iOS's numbers for the fade and supply the blur separately. Compose 1.12
has no backdrop API, so the blur is a `RenderEffect` chain: a blurred copy of
the transcript masked to the top of it and composited over the sharp original.
The mask is opaque across the bar's measured height and falls off over a 24dp
tail, matching how the iOS effect covers its region evenly and softens only at
the boundary — a mask that ramps from the very top instead leaves the title's
depth barely blurred, which is where the collision was in the first place.

`RenderEffect` needs API 31; below that the fade stands alone, which still keeps
an amount out of the status bar.

The bar reports its measured height up so the transcript can size the blur to
it, since selection and editing modes change that height.

Scroll cost is nothing measurable: p50 18ms and p90 32ms with the blur against
18ms and 32ms without it, on the same transcript.
iOS's fade is spent just short of the bar's bottom edge, which leaves alpha near
a third behind the title. That is enough against ordinary bubbles and not enough
against a cash card, whose amount is large white text: it stayed legible over the
title. Run the ramp 16dp past the bar instead, which puts roughly half alpha at
the title, and take the blur from 20dp to 28dp so the digits stop resolving.

The fade is drawn rather than laid out now. As a sibling it would have to be
taller than the bar to reach past it, and the scaffold turns the top bar slot's
height into the transcript's content padding, so a purely visual tail would have
pushed every message down.
@bmc08gt
bmc08gt force-pushed the feat/chat-top-edge-blur branch from 39fe879 to d67f6ef Compare September 8, 2026 17:32
@bmc08gt bmc08gt changed the title feat(chat): fade the transcript at the top edge feat(chat): blur and fade the transcript at the top edge Sep 8, 2026
The blurred copy was composited straight over the untouched layer, which does not
replace it: the transcript's background is transparent, and so is the spread of a
blurred glyph, so the sharp original showed through its own blurred copy. White
text on a dark ground came back as a crisp glyph wearing a halo.

Mask both halves against complementary gradients instead, so the two cover every
pixel once — blurred under the bar, sharp below the tail, a crossfade between.

Scale the blurred copy's brightness to 0.6 while there. A blur spreads a pixel's
light over its radius, so even without the doubling it hands back more lit area
than it was given; taking the light out is a different correction from the fade,
which mixes towards the background evenly rather than only where the blur lit up.
Both ramps were straight lines, and a straight line turns a corner at each end —
full strength one pixel, already dropping the next. Those corners are what the
eye reads as the edges of a band, however long the ramp between them is. Sample
a smoothstep instead, so each ramp leaves and arrives flat.

Lengthen them too: the blur's falloff goes from 24dp to 64dp and the fade's tail
from 16dp to 36dp. This is the distance over which a bubble goes from unreadable
to readable and the eye follows it the whole way, so a short one delivers that
change of state as an event rather than as a transition.

Start the blur easing 16dp above the bar's bottom edge rather than at it. Held to
the edge, the falloff begins exactly where a bubble emerges from behind the bar,
and the two coincide into a single hard boundary.

Combine the two masked copies with PLUS rather than SRC_OVER. They are exact
complements, so adding them weights each pixel between sharp and blurred once;
compositing one over the other put the lower through its own mask a second time,
which dipped the middle of the transition darker.
@bmc08gt
bmc08gt merged commit fb91d09 into code/cash Sep 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant