Skip to content

Instantly share code, notes, and snippets.

@jonahwilliams
Created October 4, 2022 19:44
Show Gist options
  • Save jonahwilliams/f876f5a6c0efcbc90788cf339f777f25 to your computer and use it in GitHub Desktop.
Save jonahwilliams/f876f5a6c0efcbc90788cf339f777f25 to your computer and use it in GitHub Desktop.
Animated Sampler
// 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