Skip to content

Commit ad3bcff

Browse files
committed
feat!: glass shadows now cut out the foreground shape to support offset shadows
`BlurStyle` is now ignored on shadows you pass to `LiquidGlass`, and always behaves similar to `BlurStyle.outer`. When any shadow has a non-zero `offset`, the glass shape is cut out of the composed shadow stack so the shadow does not bleed through the translucent glass body.
1 parent 18531dd commit ad3bcff

7 files changed

Lines changed: 127 additions & 34 deletions

File tree

packages/liquid_glass_renderer/example/lib/basic_app.dart

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,25 @@ class BasicApp extends HookWidget {
2626
final tab = useState(0);
2727
final fake = useState(false);
2828

29+
final visibility = useState(true);
30+
final visibilityValue = useSingleMotion(
31+
value: visibility.value ? 1.0 : 0.0,
32+
motion: Motion.smoothSpring(),
33+
);
34+
2935
final light = AlwaysStoppedAnimation(pi / 4);
3036

3137
const shadows = [
3238
BoxShadow(
3339
blurStyle: BlurStyle.outer,
3440
color: Color.from(alpha: 0.05, red: 0, green: 0, blue: 0),
41+
offset: Offset(0, 1),
3542
blurRadius: 2,
3643
),
3744
BoxShadow(
3845
blurStyle: BlurStyle.outer,
3946
color: Color.from(alpha: 0.1, red: 0, green: 0, blue: 0),
47+
offset: Offset(0, 8),
4048
blurRadius: 30,
4149
),
4250
];
@@ -95,9 +103,11 @@ class BasicApp extends HookWidget {
95103
glassColor: CupertinoTheme.of(
96104
context,
97105
).barBackgroundColor.withValues(alpha: 0.2),
106+
visibility: visibilityValue,
98107
);
99108
return LiquidGlassLayer(
100109
fake: fake.value,
110+
useBackdropGroup: true,
101111
settings: settings.copyWith(lightAngle: light.value),
102112
child: LiquidGlassBlendGroup(
103113
blend: blendNotifier.value,
@@ -109,19 +119,23 @@ class BasicApp extends HookWidget {
109119
mainAxisSize: MainAxisSize.min,
110120
spacing: 16,
111121
children: [
112-
LiquidStretch(
113-
child: LiquidGlass.auto(
114-
shadows: shadows,
115-
shape: LiquidRoundedSuperellipse(
116-
borderRadius: 20,
117-
),
118-
child: GlassGlow(
119-
child: SizedBox.square(
120-
dimension: 100,
121-
child: Center(
122-
child: fake.value
123-
? Text('FAKE')
124-
: Text('REAL'),
122+
GestureDetector(
123+
onTap: () =>
124+
visibility.value = !visibility.value,
125+
child: LiquidStretch(
126+
child: LiquidGlass.auto(
127+
shadows: shadows,
128+
shape: LiquidRoundedSuperellipse(
129+
borderRadius: 20,
130+
),
131+
child: GlassGlow(
132+
child: SizedBox.square(
133+
dimension: 100,
134+
child: Center(
135+
child: fake.value
136+
? Text('FAKE')
137+
: Text('REAL'),
138+
),
125139
),
126140
),
127141
),

packages/liquid_glass_renderer/example/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import FlutterMacOS
66
import Foundation
77

8+
import path_provider_foundation
89

910
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
11+
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
1012
}

packages/liquid_glass_renderer/lib/src/fake_glass.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ class FakeGlass extends StatelessWidget {
4444
final LiquidGlassSettings? settings;
4545

4646
/// The list of shadows to paint around the glass shape.
47+
///
48+
/// Only outer-equivalent shadows are supported; [BoxShadow.blurStyle] is
49+
/// ignored. When any shadow has a non-zero [BoxShadow.offset], the glass
50+
/// shape is cut out of the composed shadow stack so the shadow does not
51+
/// bleed through the translucent glass body.
4752
final List<BoxShadow> shadows;
4853

4954
/// The child widget that will be displayed inside the glass.

packages/liquid_glass_renderer/lib/src/glass_shadow.dart

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class GlassShadow extends SingleChildRenderObjectWidget {
2727
final LiquidGlassSettings settings;
2828

2929
/// The list of shadows to paint.
30+
///
31+
/// Only outer-equivalent shadows are supported; [BoxShadow.blurStyle] is
32+
/// ignored. When any shadow has a non-zero [BoxShadow.offset], the glass
33+
/// shape is cut out of the composed shadow stack so the shadow does not
34+
/// bleed through the translucent glass body.
3035
final List<BoxShadow> shadows;
3136

3237
@override
@@ -90,42 +95,69 @@ class _RenderGlassShadow extends RenderProxyBox {
9095
final rect = offset & size;
9196
final canvas = context.canvas;
9297

98+
final needsCutout = shadows.any((s) => s.offset != Offset.zero);
99+
100+
if (needsCutout) {
101+
var layerBounds = rect;
102+
for (final shadow in shadows) {
103+
layerBounds = layerBounds.expandToInclude(
104+
rect.shift(shadow.offset).inflate(
105+
shadow.spreadRadius + shadow.blurRadius * visibility,
106+
),
107+
);
108+
}
109+
canvas.saveLayer(layerBounds, Paint());
110+
}
111+
93112
for (final shadow in shadows) {
94113
final shadowRect =
95114
rect.shift(shadow.offset).inflate(shadow.spreadRadius);
96115
final paint = shadow
97116
.copyWith(
98117
blurRadius: shadow.blurRadius * visibility,
118+
blurStyle: needsCutout ? BlurStyle.normal : BlurStyle.outer,
99119
color: shadow.color.withValues(
100120
alpha: shadow.color.a * visibility,
101121
),
102122
)
103123
.toPaint();
104124

105-
switch (shape) {
106-
case LiquidRoundedSuperellipse(:final borderRadius):
107-
canvas.drawRSuperellipse(
108-
RSuperellipse.fromRectAndRadius(
109-
shadowRect,
110-
Radius.circular(borderRadius),
111-
),
112-
paint,
113-
);
114-
115-
case LiquidOval():
116-
canvas.drawOval(shadowRect, paint);
117-
case LiquidRoundedRectangle(:final borderRadius):
118-
canvas.drawRRect(
119-
RRect.fromRectAndRadius(
120-
shadowRect,
121-
Radius.circular(borderRadius),
122-
),
123-
paint,
124-
);
125-
}
125+
_drawShape(canvas, shadowRect, paint);
126+
}
127+
128+
if (needsCutout) {
129+
_drawShape(
130+
canvas,
131+
rect.deflate(.5),
132+
Paint()..blendMode = BlendMode.dstOut,
133+
);
134+
canvas.restore();
126135
}
127136
}
128137

129138
super.paint(context, offset);
130139
}
140+
141+
void _drawShape(Canvas canvas, Rect rect, Paint paint) {
142+
switch (shape) {
143+
case LiquidRoundedSuperellipse(:final borderRadius):
144+
canvas.drawRSuperellipse(
145+
RSuperellipse.fromRectAndRadius(
146+
rect,
147+
Radius.circular(borderRadius),
148+
),
149+
paint,
150+
);
151+
case LiquidOval():
152+
canvas.drawOval(rect, paint);
153+
case LiquidRoundedRectangle(:final borderRadius):
154+
canvas.drawRRect(
155+
RRect.fromRectAndRadius(
156+
rect,
157+
Radius.circular(borderRadius),
158+
),
159+
paint,
160+
);
161+
}
162+
}
131163
}

packages/liquid_glass_renderer/lib/src/liquid_glass.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ class LiquidGlass extends StatelessWidget {
145145
final (LiquidGlassSettings settings, bool fake)? ownLayerConfig;
146146

147147
/// The list of shadows to paint.
148+
///
149+
/// Only outer-equivalent shadows are supported; [BoxShadow.blurStyle] is
150+
/// ignored. When any shadow has a non-zero [BoxShadow.offset], the glass
151+
/// shape is cut out of the composed shadow stack so the shadow does not
152+
/// bleed through the translucent glass body.
148153
final List<BoxShadow> shadows;
149154

150155
/// Whether this glass should automatically detect a parent layer.

packages/liquid_glass_renderer/test/src/fake_glass_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,40 @@ void main() {
101101
],
102102
),
103103
);
104+
105+
goldenTest(
106+
'offset shadow is cut out behind glass',
107+
fileName: 'fake_glass_offset_shadow_cutout',
108+
pumpBeforeTest: pumpOnce,
109+
builder: () => GoldenTestGroup(
110+
scenarioConstraints: testScenarioConstraints,
111+
children: [
112+
for (final visibility in [0.0, 0.5, 1.0])
113+
GoldenTestScenario(
114+
name: 'visibility ${visibility.toStringAsFixed(1)}',
115+
child: buildWithGridPaper(
116+
FakeGlass(
117+
settings: LiquidGlassSettings(
118+
visibility: visibility,
119+
blur: 0,
120+
saturation: 1,
121+
chromaticAberration: 0,
122+
lightIntensity: 0,
123+
glassColor: const Color.fromARGB(128, 0, 0, 255),
124+
),
125+
shadows: const [
126+
BoxShadow(
127+
offset: Offset(16, 16),
128+
blurRadius: 24,
129+
),
130+
],
131+
shape: const LiquidRoundedSuperellipse(borderRadius: 40),
132+
child: const SizedBox.square(dimension: 300),
133+
),
134+
),
135+
),
136+
],
137+
),
138+
);
104139
});
105140
}
51.2 KB
Loading

0 commit comments

Comments
 (0)