just_liquid_glass 0.3.0
just_liquid_glass: ^0.3.0 copied to clipboard
liquid glass for flutter with fewer bugs and support for a wider variety of sdf shapes
just_liquid_glass #
A form of liquid glass for Flutter: blobby, SDF-merged glass shapes with refraction, blur, tint and shine, plus a flat fallback mode that runs on every backend.
It's entirely vibecoded (Fable 5), but it's been tested and iterated and refined and used, and is probably very stable, given the defensive approach we took. Fable was instructed to learn from other flutter libraries and take an approach that dodges some of flutter's bugs, a standout decision resulting from that was to avoid using intermediate textures so animating blobs wouldn't churn GPU memory. The full list is in Bugs dodged below.
We do aspire to look like apple's implementation by default, we currently aren't there, but we're not far.
Improvements over flutter_liquid_glass:
-
It's possible to fade shine to 0/to interpolate all the way to flatness
-
The blob shapes are quite flexible, they can each have corner radius, a hole, start and end angle, and different colors and opacities (when blobs touch, colors blend smoothly from one to the other).
-
Blobs can be animated out by shrinking (see "Animating a blob out" in
GlassBlob's docs) without crashing.flutter_liquid_glassthrows when a shape's layout goes to zero (whynotmake-it#149, open). -
Shapes can be rotated
-
The blobs of a layer implicitly form a clip mask on the child. This turns out to often be the right thing for animated entry and edge shaping.
Non-improvements:
-
Doesn't support arbitrary shapes, your shapes must compose from our round-capped segments of roundrect-toruses.
-
Arbitrarily, the number of blobs supported per layer is currently 16.
Flaws that anyone could fix instantly if they wanted to:
-
No chromatic aberration. (it could probably be done in just one prompt, mako just didn't want it (it's not actually good!), but would accept it, even as the default setting, if someone else wants to add it)
-
Our flat fallback doesn't support the blur. Fixing this would be easy. The reason it isn't in today is that mako kinda firmly recommends using fully opaque blobs on platforms that don't have full glass. Glass is a good way of adding an outline to an otherwise quite indistinct graphical effect. Without that, you probably shouldn't use transparent-blurred substances this much.
Everything below this line was written by Fable but has been reviewed.
Usage #
GlassLayer(
options: const GlassOptions(
blendRadius: 32, // how far apart blobs start fusing
blurRadius: 6,
bevelThickness: 16,
refractionIntensity: 24,
shineIntensity: 0.4,
edgeTint: Color(0x26000000), // rim darkening; keeps white-on-white legible
),
blobs: [
// A circle.
const GlassBlob(
center: Offset(120, 160),
radii: Size(70, 70),
tint: Color(0x33FFFFFF),
),
// A rotated pill.
const GlassBlob(
center: Offset(240, 200),
radii: Size(90, 36),
rotation: 0.4,
tint: Color(0x2200E5FF),
),
// A progress-ring arc segment: ring via holeRadius, sweep via angles.
GlassBlob(
center: const Offset(180, 320),
radii: const Size(64, 64),
holeRadius: 40,
startAngle: -math.pi / 2,
endAngle: -math.pi / 2 + math.pi,
tint: const Color(0x66B388FF),
),
],
child: yourContent,
)
Call GlassLayer.precache() early (e.g. in main) if you want the first
frame to include the glass; otherwise the layer renders its child alone
until the shader programs finish loading.
Shape model #
Each GlassBlob is a rounded box with half-extents radii, rotated by
rotation around center:
cornerRadius— corner rounding. The default (infinity) clamps to the smaller radius, giving a circle or stadium/pill.0gives sharp corners.holeRadius— cuts a circular hole around the center (ring/annulus). Default (-infinity) means no hole.startAngle/endAngle— clips to an angular sector in the blob's local frame (radians from the local +x axis, positive toward +y / clockwise on screen). A sweep oftauor more disables the clip. When the blob is a circular ring segment (circular radii, default corner rounding, and a hole), the open ends get circular caps automatically; other sector shapes get a hard cut.tint— the blob color. In glass mode it is mixed over the refracted backdrop with strengthtint.a; in flat mode it is the fill itself.
Modes and platform support #
| Mode | What you get | Where it works |
|---|---|---|
GlassMode.glass (default) |
Refraction, blur, tint, shine over the backdrop, falling back to flat where unsupported | Everywhere; true glass on Impeller (iOS, Android, macOS) |
GlassMode.flat |
The same merged blobby silhouette as a plain tint fill | Everywhere (Skia and Impeller, including web and desktop) |
Requesting GlassMode.glass on a backend without support falls back to flat
rather than throwing. Glass mode uses BackdropFilter + ImageFilter.shader,
which Flutter only supports on Impeller. Flat mode is an ordinary canvas shader and needs no
backdrop access, so it runs on any backend — it is a deliberate design style
of its own (blobby flat color, possibly translucent), not just a degraded
glass.
Backdrop blur is the engine's own downsampled gaussian, composed under the
glass shader (ImageFilter.compose), so blurRadius can go as wide as you
like at flat cost. This is why the package requires Flutter 3.41 or newer:
composing ImageFilter.blur with ImageFilter.shader shifted the shader's
coordinate system until
flutter#170820 was fixed
(see Bugs dodged).
Bugs dodged #
Each design decision below traces to a bug you can watch another project hitting:
- No intermediate textures.
liquid_glass_rendererrasterizes shape geometry into textures, and documents memory spikes when animating shapes because Flutter can't dispose those textures promptly (flutter#138627); the same texture path crashes outright when geometry bounds collapse to zero size (whynotmake-it#149, open). Here the blob field is evaluated analytically in the shader — animating blobs is just uniform updates, with nothing to allocate or dispose. - The layer's origin is passed as a uniform every paint.
FlutterFragCoord()is anchored to the render target, not the filtered layer, so a glass layer that moves within the target evaluates its field in the wrong place — typically ending up invisible or warped. That symptom class is on file againstliquid_glass_renderer: glass and contents disappearing when scrolled to the bounds (whynotmake-it#124, open) and vertical distortion when scrolling to the top of a feed (whynotmake-it#33, open). - Fixed-count loops, no
break. Flutter's SkSL transpile mishandles loop constructs —FragmentProgramfails to compile a plain for loop on Skia (flutter#116850, open), and during this buildbreakcompiled silently and rendered garbage. The blob loops here run all 16 iterations with anifguard, pinned bytest/sdf_field_test.dart. - Anti-aliasing without
fwidth. Derivative built-ins are rejected at runtime on web (flutter#180959, open), so edge AA uses the analytic field gradient instead — which also keeps the edge band a constant ~1.5 physical pixels wherever merges compress the field. - Unsupported backends downgrade instead of throwing. Requesting glass
where
ImageFilter.shaderisn't available silently falls back to flat; compareliquid_glass_rendererthrowing on unsupported render backends (whynotmake-it#12, open).