148 lines
4.2 KiB
Dart
148 lines
4.2 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
/// Filters live pixels beneath the glass; symbols above it remain undistorted.
|
|
class GlassRefraction extends StatefulWidget {
|
|
const GlassRefraction({
|
|
super.key,
|
|
required this.strength,
|
|
this.blurSigma = 2,
|
|
required this.child,
|
|
});
|
|
final double strength;
|
|
|
|
/// Optical softness in logical pixels, independent of display density.
|
|
final double blurSigma;
|
|
final Widget child;
|
|
@override
|
|
State<GlassRefraction> createState() => _GlassRefractionState();
|
|
}
|
|
|
|
class _GlassRefractionState extends State<GlassRefraction> {
|
|
static Future<ui.FragmentProgram>? _program;
|
|
static ui.FragmentProgram? _loadedProgram;
|
|
ui.FragmentShader? _shader;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (ui.ImageFilter.isShaderFilterSupported) _load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final program = _loadedProgram ??= await (_program ??=
|
|
ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag'));
|
|
if (mounted) setState(() => _shader = program.fragmentShader());
|
|
} catch (error) {
|
|
_program = null;
|
|
_loadedProgram = null;
|
|
debugPrint('Glass refraction unavailable: $error');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_shader?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => _shader == null
|
|
? BackdropFilter(
|
|
filter: ui.ImageFilter.blur(
|
|
sigmaX: widget.blurSigma,
|
|
sigmaY: widget.blurSigma,
|
|
tileMode: TileMode.clamp,
|
|
),
|
|
child: widget.child,
|
|
)
|
|
: _LiveGlassFilter(
|
|
shader: _shader!,
|
|
strength: widget.strength,
|
|
blurSigma: widget.blurSigma,
|
|
pixelRatio: MediaQuery.devicePixelRatioOf(context),
|
|
child: widget.child,
|
|
);
|
|
}
|
|
|
|
class _LiveGlassFilter extends SingleChildRenderObjectWidget {
|
|
const _LiveGlassFilter({
|
|
required this.shader,
|
|
required this.strength,
|
|
required this.blurSigma,
|
|
required this.pixelRatio,
|
|
required super.child,
|
|
});
|
|
final ui.FragmentShader shader;
|
|
final double strength, blurSigma, pixelRatio;
|
|
@override
|
|
_RenderLiveGlass createRenderObject(BuildContext context) =>
|
|
_RenderLiveGlass(shader, strength, blurSigma, pixelRatio);
|
|
@override
|
|
void updateRenderObject(
|
|
BuildContext context,
|
|
_RenderLiveGlass renderObject,
|
|
) => renderObject.update(shader, strength, blurSigma, pixelRatio);
|
|
}
|
|
|
|
class _RenderLiveGlass extends RenderProxyBox {
|
|
_RenderLiveGlass(
|
|
this._shader,
|
|
this._strength,
|
|
this._blurSigma,
|
|
this._pixelRatio,
|
|
);
|
|
ui.FragmentShader _shader;
|
|
double _strength, _blurSigma, _pixelRatio;
|
|
final _layer = LayerHandle<BackdropFilterLayer>();
|
|
void update(
|
|
ui.FragmentShader shader,
|
|
double strength,
|
|
double blurSigma,
|
|
double pixelRatio,
|
|
) {
|
|
_shader = shader;
|
|
_strength = strength;
|
|
_blurSigma = blurSigma;
|
|
_pixelRatio = pixelRatio;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
@override
|
|
bool get alwaysNeedsCompositing => child != null;
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (child == null) return;
|
|
// Sample coordinates are physical pixels. Resolve the actual transform
|
|
// during paint so the glass also follows the Dock's hide/show animation.
|
|
final origin = localToGlobal(Offset.zero) * _pixelRatio;
|
|
_shader
|
|
..setFloat(2, origin.dx)
|
|
..setFloat(3, origin.dy)
|
|
..setFloat(4, size.width * _pixelRatio)
|
|
..setFloat(5, size.height * _pixelRatio)
|
|
..setFloat(6, _strength);
|
|
_layer.layer ??= BackdropFilterLayer();
|
|
// Refract the original backdrop first, then diffuse it with the engine's
|
|
// separable Gaussian blur. Keeping the shader inside the composition also
|
|
// keeps its source texture coordinates independent of blur padding.
|
|
_layer.layer!.filter = ui.ImageFilter.compose(
|
|
outer: ui.ImageFilter.blur(
|
|
sigmaX: _blurSigma,
|
|
sigmaY: _blurSigma,
|
|
tileMode: TileMode.clamp,
|
|
),
|
|
inner: ui.ImageFilter.shader(_shader),
|
|
);
|
|
context.pushLayer(_layer.layer!, super.paint, offset);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_layer.layer = null;
|
|
super.dispose();
|
|
}
|
|
}
|