Created
October 4, 2022 19:44
-
-
Save jonahwilliams/f876f5a6c0efcbc90788cf339f777f25 to your computer and use it in GitHub Desktop.
Animated Sampler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 The Flutter Authors. 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:typed_data'; | |
import 'dart:ui' as ui; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
class AnimatingSamplerBuilder extends SamplerBuilder { | |
AnimatingSamplerBuilder(this.animation, this.fragmentShader) { | |
animation.addListener(notifyListeners); | |
} | |
static final Float64List _identity = Matrix4.identity().storage; | |
final Animation<double> animation; | |
final ui.FragmentShader fragmentShader; | |
@override | |
void paint(ui.Image image, Size size, ui.Canvas canvas) { | |
final ImageShader sampler = ImageShader(image, TileMode.clamp, TileMode.clamp, _identity); | |
// width | |
fragmentShader.setFloat(0, size.width); | |
// height | |
fragmentShader.setFloat(1, size.height); | |
// Animation | |
fragmentShader.setFloat(2, animation.value); | |
// Sampler | |
fragmentShader.setSampler(0, sampler); | |
canvas.drawRect(Offset.zero & size, Paint()..shader = fragmentShader); | |
} | |
} | |
late ui.FragmentProgram fragmentProgram; | |
void main() async { | |
fragmentProgram = await ui.FragmentProgram.fromAsset('shaders/example.frag'); | |
runApp(const MyWidget()); | |
} | |
class MyWidget extends StatefulWidget { | |
const MyWidget({super.key}); | |
@override | |
State<MyWidget> createState() => _MyWidgetState(); | |
} | |
class _MyWidgetState extends State<MyWidget> | |
with SingleTickerProviderStateMixin { | |
late AnimationController _controller; | |
late final builder = AnimatingSamplerBuilder(_controller, fragmentProgram.fragmentShader()); | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 1000)) | |
..forward(from: 0.0); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Center( | |
child: | |
ShaderSamplerBuilder( | |
builder, | |
child: const CircularProgressIndicator() | |
) | |
); | |
} | |
} | |
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 MaterialApp(home: 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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment