diff --git a/doc/_sphinx/extensions/flutter_app.css b/doc/_sphinx/extensions/flutter_app.css index 54af42ca3..9743bfbf9 100644 --- a/doc/_sphinx/extensions/flutter_app.css +++ b/doc/_sphinx/extensions/flutter_app.css @@ -127,6 +127,7 @@ button.flutter-app-button:after { .flutter-app-iframe { border: 1px solid #555; + display: block; height: 350px; width: 100%; } @@ -138,7 +139,6 @@ button.flutter-app-button:after { float: right; margin-left: 6pt; padding: 8px; - width: 280px; } .flutter-app-infobox button.flutter-app-iframe { @@ -148,8 +148,7 @@ button.flutter-app-button:after { .flutter-app-infobox button.flutter-app-button { float: right; font-size: 0.85em; - margin-bottom: 0; - margin-right: 0; + margin: 6px 0 0 0; min-height: 14pt; min-width: 50pt; } diff --git a/doc/_sphinx/extensions/flutter_app.py b/doc/_sphinx/extensions/flutter_app.py index bef8b948d..2d92b48ad 100644 --- a/doc/_sphinx/extensions/flutter_app.py +++ b/doc/_sphinx/extensions/flutter_app.py @@ -52,6 +52,11 @@ class FlutterAppDirective(SphinxDirective): compiled. "infobox" - the content will be displayed as an infobox floating on the right-hand side of the page. + + :width: - override the default width of an iframe in widget/infobox modes. + + :height: - override the default height of an iframe in widget/infobox + modes. """ has_content = True required_arguments = 0 @@ -60,6 +65,8 @@ class FlutterAppDirective(SphinxDirective): 'sources': directives.unchanged, 'page': directives.unchanged, 'show': directives.unchanged, + 'width': directives.unchanged, + 'height': directives.unchanged, } # Static list of targets that were already compiled during the build COMPILED = [] @@ -89,7 +96,21 @@ class FlutterAppDirective(SphinxDirective): iframe_url = _doc_root() + self.html_dir + '/index.html?' + page result = [] if 'widget' in self.modes: - result.append(IFrame(src=iframe_url, classes=['flutter-app-iframe'])) + iframe = IFrame(src=iframe_url, classes=['flutter-app-iframe']) + result.append(iframe) + styles = [] + if self.options.get('width'): + width = self.options.get('width') + if width.isdigit(): + width += 'px' + styles.append("width: " + width) + if self.options.get('height'): + height = self.options.get('height') + if height.isdigit(): + height += 'px' + styles.append("height: " + height) + if styles: + iframe.attributes['style'] = '; '.join(styles) if 'popup' in self.modes: result.append(Button( '', @@ -237,8 +258,10 @@ def _doc_root(): class IFrame(nodes.Element, nodes.General): def visit(self, node): - self.body.append( - self.starttag(node, 'iframe', src=node.attributes['src'])) + attrs = {'src': node.attributes['src']} + if 'style' in node.attributes: + attrs['style'] = node.attributes['style'] + self.body.append(self.starttag(node, 'iframe', **attrs).strip()) def depart(self, _): self.body.append('') diff --git a/doc/_sphinx/theme/flames.css b/doc/_sphinx/theme/flames.css index 499eb87ba..ca488f155 100644 --- a/doc/_sphinx/theme/flames.css +++ b/doc/_sphinx/theme/flames.css @@ -797,3 +797,12 @@ div.admonition.admonition-deprecated { --admonition-icon-color: #555; --admonition-title-background-color: #1c1c1c; } + + +pre, div[class*="highlight-"] { + clear: none; +} + +h1, h2, h3, h4, h5, h6 { + clear: both; +} diff --git a/doc/flame/examples/lib/decorator_blur.dart b/doc/flame/examples/lib/decorator_blur.dart new file mode 100644 index 000000000..6f00dbb65 --- /dev/null +++ b/doc/flame/examples/lib/decorator_blur.dart @@ -0,0 +1,30 @@ +import 'package:doc_flame_examples/flower.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; +import 'package:flame/rendering.dart'; + +class DecoratorBlurGame extends FlameGame with HasTappableComponents { + @override + Future onLoad() async { + var step = 0; + add( + Flower( + size: 100, + position: canvasSize / 2, + onTap: (flower) { + step++; + if (step == 1) { + flower.decorator = PaintDecorator.blur(3.0); + } else if (step == 2) { + flower.decorator = PaintDecorator.blur(5.0); + } else if (step == 3) { + flower.decorator = PaintDecorator.blur(0.0, 20.0); + } else { + flower.decorator = null; + step = 0; + } + }, + )..onTapUp(), + ); + } +} diff --git a/doc/flame/examples/lib/decorator_grayscale.dart b/doc/flame/examples/lib/decorator_grayscale.dart new file mode 100644 index 000000000..9ed895c4d --- /dev/null +++ b/doc/flame/examples/lib/decorator_grayscale.dart @@ -0,0 +1,32 @@ +import 'package:doc_flame_examples/flower.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; +import 'package:flame/rendering.dart'; + +class DecoratorGrayscaleGame extends FlameGame with HasTappableComponents { + @override + Future onLoad() async { + var step = 0; + add( + Flower( + size: 100, + position: canvasSize / 2, + onTap: (flower) { + step++; + if (step == 1) { + flower.decorator = PaintDecorator.grayscale(); + } else if (step == 2) { + flower.decorator = PaintDecorator.grayscale(opacity: 0.5); + } else if (step == 3) { + flower.decorator = PaintDecorator.grayscale(opacity: 0.2); + } else if (step == 4) { + flower.decorator = PaintDecorator.grayscale(opacity: 0.1); + } else { + flower.decorator = null; + step = 0; + } + }, + )..onTapUp(), + ); + } +} diff --git a/doc/flame/examples/lib/decorator_tint.dart b/doc/flame/examples/lib/decorator_tint.dart new file mode 100644 index 000000000..06c188dd6 --- /dev/null +++ b/doc/flame/examples/lib/decorator_tint.dart @@ -0,0 +1,36 @@ +import 'dart:ui'; + +import 'package:doc_flame_examples/flower.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; +import 'package:flame/rendering.dart'; + +class DecoratorTintGame extends FlameGame with HasTappableComponents { + @override + Future onLoad() async { + var step = 0; + add( + Flower( + size: 100, + position: canvasSize / 2, + onTap: (flower) { + step++; + if (step == 1) { + flower.decorator = PaintDecorator.tint(const Color(0x88FF0000)); + } else if (step == 2) { + flower.decorator = PaintDecorator.tint(const Color(0x8800FF00)); + } else if (step == 3) { + flower.decorator = PaintDecorator.tint(const Color(0x88000088)); + } else if (step == 4) { + flower.decorator = PaintDecorator.tint(const Color(0x66FFFFFF)); + } else if (step == 5) { + flower.decorator = PaintDecorator.tint(const Color(0xAA000000)); + } else { + flower.decorator = null; + step = 0; + } + }, + )..onTapUp(), + ); + } +} diff --git a/doc/flame/examples/lib/flower.dart b/doc/flame/examples/lib/flower.dart new file mode 100644 index 000000000..1e76a72b5 --- /dev/null +++ b/doc/flame/examples/lib/flower.dart @@ -0,0 +1,53 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; + +const tau = 2 * pi; + +class Flower extends PositionComponent with TapCallbacks, HasDecorator { + Flower({required double size, void Function(Flower)? onTap, super.position}) + : _onTap = onTap, + super(size: Vector2.all(size), anchor: Anchor.center) { + final radius = size * 0.38; + _paths.add(_makePath(radius * 1.4, 6, -0.05, 0.8)); + _paths.add(_makePath(radius, 6, 0.25, 1.5)); + _paths.add(_makePath(radius * 0.8, 6, 0.3, 1.4)); + _paths.add(_makePath(radius * 0.55, 6, 0.2, 1.5)); + _paths.add(_makePath(radius * 0.1, 12, 0.1, 6)); + _paints.add(Paint()..color = const Color(0xff255910)); + _paints.add(Paint()..color = const Color(0xffee3f3f)); + _paints.add(Paint()..color = const Color(0xffffbd66)); + _paints.add(Paint()..color = const Color(0xfff6f370)); + _paints.add(Paint()..color = const Color(0xfffffff0)); + } + + final List _paths = []; + final List _paints = []; + final void Function(Flower)? _onTap; + + Path _makePath(double radius, int n, double sharpness, double f) { + final radius2 = radius * f; + final p0 = Vector2(radius, 0)..rotate(0); + final path = Path()..moveTo(p0.x, p0.y); + for (var i = 0; i < n; i++) { + final p1 = Vector2(radius2, 0)..rotate(tau / n * (i + sharpness)); + final p2 = Vector2(radius2, 0)..rotate(tau / n * (i + 1 - sharpness)); + final p3 = Vector2(radius, 0)..rotate(tau / n * (i + 1)); + path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + path.close(); + return path.shift(Offset(width / 2, height / 2)); + } + + @override + void render(Canvas canvas) { + for (var i = 0; i < _paths.length; i++) { + canvas.drawPath(_paths[i], _paints[i]); + } + } + + @override + void onTapUp([TapUpEvent? event]) => _onTap?.call(this); +} diff --git a/doc/flame/examples/lib/main.dart b/doc/flame/examples/lib/main.dart index 76200327a..10d619983 100644 --- a/doc/flame/examples/lib/main.dart +++ b/doc/flame/examples/lib/main.dart @@ -1,5 +1,8 @@ import 'dart:html'; // ignore: avoid_web_libraries_in_flutter +import 'package:doc_flame_examples/decorator_blur.dart'; +import 'package:doc_flame_examples/decorator_grayscale.dart'; +import 'package:doc_flame_examples/decorator_tint.dart'; import 'package:doc_flame_examples/drag_events.dart'; import 'package:doc_flame_examples/tap_events.dart'; import 'package:flame/game.dart'; @@ -18,6 +21,15 @@ void main() { case 'drag_events': game = DragEventsGame(); break; + case 'decorator_blur': + game = DecoratorBlurGame(); + break; + case 'decorator_grayscale': + game = DecoratorGrayscaleGame(); + break; + case 'decorator_tinted': + game = DecoratorTintGame(); + break; } if (game != null) { runApp(GameWidget(game: game)); diff --git a/doc/flame/rendering/decorators.md b/doc/flame/rendering/decorators.md new file mode 100644 index 000000000..65f8ee0d9 --- /dev/null +++ b/doc/flame/rendering/decorators.md @@ -0,0 +1,103 @@ +# Decorators + +**Decorators** are classes that can encapsulate certain visual effects and then apply those visual +effects to a sequence of canvas drawing operations. Decorators are not [Component]s, but they can +be applied to components either manually or via the [HasDecorator] mixin. Likewise, decorators are +not [Effect]s, although they can be used to implement certain `Effect`s. + +There are a certain number of decorators available in Flame, and it is simple to add one's own if +necessary. We are planning to add shader-based decorators once Flutter fully supports them on the +web. + + +## Flame built-in decorators + +### PaintDecorator.blur + +```{flutter-app} +:sources: ../flame/examples +:page: decorator_blur +:show: widget code infobox +:width: 180 +:height: 160 +``` + +This decorator applies a Gaussian blur to the underlying component. The amount of blur can be +different in the X and Y direction, though this is not very common. + +```dart +final decorator = PaintDecorator.blur(3.0); +``` + +Possible uses: +- soft shadows; +- "out-of-focus" objects in the distance or very close to the camera; +- motion blur effects; +- deemphasize/obscure content when showing a popup dialog; +- blurred vision when the character is drunk. + + +### PaintDecorator.grayscale + +```{flutter-app} +:sources: ../flame/examples +:page: decorator_grayscale +:show: widget infobox +:width: 180 +:height: 160 +``` + +This decorator converts the underlying image into the shades of grey, as if it was a +black-and-white photograph. In addition, you can make the image semi-transparent to the desired +level of `opacity`. + +```dart +final decorator = PaintDecorator.grayscale(opacity: 0.5); +``` + +Possible uses: +- apply to an NPC to turn them into stone, or into a ghost! +- apply to a scene to indicate that it is a memory of the past; +- black-and-white photos. + + +### PaintDecorator.tint + +```{flutter-app} +:sources: ../flame/examples +:page: decorator_tint +:show: widget infobox +:width: 180 +:height: 160 +``` + +This decorator *tints* the underlying image with the specified color, as if watching it through a +colored glass. It is recommended that the `color` used by this decorator was semi-transparent, so +that you can see the details of the image below. + +```dart +final decorator = PaintDecorator.tint(const Color(0xAAFF0000); +``` + +Possible uses: +- NPCs affected by certain types of magic; +- items/characters in the shadows can be tinted black; +- tint the scene red to show bloodlust, or that the character is low on health; +- tint green to show that the character is poisoned or sick; +- tint the scene deep blue during the night time; + + +## Using decorators + +### HasDecorator mixin + +This `Component` mixin adds the `decorator` property, which is initially `null`. If you set this +property to an actual `Decorator` object, then that decorator will apply its visual effect during +the rendering of the component. In order to remove this visual effect, simply set the `decorator` +property back to `null`. + + + +[Component]: ../../flame/components.md#component +[Effect]: ../../flame/effects.md +[HasDecorator]: #hasdecorator-mixin diff --git a/doc/flame/rendering/rendering.md b/doc/flame/rendering/rendering.md index 3bfbb8ea2..3bd0a4306 100644 --- a/doc/flame/rendering/rendering.md +++ b/doc/flame/rendering/rendering.md @@ -3,10 +3,11 @@ ```{eval-rst} .. toctree:: :hidden: - + Images, sprites and animations Text rendering Colors and palette Particles + Decorators Layers ``` diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 757d120fb..522bcfab6 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -14,6 +14,7 @@ export 'src/components/mixins/component_viewport_margin.dart'; export 'src/components/mixins/draggable.dart'; export 'src/components/mixins/gesture_hitboxes.dart'; export 'src/components/mixins/has_ancestor.dart'; +export 'src/components/mixins/has_decorator.dart' show HasDecorator; export 'src/components/mixins/has_game_ref.dart'; export 'src/components/mixins/has_paint.dart'; export 'src/components/mixins/hoverable.dart'; diff --git a/packages/flame/lib/rendering.dart b/packages/flame/lib/rendering.dart new file mode 100644 index 000000000..057934c93 --- /dev/null +++ b/packages/flame/lib/rendering.dart @@ -0,0 +1,2 @@ +export 'src/rendering/decorator.dart' show Decorator; +export 'src/rendering/paint_decorator.dart' show PaintDecorator; diff --git a/packages/flame/lib/src/components/mixins/has_decorator.dart b/packages/flame/lib/src/components/mixins/has_decorator.dart new file mode 100644 index 000000000..46fcbadc7 --- /dev/null +++ b/packages/flame/lib/src/components/mixins/has_decorator.dart @@ -0,0 +1,27 @@ +import 'dart:ui'; + +import 'package:flame/src/components/component.dart'; +import 'package:flame/src/rendering/decorator.dart'; + +/// [HasDecorator] mixin adds a nullable [decorator] field to a Component. If +/// this field is set, it will apply the visual effect encapsulated in this +/// [Decorator] to the component. If the field is not set, then the component +/// will be rendered normally. +/// +/// Note that the decorator only affects visual rendering of a component, but +/// not its perceived size or shape from the point of view of tap events. +/// +/// See also: +/// - [Decorator] class for the list of available decorators. +mixin HasDecorator on Component { + Decorator? decorator; + + @override + void renderTree(Canvas canvas) { + if (decorator == null) { + super.renderTree(canvas); + } else { + decorator!.apply(super.renderTree, canvas); + } + } +} diff --git a/packages/flame/lib/src/rendering/decorator.dart b/packages/flame/lib/src/rendering/decorator.dart new file mode 100644 index 000000000..52e8a266c --- /dev/null +++ b/packages/flame/lib/src/rendering/decorator.dart @@ -0,0 +1,26 @@ +import 'dart:ui'; + +import 'package:flame/src/rendering/paint_decorator.dart'; + +/// [Decorator] is an abstract class that encapsulates a particular visual +/// effect that should apply to drawing commands wrapped by this class. +/// +/// The simplest way to apply a [Decorator] to a component is to override its +/// `renderTree` method like this: +/// ```dart +/// @override +/// void renderTree(Canvas canvas) { +/// decorator.apply(super.renderTree, canvas); +/// } +/// ``` +/// +/// The following implementations are available: +/// - [PaintDecorator] +abstract class Decorator { + /// Applies visual effect while [draw]ing on the [canvas]. + /// + /// A no-op decorator would simply call `draw(canvas)`. Any other non-trivial + /// decorator can transform the canvas before drawing, or perform any other + /// adjustment. + void apply(void Function(Canvas) draw, Canvas canvas); +} diff --git a/packages/flame/lib/src/rendering/paint_decorator.dart b/packages/flame/lib/src/rendering/paint_decorator.dart new file mode 100644 index 000000000..99535ea00 --- /dev/null +++ b/packages/flame/lib/src/rendering/paint_decorator.dart @@ -0,0 +1,41 @@ +import 'dart:ui'; + +import 'package:flame/src/rendering/decorator.dart'; + +/// [PaintDecorator] applies a paint filter to a group of drawing operations. +/// +/// Specifically, the following filters are available: +/// - [PaintDecorator.blur] adds Gaussian blur to the image, as if your vision +/// became blurry and out of focus; +/// - [PaintDecorator.tint] tints the picture with the specified color, as if +/// looking through a colored glass; +/// - [PaintDecorator.grayscale] removes all color from the picture, as if it +/// was a black-and-white photo. +class PaintDecorator extends Decorator { + PaintDecorator.blur(double amount, [double? amountY]) { + addBlur(amount, amountY ?? amount); + } + + PaintDecorator.tint(Color color) { + _paint.colorFilter = ColorFilter.mode(color, BlendMode.srcATop); + } + + PaintDecorator.grayscale({double opacity = 1.0}) { + _paint.color = Color.fromARGB((255 * opacity).toInt(), 0, 0, 0); + _paint.blendMode = BlendMode.luminosity; + } + + final _paint = Paint(); + + void addBlur(double amount, [double? amountY]) { + _paint.imageFilter = + ImageFilter.blur(sigmaX: amount, sigmaY: amountY ?? amount); + } + + @override + void apply(void Function(Canvas) draw, Canvas canvas) { + canvas.saveLayer(null, _paint); + draw(canvas); + canvas.restore(); + } +} diff --git a/packages/flame/test/_goldens/has_decorator_1.png b/packages/flame/test/_goldens/has_decorator_1.png new file mode 100644 index 000000000..d0db07248 Binary files /dev/null and b/packages/flame/test/_goldens/has_decorator_1.png differ diff --git a/packages/flame/test/_goldens/paint_decorator_blur.png b/packages/flame/test/_goldens/paint_decorator_blur.png new file mode 100644 index 000000000..37a1bb098 Binary files /dev/null and b/packages/flame/test/_goldens/paint_decorator_blur.png differ diff --git a/packages/flame/test/_goldens/paint_decorator_grayscale.png b/packages/flame/test/_goldens/paint_decorator_grayscale.png new file mode 100644 index 000000000..f2beca2c1 Binary files /dev/null and b/packages/flame/test/_goldens/paint_decorator_grayscale.png differ diff --git a/packages/flame/test/_goldens/paint_decorator_tinted.png b/packages/flame/test/_goldens/paint_decorator_tinted.png new file mode 100644 index 000000000..c30f600dd Binary files /dev/null and b/packages/flame/test/_goldens/paint_decorator_tinted.png differ diff --git a/packages/flame/test/_goldens/paint_decorator_with_blur.png b/packages/flame/test/_goldens/paint_decorator_with_blur.png new file mode 100644 index 000000000..f55ed620e Binary files /dev/null and b/packages/flame/test/_goldens/paint_decorator_with_blur.png differ diff --git a/packages/flame/test/_resources/zz_guitarre.png b/packages/flame/test/_resources/zz_guitarre.png new file mode 100644 index 000000000..e3f78bd5c Binary files /dev/null and b/packages/flame/test/_resources/zz_guitarre.png differ diff --git a/packages/flame/test/components/mixins/has_decorator_test.dart b/packages/flame/test/components/mixins/has_decorator_test.dart new file mode 100644 index 000000000..54382ea6d --- /dev/null +++ b/packages/flame/test/components/mixins/has_decorator_test.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/rendering.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('HasDecorator', () { + testGolden( + 'Component rendering with and without a Decorator', + (game) async { + await game.add( + _DecoratedComponent( + position: Vector2.all(25), + size: Vector2.all(40), + ), + ); + await game.add( + _DecoratedComponent( + position: Vector2(75, 25), + size: Vector2.all(40), + )..decorator = (PaintDecorator.grayscale()..addBlur(2)), + ); + }, + size: Vector2(100, 50), + goldenFile: '../../_goldens/has_decorator_1.png', + ); + }); +} + +class _DecoratedComponent extends PositionComponent with HasDecorator { + _DecoratedComponent({super.position, super.size}) + : super(anchor: Anchor.center); + + final paint = Paint()..color = const Color(0xff30ccd2); + + @override + void render(Canvas canvas) { + final radius = size.x / 2; + canvas.drawCircle(Offset(radius, radius), radius, paint); + } +} diff --git a/packages/flame/test/rendering/paint_decorator_test.dart b/packages/flame/test/rendering/paint_decorator_test.dart new file mode 100644 index 000000000..35fd567fd --- /dev/null +++ b/packages/flame/test/rendering/paint_decorator_test.dart @@ -0,0 +1,116 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/rendering.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../_resources/load_image.dart'; + +void main() { + group('PaintDecorator', () { + testGolden( + 'blur effect', + (game) async { + final image = await loadImage('flame.png'); + game.addAll([ + SpriteComponent(sprite: Sprite(image)), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.blur(0, 10), + position: Vector2(150, 0), + ), + ]); + }, + size: Vector2(300, 220), + goldenFile: '../_goldens/paint_decorator_blur.png', + ); + + testGolden( + 'grayscale effect', + (game) async { + final image = await loadImage('flame.png'); + game.addAll([ + SpriteComponent(sprite: Sprite(image)), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.grayscale(), + position: Vector2(150, 0), + ), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.grayscale(opacity: 0.5), + position: Vector2(300, 0), + ), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.grayscale(opacity: 0.25), + position: Vector2(450, 0), + ), + ]); + }, + size: Vector2(600, 220), + goldenFile: '../_goldens/paint_decorator_grayscale.png', + ); + + testGolden( + 'tint effect', + (game) async { + final image = await loadImage('zz_guitarre.png'); + game.addAll([ + SpriteComponent(sprite: Sprite(image)), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.tint(const Color(0x8800FF00)), + position: Vector2(100, 0), + ), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.tint(const Color(0x880000FF)), + position: Vector2(200, 0), + ), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.tint(const Color(0xAAFFFFFF)), + position: Vector2(300, 0), + ), + ]); + }, + size: Vector2(400, 300), + goldenFile: '../_goldens/paint_decorator_tinted.png', + ); + + testGolden( + 'grayscale/tinted with blur', + (game) async { + final image = await loadImage('zz_guitarre.png'); + const color = Color(0x88EBFF7F); + game.addAll([ + SpriteComponent(sprite: Sprite(image)), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.grayscale()..addBlur(3), + position: Vector2(100, 0), + ), + _DecoratedSprite( + sprite: Sprite(image), + decorator: PaintDecorator.tint(color)..addBlur(3), + position: Vector2(200, 0), + ), + ]); + }, + size: Vector2(300, 300), + goldenFile: '../_goldens/paint_decorator_with_blur.png', + ); + }); +} + +class _DecoratedSprite extends SpriteComponent { + _DecoratedSprite({super.sprite, super.position, required this.decorator}); + final Decorator decorator; + + @override + void renderTree(Canvas canvas) { + decorator.apply(super.renderTree, canvas); + } +}