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 Components, but they can be applied to components either manually or via the HasDecorator mixin. Likewise, decorators are not Effects, although they can be used to implement certain Effects.

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

decorator_blur.dart
 1import 'package:doc_flame_examples/flower.dart';
 2import 'package:flame/game.dart';
 3import 'package:flame/rendering.dart';
 4
 5class DecoratorBlurGame extends FlameGame {
 6  @override
 7  Future<void> onLoad() async {
 8    var step = 0;
 9    add(
10      Flower(
11        size: 100,
12        position: canvasSize / 2,
13        onTap: (flower) {
14          final decorator = flower.decorator;
15          step++;
16          if (step == 1) {
17            decorator.addLast(PaintDecorator.blur(3.0));
18          } else if (step == 2) {
19            decorator.replaceLast(PaintDecorator.blur(5.0));
20          } else if (step == 3) {
21            decorator.replaceLast(PaintDecorator.blur(0.0, 20.0));
22          } else {
23            decorator.replaceLast(null);
24            step = 0;
25          }
26        },
27      )..onTapUp(),
28    );
29  }
30}

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.

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

decorator_grayscale.dart
 1import 'package:doc_flame_examples/flower.dart';
 2import 'package:flame/game.dart';
 3import 'package:flame/rendering.dart';
 4
 5class DecoratorGrayscaleGame extends FlameGame {
 6  @override
 7  Future<void> onLoad() async {
 8    var step = 0;
 9    add(
10      Flower(
11        size: 100,
12        position: canvasSize / 2,
13        onTap: (flower) {
14          final decorator = flower.decorator;
15          step++;
16          if (step == 1) {
17            decorator.addLast(PaintDecorator.grayscale());
18          } else if (step == 2) {
19            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.5));
20          } else if (step == 3) {
21            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.2));
22          } else if (step == 4) {
23            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.1));
24          } else {
25            decorator.removeLast();
26            step = 0;
27          }
28        },
29      )..onTapUp(),
30    );
31  }
32}

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.

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

decorator_tint.dart
 1import 'dart:ui';
 2
 3import 'package:doc_flame_examples/flower.dart';
 4import 'package:flame/game.dart';
 5import 'package:flame/rendering.dart';
 6
 7class DecoratorTintGame extends FlameGame {
 8  @override
 9  Future<void> onLoad() async {
10    var step = 0;
11    add(
12      Flower(
13        size: 100,
14        position: canvasSize / 2,
15        onTap: (flower) {
16          final decorator = flower.decorator;
17          step++;
18          if (step == 1) {
19            decorator.addLast(PaintDecorator.tint(const Color(0x88FF0000)));
20          } else if (step == 2) {
21            decorator.replaceLast(PaintDecorator.tint(const Color(0x8800FF00)));
22          } else if (step == 3) {
23            decorator.replaceLast(PaintDecorator.tint(const Color(0x88000088)));
24          } else if (step == 4) {
25            decorator.replaceLast(PaintDecorator.tint(const Color(0x66FFFFFF)));
26          } else if (step == 5) {
27            decorator.replaceLast(PaintDecorator.tint(const Color(0xAA000000)));
28          } else {
29            decorator.removeLast();
30            step = 0;
31          }
32        },
33      )..onTapUp(),
34    );
35  }
36}

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.

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;

Rotate3DDecorator

decorator_rotate3d.dart
 1import 'package:doc_flame_examples/flower.dart';
 2import 'package:flame/game.dart';
 3import 'package:flame/rendering.dart';
 4
 5class DecoratorRotate3DGame extends FlameGame {
 6  @override
 7  Future<void> onLoad() async {
 8    var step = 0;
 9    final decorator = Rotate3DDecorator()
10      ..center = Vector2.all(50)
11      ..perspective = 0.01;
12    add(
13      Flower(
14        size: 100,
15        position: canvasSize / 2,
16        decorator: decorator,
17        onTap: (flower) {
18          step++;
19          if (step == 1) {
20            decorator.angleY = -0.8;
21          } else if (step == 2) {
22            decorator.angleX = 1.0;
23          } else if (step == 3) {
24            decorator.angleZ = 0.2;
25          } else if (step == 4) {
26            decorator.angleX = 10;
27          } else if (step == 5) {
28            decorator.angleY = 2;
29          } else {
30            decorator
31              ..angleX = 0
32              ..angleY = 0
33              ..angleZ = 0;
34            step = 0;
35          }
36        },
37      )..onTapUp(),
38    );
39  }
40}

This decorator applies a 3D rotation to the underlying component. You can specify the angles of the rotation, as well as the pivot point and the amount of perspective distortion to apply.

The decorator also supplies the isFlipped property, which allows you to determine whether the component is currently being viewed from the front side or from the back. This is useful if you want to draw a component whose appearance is different in the front and in the back.

final decorator = Rotate3DDecorator(
  center: component.center,
  angleX: rotationAngle,
  perspective: 0.002,
);

Possible uses:

  • a card that can be flipped over;

  • pages in a book;

  • transitions between app routes;

  • 3d falling particles such as snowflakes or leaves.

Shadow3DDecorator

decorator_shadow3d.dart
 1import 'dart:ui';
 2
 3import 'package:doc_flame_examples/flower.dart';
 4import 'package:flame/components.dart';
 5import 'package:flame/game.dart';
 6import 'package:flame/rendering.dart';
 7
 8class DecoratorShadowGame extends FlameGame {
 9  @override
10  Color backgroundColor() => const Color(0xFFC7C7C7);
11
12  @override
13  Future<void> onLoad() async {
14    final decorator = Shadow3DDecorator(base: Vector2(0, 100));
15    var step = 0;
16    add(Grid());
17    add(
18      Flower(
19        size: 100,
20        position: canvasSize / 2,
21        decorator: decorator,
22        onTap: (flower) {
23          step++;
24          if (step == 1) {
25            decorator.xShift = 200;
26            decorator.opacity = 0.5;
27          } else if (step == 2) {
28            decorator.xShift = 400;
29            decorator.yScale = 3;
30            decorator.blur = 1;
31          } else if (step == 3) {
32            decorator.angle = 1.7;
33            decorator.blur = 2;
34          } else if (step == 4) {
35            decorator.ascent = 20;
36            decorator.angle = 1.7;
37            decorator.blur = 2;
38            flower.y -= 20;
39          } else {
40            decorator.ascent = 0;
41            decorator.xShift = 0;
42            decorator.yScale = 1;
43            decorator.angle = -1.4;
44            decorator.opacity = 0.8;
45            decorator.blur = 0;
46            flower.y += 20;
47            step = 0;
48          }
49        },
50      )..onTapUp(),
51    );
52  }
53}
54
55class Grid extends Component {
56  final paint = Paint()
57    ..color = const Color(0xffa9a9a9)
58    ..style = PaintingStyle.stroke
59    ..strokeWidth = 1;
60  @override
61  void render(Canvas canvas) {
62    for (var i = 0; i < 50; i++) {
63      canvas.drawLine(Offset(0, i * 25), Offset(500, i * 25), paint);
64      canvas.drawLine(Offset(i * 25, 0), Offset(i * 25, 500), paint);
65    }
66  }
67}

This decorator renders a shadow underneath the component, as if the component was a 3D object standing on a plane. This effect works best for games that use isometric camera projection.

The shadow produced by this generator is quite flexible: you can control its angle, length, opacity, blur, etc. For a full description of what properties this decorator has and their meaning, see the class documentation.

final decorator = Shadow3DDecorator(
  base: Vector2(100, 150),
  angle: -1.4,
  xShift: 200,
  yScale: 1.5,
  opacity: 0.5,
  blur: 1.5,
);

The primary purpose of this decorator is to add shadows on the ground to your components. The main limitation is that the shadows are flat and cannot interact with the environment. For example, this decorator cannot handle shadows that fall onto walls or other vertical structures.

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.

PositionComponent

PositionComponent (and all the derived classes) already has a decorator property, so for these components the HasDecorator mixin is not needed.

In fact, the PositionComponent uses its decorator in order to properly position the component on the screen. Thus, any new decorators that you’d want to apply to the PositionComponent will need to be chained (see the Multiple decorators section below).

It is also possible to replace the root decorator of the PositionComponent, if you want to create an alternative logic for how the component shall be positioned on the screen.

Multiple decorators

It is possible to apply several decorators simultaneously to the same component: the Decorator class supports chaining. That is, if you have an existing decorator on a component and you want to add another one, then you can call component.decorator.addLast(newDecorator) – this will add the new decorator at the end of the existing chain. The method removeLast() can remove that decorator later.

Several decorators can be chain that way. For example, if A is an initial decorator, then A.addLast(B) can be followed by either A.addLast(C) or B.addLast(C) – and in both cases the chain A -> B -> C will be created. In practice, it means that the entire chain can be manipulated from its root, which usually is component.decorator.