Skip to content

Commit c52b7b8

Browse files
committed
feat: add FakeGlass widget that aims to match LiquidGlass appearance while being much more performant
This is achieved by ommitting the dispersion and refraction effects.
1 parent ed6a415 commit c52b7b8

File tree

5 files changed

+268
-12
lines changed

5 files changed

+268
-12
lines changed

packages/liquid_glass_renderer/example/lib/basic_app.dart

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ final settingsNotifier = ValueNotifier(
1414
thickness: 20,
1515
blur: 10,
1616
refractiveIndex: 1.2,
17-
glassColor: Colors.white.withValues(alpha: 0.1),
17+
glassColor: Colors.white.withValues(alpha: 0.2),
1818
),
1919
);
2020

@@ -27,6 +27,7 @@ class BasicApp extends HookWidget {
2727
final lightAngleController = useRotatingAnimationController();
2828

2929
final tab = useState(0);
30+
3031
return GestureDetector(
3132
onTap: () {
3233
SettingsSheet(
@@ -51,6 +52,49 @@ class BasicApp extends HookWidget {
5152
),
5253
],
5354
),
55+
Center(
56+
child: ListenableBuilder(
57+
listenable: Listenable.merge([
58+
settingsNotifier,
59+
lightAngleController,
60+
]),
61+
builder: (context, child) {
62+
final settings = settingsNotifier.value.copyWith(
63+
lightAngle: lightAngleController.value,
64+
glassColor: CupertinoTheme.of(
65+
context,
66+
).barBackgroundColor.withValues(alpha: 0.4),
67+
);
68+
return Column(
69+
mainAxisSize: MainAxisSize.min,
70+
spacing: 16,
71+
children: [
72+
LiquidGlass(
73+
settings: settings,
74+
shape: LiquidRoundedSuperellipse(
75+
borderRadius: Radius.circular(20),
76+
),
77+
glassContainsChild: false,
78+
child: SizedBox.square(
79+
dimension: 100,
80+
child: Center(child: Text('REAL')),
81+
),
82+
),
83+
FakeGlass(
84+
settings: settings,
85+
shape: LiquidRoundedSuperellipse(
86+
borderRadius: Radius.circular(20),
87+
),
88+
child: SizedBox.square(
89+
dimension: 100,
90+
child: Center(child: Text('FAKE')),
91+
),
92+
),
93+
],
94+
);
95+
},
96+
),
97+
),
5498
SafeArea(
5599
bottom: false,
56100
child: Align(

packages/liquid_glass_renderer/example/lib/widgets/bottom_bar.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ class _LiquidGlassBottomBarState extends State<LiquidGlassBottomBar> {
9696
widget.glassSettings ??
9797
LiquidGlassSettings(
9898
refractiveIndex: 1.21,
99-
thickness: 40,
100-
blur: 4,
99+
thickness: 30,
100+
blur: 8,
101101
saturation: 1.5,
102102
blend: 10,
103103
lightIntensity: isDark ? .7 : 1,
@@ -117,7 +117,6 @@ class _LiquidGlassBottomBarState extends State<LiquidGlassBottomBar> {
117117
bottom: widget.bottomPadding,
118118
),
119119
child: Row(
120-
crossAxisAlignment: CrossAxisAlignment.end,
121120
spacing: widget.spacing,
122121
children: [
123122
Expanded(
@@ -135,7 +134,6 @@ class _LiquidGlassBottomBarState extends State<LiquidGlassBottomBar> {
135134
onTabChanged: widget.onTabSelected,
136135
child: Container(
137136
padding: const EdgeInsets.symmetric(horizontal: 4),
138-
constraints: const BoxConstraints(maxWidth: 600),
139137
height: widget.barHeight,
140138
child: Row(
141139
crossAxisAlignment: CrossAxisAlignment.center,
@@ -653,8 +651,8 @@ class _IndicatorTransform extends StatelessWidget {
653651
alignment: Alignment.center,
654652
transform: buildJellyTransform(
655653
velocity: Offset(velocity, 0),
656-
maxDistortion: .5,
657-
velocityScale: 15,
654+
maxDistortion: .8,
655+
velocityScale: 10,
658656
),
659657
child: child,
660658
);

packages/liquid_glass_renderer/lib/liquid_glass_renderer.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// Liquid Glass Effect for Flutter
22
library liquid_glass_renderer;
33

4+
export 'src/fake_glass.dart' show FakeGlass;
45
export 'src/glass_link.dart' show GlassLink;
56
export 'src/glassify.dart' show Glassify;
67
export 'src/liquid_glass.dart' show LiquidGlass;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import 'dart:math' as math;
2+
import 'dart:ui';
3+
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter/rendering.dart';
6+
import 'package:liquid_glass_renderer/liquid_glass_renderer.dart';
7+
8+
class FakeGlass extends SingleChildRenderObjectWidget {
9+
const FakeGlass({
10+
required this.shape,
11+
required this.child,
12+
this.settings = const LiquidGlassSettings(),
13+
super.key,
14+
});
15+
16+
final LiquidShape shape;
17+
18+
final Widget child;
19+
20+
final LiquidGlassSettings settings;
21+
22+
@override
23+
RenderObject createRenderObject(BuildContext context) {
24+
return _RenderFakeGlass(
25+
shape: shape,
26+
settings: settings,
27+
);
28+
}
29+
30+
@override
31+
void updateRenderObject(
32+
BuildContext context, covariant RenderObject renderObject) {
33+
if (renderObject is _RenderFakeGlass) {
34+
renderObject
35+
..shape = shape
36+
..settings = settings;
37+
}
38+
}
39+
}
40+
41+
class _RenderFakeGlass extends RenderBox
42+
with RenderObjectWithChildMixin<RenderBox> {
43+
_RenderFakeGlass({
44+
required LiquidShape shape,
45+
required LiquidGlassSettings settings,
46+
}) : _shape = shape,
47+
_settings = settings;
48+
49+
LiquidShape _shape;
50+
LiquidShape get shape => _shape;
51+
set shape(LiquidShape value) {
52+
if (_shape == value) return;
53+
_shape = value;
54+
markNeedsPaint();
55+
}
56+
57+
LiquidGlassSettings _settings;
58+
LiquidGlassSettings get settings => _settings;
59+
set settings(LiquidGlassSettings value) {
60+
if (_settings == value) return;
61+
_settings = value;
62+
markNeedsPaint();
63+
}
64+
65+
@override
66+
void performLayout() {
67+
if (child != null) {
68+
child!.layout(constraints, parentUsesSize: true);
69+
size = child!.size;
70+
} else {
71+
size = constraints.smallest;
72+
}
73+
}
74+
75+
final LayerHandle<BackdropFilterLayer> _blurHandle =
76+
LayerHandle<BackdropFilterLayer>();
77+
78+
final LayerHandle<ClipPathLayer> _clipHandle = LayerHandle<ClipPathLayer>();
79+
80+
@override
81+
void detach() {
82+
_blurHandle.layer = null;
83+
_clipHandle.layer = null;
84+
super.detach();
85+
}
86+
87+
@override
88+
void paint(PaintingContext context, Offset offset) {
89+
final clipPath = shape.getOuterPath(offset & size);
90+
91+
// Create saturation filter if needed
92+
final ImageFilter? saturationFilter = settings.saturation != 1.0
93+
? ColorFilter.matrix(_createSaturationMatrix(settings.saturation))
94+
: null;
95+
96+
final blurFilter = ImageFilter.blur(
97+
sigmaX: settings.blur,
98+
sigmaY: settings.blur,
99+
);
100+
101+
// Combine blur and saturation filters
102+
final combinedFilter = saturationFilter != null
103+
? ImageFilter.compose(
104+
inner: saturationFilter,
105+
outer: blurFilter,
106+
)
107+
: blurFilter;
108+
109+
final blurLayer = (_blurHandle.layer ??= BackdropFilterLayer())
110+
..filter = combinedFilter;
111+
112+
final clipLayer = (_clipHandle.layer ??= ClipPathLayer())
113+
..clipPath = clipPath;
114+
115+
context.pushLayer(
116+
clipLayer,
117+
(context, offset) {
118+
context.pushLayer(
119+
blurLayer,
120+
(context, offset) {
121+
if (child != null) {
122+
_paintColor(context.canvas, clipPath);
123+
context.paintChild(child!, offset);
124+
_paintSpecular(context.canvas, clipPath);
125+
}
126+
},
127+
offset,
128+
);
129+
},
130+
offset,
131+
);
132+
133+
super.paint(context, offset);
134+
}
135+
136+
/// Creates a saturation adjustment matrix
137+
/// saturation = 0 -> grayscale (using Rec. 709 luma coefficients)
138+
/// saturation = 1 -> original color (no change)
139+
/// saturation > 1 -> over-saturated
140+
List<double> _createSaturationMatrix(double saturation) {
141+
// Rec. 709 luma coefficients for RGB to grayscale conversion
142+
const lumR = 0.299;
143+
const lumG = 0.587;
144+
const lumB = 0.114;
145+
146+
// Saturation matrix that interpolates between grayscale and original color
147+
// Based on: result = luminance + (color - luminance) * saturation
148+
final s = saturation;
149+
final invSat = 1.0 - s;
150+
151+
return [
152+
lumR * invSat + s, lumG * invSat, lumB * invSat, 0, 0, // R
153+
lumR * invSat, lumG * invSat + s, lumB * invSat, 0, 0, // G
154+
lumR * invSat, lumG * invSat, lumB * invSat + s, 0, 0, // B
155+
0, 0, 0, 1, 0, // A
156+
];
157+
}
158+
159+
void _paintColor(Canvas canvas, Path path) {
160+
final luminance = settings.glassColor.computeLuminance();
161+
162+
final blendMode = luminance < 0.5 ? BlendMode.multiply : BlendMode.screen;
163+
164+
final paint = Paint()
165+
..color = settings.glassColor
166+
..blendMode = blendMode
167+
..style = PaintingStyle.fill;
168+
169+
canvas.drawPath(path, paint);
170+
}
171+
172+
void _paintSpecular(Canvas canvas, Path path) {
173+
// Compute alignments from light angle
174+
final radians = settings.lightAngle;
175+
176+
final x = -1 * math.cos(radians);
177+
final y = -1 * math.sin(radians);
178+
179+
final color = Colors.white.withValues(
180+
alpha: settings.lightIntensity.clamp(0, 1),
181+
);
182+
final shader = LinearGradient(
183+
colors: [
184+
color,
185+
color.withValues(alpha: settings.ambientStrength.clamp(0, 1)),
186+
color.withValues(alpha: settings.ambientStrength.clamp(0, 1)),
187+
color,
188+
],
189+
begin: Alignment(x, y),
190+
end: Alignment(-x, -y),
191+
).createShader(path.getBounds());
192+
193+
// Paint sharp outline
194+
195+
final paint = Paint()
196+
..shader = shader
197+
..blendMode = BlendMode.lighten
198+
..style = PaintingStyle.stroke
199+
..strokeWidth = 1
200+
..maskFilter = const MaskFilter.blur(BlurStyle.normal, .7);
201+
canvas.drawPath(path, paint);
202+
203+
// Paint a second, slightly blurred outline
204+
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
205+
canvas.drawPath(path, paint);
206+
}
207+
}

packages/liquid_glass_renderer/lib/src/liquid_glass_layer.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,13 @@ class RenderLiquidGlassLayer extends RenderProxyBox {
345345
}
346346
});
347347

348-
_paintShapeContents(context, offset, shapes, glassContainsChild: true);
349-
350348
final shaderLayer = (_shaderHandle.layer ??= BackdropFilterLayer())
351349
..filter = ImageFilter.shader(_shader);
352350

353351
final blurLayer = (_blurLayerHandle.layer ??= BackdropFilterLayer())
354352
..filter = ImageFilter.blur(
355-
sigmaX: _settings.blur * _devicePixelRatio,
356-
sigmaY: _settings.blur * _devicePixelRatio,
353+
sigmaX: _settings.blur,
354+
sigmaY: _settings.blur,
357355
);
358356

359357
final clipPath = Path();
@@ -378,7 +376,15 @@ class RenderLiquidGlassLayer extends RenderProxyBox {
378376
(context, offset) {
379377
context.pushLayer(
380378
blurLayer,
381-
(context, offset) {},
379+
(context, offset) {
380+
// If glass contains child we paint it above blur but below shader
381+
_paintShapeContents(
382+
context,
383+
offset,
384+
shapes,
385+
glassContainsChild: true,
386+
);
387+
},
382388
offset,
383389
);
384390
},

0 commit comments

Comments
 (0)