Files
TPSchedule/mobile/lib/widgets/glass_refraction.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();
}
}