mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 11:43:19 +08:00
feat: Added Decorator class and HasDecorator mixin (#1781)
The new class allows applying visual filters to canvas drawing operations. It is conceptually similar to the LayerProcessor class, except that it applies directly to a canvas instead of a Picture. This functionality was extracted from PR #1755. NB: the guitar image was taken from here, which is a public domain image, i.e. licensed under CC0 1.0.
This commit is contained in:
@ -127,6 +127,7 @@ button.flutter-app-button:after {
|
|||||||
|
|
||||||
.flutter-app-iframe {
|
.flutter-app-iframe {
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
|
display: block;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -138,7 +139,6 @@ button.flutter-app-button:after {
|
|||||||
float: right;
|
float: right;
|
||||||
margin-left: 6pt;
|
margin-left: 6pt;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
width: 280px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flutter-app-infobox button.flutter-app-iframe {
|
.flutter-app-infobox button.flutter-app-iframe {
|
||||||
@ -148,8 +148,7 @@ button.flutter-app-button:after {
|
|||||||
.flutter-app-infobox button.flutter-app-button {
|
.flutter-app-infobox button.flutter-app-button {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
margin-bottom: 0;
|
margin: 6px 0 0 0;
|
||||||
margin-right: 0;
|
|
||||||
min-height: 14pt;
|
min-height: 14pt;
|
||||||
min-width: 50pt;
|
min-width: 50pt;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,11 @@ class FlutterAppDirective(SphinxDirective):
|
|||||||
compiled.
|
compiled.
|
||||||
"infobox" - the content will be displayed as an infobox floating on
|
"infobox" - the content will be displayed as an infobox floating on
|
||||||
the right-hand side of the page.
|
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
|
has_content = True
|
||||||
required_arguments = 0
|
required_arguments = 0
|
||||||
@ -60,6 +65,8 @@ class FlutterAppDirective(SphinxDirective):
|
|||||||
'sources': directives.unchanged,
|
'sources': directives.unchanged,
|
||||||
'page': directives.unchanged,
|
'page': directives.unchanged,
|
||||||
'show': directives.unchanged,
|
'show': directives.unchanged,
|
||||||
|
'width': directives.unchanged,
|
||||||
|
'height': directives.unchanged,
|
||||||
}
|
}
|
||||||
# Static list of targets that were already compiled during the build
|
# Static list of targets that were already compiled during the build
|
||||||
COMPILED = []
|
COMPILED = []
|
||||||
@ -89,7 +96,21 @@ class FlutterAppDirective(SphinxDirective):
|
|||||||
iframe_url = _doc_root() + self.html_dir + '/index.html?' + page
|
iframe_url = _doc_root() + self.html_dir + '/index.html?' + page
|
||||||
result = []
|
result = []
|
||||||
if 'widget' in self.modes:
|
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:
|
if 'popup' in self.modes:
|
||||||
result.append(Button(
|
result.append(Button(
|
||||||
'',
|
'',
|
||||||
@ -237,8 +258,10 @@ def _doc_root():
|
|||||||
|
|
||||||
class IFrame(nodes.Element, nodes.General):
|
class IFrame(nodes.Element, nodes.General):
|
||||||
def visit(self, node):
|
def visit(self, node):
|
||||||
self.body.append(
|
attrs = {'src': node.attributes['src']}
|
||||||
self.starttag(node, 'iframe', 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, _):
|
def depart(self, _):
|
||||||
self.body.append('</iframe>')
|
self.body.append('</iframe>')
|
||||||
|
|||||||
@ -797,3 +797,12 @@ div.admonition.admonition-deprecated {
|
|||||||
--admonition-icon-color: #555;
|
--admonition-icon-color: #555;
|
||||||
--admonition-title-background-color: #1c1c1c;
|
--admonition-title-background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pre, div[class*="highlight-"] {
|
||||||
|
clear: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|||||||
30
doc/flame/examples/lib/decorator_blur.dart
Normal file
30
doc/flame/examples/lib/decorator_blur.dart
Normal file
@ -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<void> 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
doc/flame/examples/lib/decorator_grayscale.dart
Normal file
32
doc/flame/examples/lib/decorator_grayscale.dart
Normal file
@ -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<void> 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
doc/flame/examples/lib/decorator_tint.dart
Normal file
36
doc/flame/examples/lib/decorator_tint.dart
Normal file
@ -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<void> 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
doc/flame/examples/lib/flower.dart
Normal file
53
doc/flame/examples/lib/flower.dart
Normal file
@ -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<Path> _paths = [];
|
||||||
|
final List<Paint> _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);
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
|
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/drag_events.dart';
|
||||||
import 'package:doc_flame_examples/tap_events.dart';
|
import 'package:doc_flame_examples/tap_events.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
@ -18,6 +21,15 @@ void main() {
|
|||||||
case 'drag_events':
|
case 'drag_events':
|
||||||
game = DragEventsGame();
|
game = DragEventsGame();
|
||||||
break;
|
break;
|
||||||
|
case 'decorator_blur':
|
||||||
|
game = DecoratorBlurGame();
|
||||||
|
break;
|
||||||
|
case 'decorator_grayscale':
|
||||||
|
game = DecoratorGrayscaleGame();
|
||||||
|
break;
|
||||||
|
case 'decorator_tinted':
|
||||||
|
game = DecoratorTintGame();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (game != null) {
|
if (game != null) {
|
||||||
runApp(GameWidget(game: game));
|
runApp(GameWidget(game: game));
|
||||||
|
|||||||
103
doc/flame/rendering/decorators.md
Normal file
103
doc/flame/rendering/decorators.md
Normal file
@ -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
|
||||||
@ -3,10 +3,11 @@
|
|||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|
||||||
Images, sprites and animations <images.md>
|
Images, sprites and animations <images.md>
|
||||||
Text rendering <text.md>
|
Text rendering <text.md>
|
||||||
Colors and palette <palette.md>
|
Colors and palette <palette.md>
|
||||||
Particles <particles.md>
|
Particles <particles.md>
|
||||||
|
Decorators <decorators.md>
|
||||||
Layers <layers.md>
|
Layers <layers.md>
|
||||||
```
|
```
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export 'src/components/mixins/component_viewport_margin.dart';
|
|||||||
export 'src/components/mixins/draggable.dart';
|
export 'src/components/mixins/draggable.dart';
|
||||||
export 'src/components/mixins/gesture_hitboxes.dart';
|
export 'src/components/mixins/gesture_hitboxes.dart';
|
||||||
export 'src/components/mixins/has_ancestor.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_game_ref.dart';
|
||||||
export 'src/components/mixins/has_paint.dart';
|
export 'src/components/mixins/has_paint.dart';
|
||||||
export 'src/components/mixins/hoverable.dart';
|
export 'src/components/mixins/hoverable.dart';
|
||||||
|
|||||||
2
packages/flame/lib/rendering.dart
Normal file
2
packages/flame/lib/rendering.dart
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export 'src/rendering/decorator.dart' show Decorator;
|
||||||
|
export 'src/rendering/paint_decorator.dart' show PaintDecorator;
|
||||||
27
packages/flame/lib/src/components/mixins/has_decorator.dart
Normal file
27
packages/flame/lib/src/components/mixins/has_decorator.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/flame/lib/src/rendering/decorator.dart
Normal file
26
packages/flame/lib/src/rendering/decorator.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
41
packages/flame/lib/src/rendering/paint_decorator.dart
Normal file
41
packages/flame/lib/src/rendering/paint_decorator.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/flame/test/_goldens/has_decorator_1.png
Normal file
BIN
packages/flame/test/_goldens/has_decorator_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
packages/flame/test/_goldens/paint_decorator_blur.png
Normal file
BIN
packages/flame/test/_goldens/paint_decorator_blur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
packages/flame/test/_goldens/paint_decorator_grayscale.png
Normal file
BIN
packages/flame/test/_goldens/paint_decorator_grayscale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
packages/flame/test/_goldens/paint_decorator_tinted.png
Normal file
BIN
packages/flame/test/_goldens/paint_decorator_tinted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/flame/test/_goldens/paint_decorator_with_blur.png
Normal file
BIN
packages/flame/test/_goldens/paint_decorator_with_blur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/flame/test/_resources/zz_guitarre.png
Normal file
BIN
packages/flame/test/_resources/zz_guitarre.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
packages/flame/test/rendering/paint_decorator_test.dart
Normal file
116
packages/flame/test/rendering/paint_decorator_test.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user