// Copyright 2023 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; enum Shader { nothing('nothing'), bwSplit('bw_split'), colorSplit('color_split'), rowOffset('row_offset'), wavyCirc('wavy_circ'), wavy('wavy'), wavy2('wavy2'); const Shader(this.name); final String name; Future get program => ui.FragmentProgram.fromAsset('shaders/$name.frag'); } class FragmentShaded extends StatefulWidget { final Widget child; final Shader shader; final int shaderDuration; static const int dampenDuration = 1000; static final Map _programCache = {}; const FragmentShaded({ required this.shader, required this.shaderDuration, required this.child, super.key, }); @override State createState() => FragmentShadedState(); } class FragmentShadedState extends State with TickerProviderStateMixin { late final AnimationController _controller; late final Animation _dampenAnimation; late final Animation _dampenCurve; late final AnimationController _dampenController; late AnimatingSamplerBuilder builder; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: widget.shaderDuration), )..repeat(reverse: false); _dampenController = AnimationController( vsync: this, duration: const Duration(milliseconds: FragmentShaded.dampenDuration), ); _dampenCurve = CurvedAnimation( parent: _dampenController, curve: Curves.easeInOut, ); _dampenAnimation = Tween(begin: 1.0, end: 0.0).animate(_dampenCurve); initializeFragmentProgramsAndBuilder(); } Future initializeFragmentProgramsAndBuilder() async { if (FragmentShaded._programCache.isEmpty) { for (final shader in Shader.values) { FragmentShaded._programCache[shader] = await shader.program; } } setState(() { builder = AnimatingSamplerBuilder(_controller, _dampenAnimation, FragmentShaded._programCache[widget.shader]!.fragmentShader()); }); } @override void dispose() { _controller.dispose(); _dampenController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (null == FragmentShaded._programCache[widget.shader]) { setState(() {}); return const SizedBox( width: 0, height: 0, ); } return Transform.scale( scale: 0.5, child: ShaderSamplerBuilder( builder, child: widget.child, ), ); } void startDampening() { _dampenController.forward(); } } class AnimatingSamplerBuilder extends SamplerBuilder { AnimatingSamplerBuilder( this.animation, this.dampenAnimation, this.fragmentShader) { animation.addListener(notifyListeners); dampenAnimation.addListener(notifyListeners); } final Animation animation; final Animation dampenAnimation; final ui.FragmentShader fragmentShader; @override void paint(ui.Image image, Size size, ui.Canvas canvas) { // animation fragmentShader.setFloat(0, animation.value); // width fragmentShader.setFloat(1, size.width); // height fragmentShader.setFloat(2, size.height); // dampener fragmentShader.setFloat(3, dampenAnimation.value); // sampler fragmentShader.setImageSampler(0, image); canvas.drawRect(Offset.zero & size, Paint()..shader = fragmentShader); } } abstract class SamplerBuilder extends ChangeNotifier { void paint(ui.Image image, Size size, ui.Canvas canvas); } class ShaderSamplerBuilder extends StatelessWidget { const ShaderSamplerBuilder(this.builder, {required this.child, super.key}); final SamplerBuilder builder; final Widget child; @override Widget build(BuildContext context) { return RepaintBoundary( child: _ShaderSamplerImpl( builder, child: child, )); } } class _ShaderSamplerImpl extends SingleChildRenderObjectWidget { const _ShaderSamplerImpl(this.builder, {super.child}); final SamplerBuilder builder; @override RenderObject createRenderObject(BuildContext context) { return _RenderShaderSamplerBuilderWidget( devicePixelRatio: MediaQuery.of(context).devicePixelRatio, builder: builder, ); } @override void updateRenderObject( BuildContext context, covariant RenderObject renderObject) { (renderObject as _RenderShaderSamplerBuilderWidget) ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio ..builder = builder; } } // A render object that conditionally converts its child into a [ui.Image] // and then paints it in place of the child. class _RenderShaderSamplerBuilderWidget extends RenderProxyBox { // Create a new [_RenderSnapshotWidget]. _RenderShaderSamplerBuilderWidget({ required double devicePixelRatio, required SamplerBuilder builder, }) : _devicePixelRatio = devicePixelRatio, _builder = builder; /// The device pixel ratio used to create the child image. double get devicePixelRatio => _devicePixelRatio; double _devicePixelRatio; set devicePixelRatio(double value) { if (value == devicePixelRatio) { return; } _devicePixelRatio = value; if (_childRaster == null) { return; } else { _childRaster?.dispose(); _childRaster = null; markNeedsPaint(); } } /// The painter used to paint the child snapshot or child widgets. SamplerBuilder get builder => _builder; SamplerBuilder _builder; set builder(SamplerBuilder value) { if (value == builder) { return; } builder.removeListener(markNeedsPaint); _builder = value; builder.addListener(markNeedsPaint); markNeedsPaint(); } ui.Image? _childRaster; @override void attach(PipelineOwner owner) { builder.addListener(markNeedsPaint); super.attach(owner); } @override void detach() { _childRaster?.dispose(); _childRaster = null; builder.removeListener(markNeedsPaint); super.detach(); } @override void dispose() { builder.removeListener(markNeedsPaint); _childRaster?.dispose(); _childRaster = null; super.dispose(); } // Paint [child] with this painting context, then convert to a raster and detach all // children from this layer. ui.Image? _paintAndDetachToImage() { final OffsetLayer offsetLayer = OffsetLayer(); final PaintingContext context = PaintingContext(offsetLayer, Offset.zero & size); super.paint(context, Offset.zero); // This ignore is here because this method is protected by the `PaintingContext`. Adding a new // method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but // that would conflict with our goals of minimizing painting context. // ignore: invalid_use_of_protected_member context.stopRecordingIfNeeded(); final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio); offsetLayer.dispose(); return image; } @override void paint(PaintingContext context, Offset offset) { if (size.isEmpty) { _childRaster?.dispose(); _childRaster = null; return; } _childRaster?.dispose(); _childRaster = _paintAndDetachToImage(); builder.paint(_childRaster!, size, context.canvas); } }