You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
samples/experimental/varfont_shader_puzzle/lib/components/fragment_shaded.dart

279 lines
7.5 KiB

// 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<ui.FragmentProgram> 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<Shader, ui.FragmentProgram> _programCache = {};
const FragmentShaded({
required this.shader,
required this.shaderDuration,
required this.child,
super.key,
});
@override
State<FragmentShaded> createState() => FragmentShadedState();
}
class FragmentShadedState extends State<FragmentShaded>
with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _dampenAnimation;
late final Animation<double> _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<double>(begin: 1.0, end: 0.0).animate(_dampenCurve);
initializeFragmentProgramsAndBuilder();
}
Future<void> 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<double> animation;
final Animation<double> 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);
}
}