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/next_gen_ui_demo/lib/common/shader_effect.dart

363 lines
10 KiB

// Copyright 2023 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:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_animate/flutter_animate.dart';
/**
* This is an unfinished, pre-release effect for Flutter Animate:
* https://pub.dev/packages/flutter_animate
*
* It includes a copy of `AnimatedSampler` from Flutter Shaders:
* https://github.com/jonahwilliams/flutter_shaders
*
* Once `AnimatedSampler` (or equivalent) is stable, or included in the core
* SDK, this effect will be updated, tested, refined, and added to the
* effects.dart file.
*/
// TODO: document.
/// An effect that lets you apply an animated fragment shader to a target.
@immutable
class ShaderEffect extends Effect<double> {
const ShaderEffect({
super.delay,
super.duration,
super.curve,
this.shader,
this.update,
ShaderLayer? layer,
}) : layer = layer ?? ShaderLayer.replace,
super(
begin: 0,
end: 1,
);
final ui.FragmentShader? shader;
final ShaderUpdateCallback? update;
final ShaderLayer layer;
@override
Widget build(
BuildContext context,
Widget child,
AnimationController controller,
EffectEntry entry,
) {
double ratio = 1 / MediaQuery.of(context).devicePixelRatio;
Animation<double> animation = buildAnimation(controller, entry);
return getOptimizedBuilder<double>(
animation: animation,
builder: (_, __) {
return AnimatedSampler(
(image, size, canvas) {
EdgeInsets? insets;
if (update != null) {
insets = update!(shader!, animation.value, size, image);
}
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
rect = insets?.inflateRect(rect) ?? rect;
void drawImage() {
canvas.save();
canvas.scale(ratio, ratio);
canvas.drawImage(image, Offset.zero, Paint());
canvas.restore();
}
if (layer == ShaderLayer.foreground) drawImage();
if (shader != null) canvas.drawRect(rect, Paint()..shader = shader);
if (layer == ShaderLayer.background) drawImage();
},
enabled: shader != null,
child: child,
);
},
);
}
}
extension ShaderEffectExtensions<T> on AnimateManager<T> {
/// Adds a [shader] extension to [AnimateManager] ([Animate] and [AnimateList]).
T shader({
Duration? delay,
Duration? duration,
Curve? curve,
ui.FragmentShader? shader,
ShaderUpdateCallback? update,
ShaderLayer? layer,
}) =>
addEffect(ShaderEffect(
delay: delay,
duration: duration,
curve: curve,
shader: shader,
update: update,
layer: layer,
));
}
enum ShaderLayer { foreground, background, replace }
/// Function signature for [ShaderEffect] update handlers.
typedef ShaderUpdateCallback = EdgeInsets? Function(
ui.FragmentShader shader, double value, Size size, ui.Image image);
/******************************************************************************/
// TODO: add this as a dependency instead of copying it in once it is stable:
// https://github.com/jonahwilliams/flutter_shaders
// Copyright 2013 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.
/// A callback for the [AnimatedSamplerBuilder] widget.
typedef AnimatedSamplerBuilder = void Function(
ui.Image image,
Size size,
ui.Canvas canvas,
);
/// A widget that allows access to a snapshot of the child widgets for painting
/// with a sampler applied to a [FragmentProgram].
///
/// When [enabled] is true, the child widgets will be painted into a texture
/// exposed as a [ui.Image]. This can then be passed to a [FragmentShader]
/// instance via [FragmentShader.setSampler].
///
/// If [enabled] is false, then the child widgets are painted as normal.
///
/// Caveats:
/// * Platform views cannot be captured in a texture. If any are present they
/// will be excluded from the texture. Texture-based platform views are OK.
///
/// Example:
///
/// Providing an image to a fragment shader using
/// [FragmentShader.setImageSampler].
///
/// ```dart
/// Widget build(BuildContext context) {
/// return AnimatedSampler(
/// (ui.Image image, Size size, Canvas canvas) {
/// shader
/// ..setFloat(0, size.width)
/// ..setFloat(1, size.height)
/// ..setImageSampler(0, image);
/// canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
/// },
/// child: widget.child,
/// );
/// }
/// ```
///
/// See also:
/// * [SnapshotWidget], which provides a similar API for the purpose of
/// caching during expensive animations.
class AnimatedSampler extends StatelessWidget {
/// Create a new [AnimatedSampler].
const AnimatedSampler(
this.builder, {
required this.child,
super.key,
this.enabled = true,
});
/// A callback used by this widget to provide the children captured in
/// a texture.
final AnimatedSamplerBuilder builder;
/// Whether the children should be captured in a texture or displayed as
/// normal.
final bool enabled;
/// The child widget.
final Widget child;
@override
Widget build(BuildContext context) {
return _ShaderSamplerBuilder(
builder,
enabled: enabled,
child: child,
);
}
}
class _ShaderSamplerBuilder extends SingleChildRenderObjectWidget {
const _ShaderSamplerBuilder(
this.builder, {
super.child,
required this.enabled,
});
final AnimatedSamplerBuilder builder;
final bool enabled;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderShaderSamplerBuilderWidget(
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
builder: builder,
enabled: enabled,
);
}
@override
void updateRenderObject(
BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderShaderSamplerBuilderWidget)
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
..builder = builder
..enabled = enabled;
}
}
// 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 AnimatedSamplerBuilder builder,
required bool enabled,
}) : _devicePixelRatio = devicePixelRatio,
_builder = builder,
_enabled = enabled;
@override
OffsetLayer updateCompositedLayer(
{required covariant _ShaderSamplerBuilderLayer? oldLayer}) {
final _ShaderSamplerBuilderLayer layer =
oldLayer ?? _ShaderSamplerBuilderLayer(builder);
layer
..callback = builder
..size = size
..devicePixelRatio = devicePixelRatio;
return layer;
}
/// 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;
markNeedsCompositedLayerUpdate();
}
/// The painter used to paint the child snapshot or child widgets.
AnimatedSamplerBuilder get builder => _builder;
AnimatedSamplerBuilder _builder;
set builder(AnimatedSamplerBuilder value) {
if (value == builder) {
return;
}
_builder = value;
markNeedsCompositedLayerUpdate();
}
bool get enabled => _enabled;
bool _enabled;
set enabled(bool value) {
if (value == enabled) {
return;
}
_enabled = value;
markNeedsPaint();
markNeedsCompositingBitsUpdate();
}
@override
bool get isRepaintBoundary => alwaysNeedsCompositing;
@override
bool get alwaysNeedsCompositing => enabled;
@override
void paint(PaintingContext context, Offset offset) {
if (size.isEmpty || !_enabled) {
return;
}
assert(offset == Offset.zero);
return super.paint(context, offset);
}
}
/// A [Layer] that uses an [AnimatedSamplerBuilder] to create a [ui.Picture]
/// every time it is added to a scene.
class _ShaderSamplerBuilderLayer extends OffsetLayer {
_ShaderSamplerBuilderLayer(this._callback);
ui.Picture? _lastPicture;
Size get size => _size;
Size _size = Size.zero;
set size(Size value) {
if (value == size) {
return;
}
_size = value;
markNeedsAddToScene();
}
double get devicePixelRatio => _devicePixelRatio;
double _devicePixelRatio = 1.0;
set devicePixelRatio(double value) {
if (value == devicePixelRatio) {
return;
}
_devicePixelRatio = value;
markNeedsAddToScene();
}
AnimatedSamplerBuilder get callback => _callback;
AnimatedSamplerBuilder _callback;
set callback(AnimatedSamplerBuilder value) {
if (value == callback) {
return;
}
_callback = value;
markNeedsAddToScene();
}
ui.Image _buildChildScene(Rect bounds, double pixelRatio) {
final ui.SceneBuilder builder = ui.SceneBuilder();
final Matrix4 transform =
Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1);
builder.pushTransform(transform.storage);
addChildrenToScene(builder);
builder.pop();
return builder.build().toImageSync(
(pixelRatio * bounds.width).ceil(),
(pixelRatio * bounds.height).ceil(),
);
}
@override
void addToScene(ui.SceneBuilder builder) {
if (size.isEmpty) return;
final ui.Image image = _buildChildScene(
offset & size,
devicePixelRatio,
);
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
try {
callback(image, size, canvas);
} finally {
image.dispose();
}
_lastPicture?.dispose();
_lastPicture = pictureRecorder.endRecording();
builder.addPicture(offset, _lastPicture!);
}
}