Skip to content

Commit be912c0

Browse files
committed
feat: (#3851) add HueEffect and HueDecorator for color hue manipulation.
1 parent 98d94a6 commit be912c0

16 files changed

Lines changed: 435 additions & 8 deletions

File tree

doc/flame/effects/color_effects.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,29 @@ final effect = GlowEffect(
136136
```
137137

138138
Currently this effect can only be applied to components that have a `HasPaint` mixin.
139+
140+
141+
## HueEffect
142+
143+
This effect will rotate the hue of the target over time. It can only be applied to components that
144+
implement the `PaintProvider`.
145+
146+
```{flutter-app}
147+
:sources: ../flame/examples
148+
:page: hue_effect
149+
:show: widget code infobox
150+
:width: 180
151+
:height: 160
152+
```
153+
154+
```dart
155+
final effect = HueEffect(
156+
2 * tau,
157+
EffectController(duration: 3),
158+
);
159+
```
160+
161+
> [!TIP]
162+
> **Performance Note**: `HueEffect` is extremely efficient because it modifies the `Paint`'s
163+
> `colorFilter` directly. If you have many components, prefer this effect over the `HueDecorator`,
164+
> which uses `saveLayer()` and has much higher overhead.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'dart:math';
2+
3+
import 'package:doc_flame_examples/flower.dart';
4+
import 'package:flame/game.dart';
5+
import 'package:flame/rendering.dart';
6+
7+
class HueDecoratorGame 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(HueDecorator(pi / 4));
20+
} else if (step == 2) {
21+
decorator.replaceLast(HueDecorator(pi / 2));
22+
} else if (step == 3) {
23+
decorator.replaceLast(HueDecorator(pi));
24+
} else {
25+
decorator.replaceLast(null);
26+
step = 0;
27+
}
28+
},
29+
)..onTapUp(),
30+
);
31+
}
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'dart:math';
2+
3+
import 'package:doc_flame_examples/ember.dart';
4+
import 'package:flame/components.dart';
5+
import 'package:flame/effects.dart';
6+
import 'package:flame/game.dart';
7+
8+
class HueEffectExample extends FlameGame {
9+
@override
10+
Future<void> onLoad() async {
11+
final ember = EmberPlayer(
12+
position: size / 2,
13+
size: size / 4,
14+
onTap: (ember) {
15+
ember.add(
16+
HueEffect(
17+
2 * pi,
18+
EffectController(duration: 3),
19+
),
20+
);
21+
},
22+
)..anchor = Anchor.center;
23+
24+
add(ember);
25+
}
26+
}

doc/flame/examples/lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:doc_flame_examples/decorator_shadow3d.dart';
1010
import 'package:doc_flame_examples/decorator_tint.dart';
1111
import 'package:doc_flame_examples/drag_events.dart';
1212
import 'package:doc_flame_examples/glow_effect.dart';
13+
import 'package:doc_flame_examples/hue_decorator.dart';
14+
import 'package:doc_flame_examples/hue_effect.dart';
1315
import 'package:doc_flame_examples/move_along_path_effect.dart';
1416
import 'package:doc_flame_examples/move_by_effect.dart';
1517
import 'package:doc_flame_examples/move_to_effect.dart';
@@ -49,8 +51,10 @@ final routes = <String, Game Function()>{
4951
'decorator_rotate3d': DecoratorRotate3DGame.new,
5052
'decorator_shadow3d': DecoratorShadowGame.new,
5153
'decorator_tint': DecoratorTintGame.new,
54+
'decorator_hue': HueDecoratorGame.new,
5255
'drag_events': DragEventsGame.new,
5356
'glow_effect': GlowEffectExample.new,
57+
'hue_effect': HueEffectExample.new,
5458
'move_along_path_effect': MoveAlongPathEffectGame.new,
5559
'move_by_effect': MoveByEffectGame.new,
5660
'move_to_effect': MoveToEffectGame.new,

doc/flame/rendering/decorators.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
# Decorators
1+
## Performance Warning: Decorators vs Effects
22

3-
**Decorators** are classes that can encapsulate certain visual effects and then apply those visual
4-
effects to a sequence of canvas drawing operations. Decorators are not [Component]s, but they can
5-
be applied to components either manually or via the [HasDecorator] mixin. Likewise, decorators are
6-
not [Effect]s, although they can be used to implement certain `Effect`s.
3+
Applying a [Decorator] to a component can have a significant performance overhead, especially when
4+
it involves `canvas.saveLayer()`.
75

8-
There are a certain number of decorators available in Flame, and it is simple to add one's own if
9-
necessary. We are planning to add shader-based decorators once Flutter fully supports them on the
10-
web.
6+
- **Decorators** (e.g., [HueDecorator]): Use `canvas.saveLayer()` to isolate rendering and apply
7+
filters. This requires off-screen buffer allocation and GPU context switches. In high-density
8+
scenes (100+ sprites), this can be significantly slower than direct paint manipulation.
9+
- **Effects** (e.g., [HueEffect]): Modify the component's [Paint] directly. These are
10+
hardware-accelerated in the fragment shader with virtually zero memory overhead.
11+
12+
**Recommendation**: Always prefer **Effects** for simple color transformations on large numbers of
13+
units. Use **Decorators** only when you need complex visual composition or when targeting raw
14+
canvas drawing that isn't governed by a single [Paint].
1115

1216

1317
## Flame built-in decorators
@@ -157,6 +161,29 @@ limitation is that the shadows are flat and cannot interact with the environment
157161
decorator cannot handle shadows that fall onto walls or other vertical structures.
158162

159163

164+
### HueDecorator
165+
166+
```{flutter-app}
167+
:sources: ../flame/examples
168+
:page: decorator_hue
169+
:show: widget code infobox
170+
:width: 180
171+
:height: 160
172+
```
173+
174+
This decorator shifts the hue of the underlying component by the specified angle in radians.
175+
176+
```dart
177+
final decorator = HueDecorator(tau / 4);
178+
```
179+
180+
Possible uses:
181+
182+
- alternative color schemes for enemies ("palette swapping");
183+
- environmental changes (e.g., world turning purple/surreal);
184+
- power-up indicators.
185+
186+
160187
## Using decorators
161188

162189

examples/lib/stories/effects/effects.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:examples/stories/effects/combined_effect_example.dart';
55
import 'package:examples/stories/effects/dual_effect_removal_example.dart';
66
import 'package:examples/stories/effects/effect_controllers_example.dart';
77
import 'package:examples/stories/effects/function_effect_example.dart';
8+
import 'package:examples/stories/effects/hue_effect_example.dart';
89
import 'package:examples/stories/effects/move_effect_example.dart';
910
import 'package:examples/stories/effects/opacity_effect_example.dart';
1011
import 'package:examples/stories/effects/remove_effect_example.dart';
@@ -59,6 +60,12 @@ void addEffectsStories(Dashbook dashbook) {
5960
codeLink: baseLink('effects/opacity_effect_example.dart'),
6061
info: OpacityEffectExample.description,
6162
)
63+
..add(
64+
'Hue Effect',
65+
(_) => GameWidget(game: HueEffectExample()),
66+
codeLink: baseLink('effects/hue_effect_example.dart'),
67+
info: HueEffectExample.description,
68+
)
6269
..add(
6370
'Color Effect',
6471
(_) => GameWidget(game: ColorEffectExample()),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'dart:math';
2+
3+
import 'package:examples/commons/ember.dart';
4+
import 'package:flame/effects.dart';
5+
import 'package:flame/game.dart';
6+
7+
class HueEffectExample extends FlameGame {
8+
static const String description = '''
9+
In this example we show how the `HueEffect` can be used.
10+
Ember will shift its hue over time.
11+
''';
12+
13+
@override
14+
Future<void> onLoad() async {
15+
add(
16+
Ember(
17+
position: Vector2(size.x / 2, size.y / 2),
18+
size: Vector2.all(100),
19+
)..add(
20+
HueEffect(
21+
2 * pi,
22+
EffectController(
23+
duration: 3,
24+
infinite: true,
25+
),
26+
),
27+
),
28+
);
29+
}
30+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'dart:math';
2+
3+
import 'package:examples/commons/ember.dart';
4+
import 'package:flame/events.dart';
5+
import 'package:flame/game.dart';
6+
import 'package:flame/rendering.dart';
7+
8+
class HueDecoratorExample extends FlameGame with TapCallbacks {
9+
static const String description = '''
10+
In this example we show how the `HueDecorator` can be used.
11+
Click anywhere to cycle through different hue shifts on Ember.
12+
''';
13+
14+
late final Ember ember;
15+
int step = 0;
16+
17+
@override
18+
Future<void> onLoad() async {
19+
add(
20+
ember = Ember(
21+
position: size / 2,
22+
size: Vector2.all(100),
23+
)..decorator = HueDecorator(),
24+
);
25+
}
26+
27+
@override
28+
void onTapDown(TapDownEvent event) {
29+
step++;
30+
final decorator = ember.decorator as HueDecorator;
31+
if (step == 1) {
32+
decorator.hue = pi / 4;
33+
} else if (step == 2) {
34+
decorator.hue = pi / 2;
35+
} else if (step == 3) {
36+
decorator.hue = pi;
37+
} else {
38+
decorator.hue = 0;
39+
step = 0;
40+
}
41+
}
42+
}

examples/lib/stories/rendering/rendering.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:dashbook/dashbook.dart';
22
import 'package:examples/commons/commons.dart';
33
import 'package:examples/stories/rendering/flip_sprite_example.dart';
4+
import 'package:examples/stories/rendering/hue_decorator_example.dart';
45
import 'package:examples/stories/rendering/isometric_tile_map_example.dart';
56
import 'package:examples/stories/rendering/layers_example.dart';
67
import 'package:examples/stories/rendering/nine_tile_box_example.dart';
@@ -14,6 +15,12 @@ import 'package:flutter/material.dart';
1415

1516
void addRenderingStories(Dashbook dashbook) {
1617
dashbook.storiesOf('Rendering')
18+
..add(
19+
'Hue Decorator',
20+
(_) => GameWidget(game: HueDecoratorExample()),
21+
codeLink: baseLink('rendering/hue_decorator_example.dart'),
22+
info: HueDecoratorExample.description,
23+
)
1724
..add(
1825
'Text',
1926
(_) => GameWidget(game: TextExample()),

packages/flame/lib/effects.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export 'src/effects/effect.dart';
2525
export 'src/effects/effect_target.dart';
2626
export 'src/effects/function_effect.dart';
2727
export 'src/effects/glow_effect.dart';
28+
export 'src/effects/hue_effect.dart';
2829
export 'src/effects/move_along_path_effect.dart';
2930
export 'src/effects/move_by_effect.dart';
3031
export 'src/effects/move_effect.dart';

0 commit comments

Comments
 (0)