E5E7 feat: Add HueEffect and HueDecorator by s1r1m1r1 · Pull Request #3852 · flame-engine/flame · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/flame/effects/color_effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,44 @@ final effect = GlowEffect(
```

Currently this effect can only be applied to components that have a `HasPaint` mixin.


## `HueToEffect`

This effect will change the hue of the target over time to the specified angle in radians.
It can only be applied to components that implement the `HueProvider`.

```dart
final effect = HueEffect.to(
pi / 2,
EffectController(duration: 3),
);
```

## `HueByEffect`

This effect will rotate the hue of the target relative by the specified angle in radians.
It can only be applied to components that implement the `HueProvider`.

```{flutter-app}
:sources: ../flame/examples
:page: hue_effect
:show: widget code infobox
:width: 180
:height: 160
```

```dart
final effect = HueEffect.by(
2 * tau,
EffectController(duration: 3),
);
```

Both effects can target any component implementing `HueProvider`. The `HasPaint` mixin
implements `HueProvider` and handles the necessary `ColorFilter` updates automatically.

> [!TIP]
> **Performance Note**: `HueEffect` is extremely efficient because it modifies the `Paint`'s
> `colorFilter` directly. If you have many components, prefer this effect over the `HueDecorator`,
> which uses `saveLayer()` and has much higher overhead.
14 changes: 14 additions & 0 deletions doc/flame/effects/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ that property to a fixed value. This way multiple effects would be able to act o
without interfering with each other.


## Effects vs Decorators

While effects and decorators can sometimes achieve similar visual results (like changing opacity
or color), they have different performance and visual characteristics:

- **Effects** are fast and generally change a property on a single component. When applied to
a group, they affect each child individually.
- **Decorators** are more powerful but slower. They use `saveLayer` to flatten a whole
component subtree into a single layer before applying an effect. This is essential for
correctly rendering composite objects with transparency or complex filters.

See the [Decorators documentation](../rendering/decorators.md) for a more detailed comparison.


## See also

- [Examples of various effects](https://examples.flame-engine.org/).
Expand Down
32 changes: 32 additions & 0 deletions doc/flame/examples/lib/decorator_hue.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'dart:math';

import 'package:doc_flame_examples/flower.dart';
import 'package:flame/game.dart';
import 'package:flame/rendering.dart';

class DecoratorHueGame extends FlameGame {
@override
Future<void> onLoad() async {
var step = 0;
add(
Flower(
size: 100,
position: canvasSize / 2,
onTap: (flower) {
final decorator = flower.decorator;
step++;
if (step == 1) {
decorator.addLast(HueDecorator(hue: pi / 4));
} else if (step == 2) {
decorator.replaceLast(HueDecorator(hue: pi / 2));
} else if (step == 3) {
decorator.replaceLast(HueDecorator(hue: pi));
} else {
decorator.replaceLast(null);
step = 0;
}
},
)..onTapUp(),
);
}
}
26 changes: 26 additions & 0 deletions doc/flame/examples/lib/hue_effect.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'dart:math';

import 'package:doc_flame_examples/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';

class HueEffectExample extends FlameGame {
@override
Future<void> onLoad() async {
final ember = EmberPlayer(
position: size / 2,
size: size / 4,
onTap: (ember) {
ember.add(
HueEffect.by(
2 * pi,
EffectController(duration: 3),
),
);
},
)..anchor = Anchor.center;

add(ember);
}
}
6 changes: 5 additions & 1 deletion doc/flame/examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import 'package:doc_flame_examples/collision_detection.dart';
import 'package:doc_flame_examples/color_effect.dart';
import 'package:doc_flame_examples/decorator_blur.dart';
import 'package:doc_flame_examples/decorator_grayscale.dart';
import 'package:doc_flame_examples/decorator_hue.dart';
import 'package:doc_flame_examples/decorator_rotate3d.dart';
import 'package:doc_flame_examples/decorator_shadow3d.dart';
import 'package:doc_flame_examples/decorator_tint.dart';
import 'package:doc_flame_examples/drag_events.dart';
import 'package:doc_flame_examples/glow_effect.dart';
import 'package:doc_flame_examples/hue_effect.dart';
import 'package:doc_flame_examples/move_along_path_effect.dart';
import 'package:doc_flame_examples/move_by_effect.dart';
import 'package:doc_flame_examples/move_to_effect.dart';
Expand Down Expand Up @@ -39,18 +41,20 @@ import 'package:flutter/widgets.dart';
import 'package:web/web.dart' as web;

final routes = <String, Game Function()>{
'anchor': AnchorGame.new,
'anchor_by_effect': AnchorByEffectGame.new,
'anchor_to_effect': AnchorToEffectGame.new,
'anchor': AnchorGame.new,
'collision_detection': CollisionDetectionGame.new,
'color_effect': ColorEffectExample.new,
'decorator_blur': DecoratorBlurGame.new,
'decorator_grayscale': DecoratorGrayscaleGame.new,
'decorator_hue': DecoratorHueGame.new,
'decorator_rotate3d': DecoratorRotate3DGame.new,
'decorator_shadow3d': DecoratorShadowGame.new,
'decorator_tint': DecoratorTintGame.new,
'drag_events': DragEventsGame.new,
'glow_effect': GlowEffectExample.new,
'hue_effect': HueEffectExample.new,
'move_along_path_effect': MoveAlongPathEffectGame.new,
'move_by_effect': MoveByEffectGame.new,
'move_to_effect': MoveToEffectGame.new,
Expand Down
59 changes: 57 additions & 2 deletions doc/flame/rendering/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,38 @@ necessary. We are planning to add shader-based decorators once Flutter fully sup
web.


## Performance considerations

Applying a Decorator to a component can have a significant performance overhead, especially when
it involves `canvas.saveLayer()`.

- **Decorators**: Use `canvas.saveLayer()` by default to isolate rendering and apply
filters. This requires off-screen buffer allocation and GPU context switches. This is
computationally expensive but essential for correct visual composition of complex
objects (see below).
- **Effects** (e.g., `OpacityEffect`, `ColorEffect`): Modify the component's properties or `Paint` directly. These are extremely fast and hardware-accelerated, but they apply to each child individually.

### Decorators vs Effects: Visual Composition

The key difference lies in how they handle composite objects (components with multiple
overlapping children):

1. **Effects (Individual Blend)**: If you apply an `OpacityEffect` to a parent component,
Flame will render each child with that opacity. If children overlap, you will see
through them to the background and to other children, creating a "double-exposure"
look.
2. **Decorators (Group Blend)**: Because decorators use `saveLayer`, they render the
entire subtree into a flat buffer first, and then apply the effect to that
buffer. This results in a uniform appearance where overlaps are not visible,
making the group look like a single solid object.

**Recommendation**:
- Use **Effects** for simple property animations and high-performance color shifts on
large numbers of units.
- Use **Decorators** for advanced post-processing (blurs, tints) and when you need
to treat a group of components as a single visual unit.


## Flame built-in decorators


Expand Down Expand Up @@ -157,6 +189,29 @@ limitation is that the shadows are flat and cannot interact with the environment
decorator cannot handle shadows that fall onto walls or other vertical structures.


### HueDecorator

```{flutter-app}
:sources: ../flame/examples
:page: decorator_hue
:show: widget code infobox
:width: 180
:height: 160
```

This decorator shifts the hue of the underlying component by the specified angle in radians.

```dart
final decorator = HueDecorator(hue: tau / 4);
```

Possible uses:

- alternative color schemes for enemies ("palette swapping");
- environmental changes (e.g., world turning purple/surreal);
- power-up indicators.


## Using decorators


Expand All @@ -175,7 +230,7 @@ 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).
to be chained (see the [Multiple decorators](#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.
Expand All @@ -196,5 +251,5 @@ from its root, which usually is `component.decorator`.


[Component]: ../components/components.md#component
[Effect]: ../../flame/effects.md
[Effect]: ../effects/effects.md
[HasDecorator]: #hasdecorator-mixin
2 changes: 2 additions & 0 deletions examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:examples/stories/image/image.dart';
import 'package:examples/stories/input/input.dart';
import 'package:examples/stories/layout/layout.dart';
import 'package:examples/stories/parallax/parallax.dart';
import 'package:examples/stories/rendering/decorators.dart';
import 'package:examples/stories/rendering/rendering.dart';
import 'package:examples/stories/router/router.dart';
import 'package:examples/stories/sprites/sprites.dart';
Expand Down Expand Up @@ -83,6 +84,7 @@ void runAsDashbook() {
addCameraAndViewportStories(dashbook);
addCollisionDetectionStories(dashbook);
addComponentsStories(dashbook);
addDecoratorStories(dashbook);
addEffectsStories(dashbook);
addExperimentalStories(dashbook);
addInputStories(dashbook);
Expand Down
7 changes: 7 additions & 0 deletions examples/lib/stories/effects/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:examples/stories/effects/combined_effect_example.dart';
import 'package:examples/stories/effects/dual_effect_removal_example.dart';
import 'package:examples/stories/effects/effect_controllers_example.dart';
import 'package:examples/stories/effects/function_effect_example.dart';
import 'package:examples/stories/effects/hue_effect_example.dart';
import 'package:examples/stories/effects/move_effect_example.dart';
import 'package:examples/stories/effects/opacity_effect_example.dart';
import 'package:examples/stories/effects/remove_effect_example.dart';
Expand Down Expand Up @@ -59,6 +60,12 @@ void addEffectsStories(Dashbook dashbook) {
codeLink: baseLink('effects/opacity_effect_example.dart'),
info: OpacityEffectExample.description,
)
..add(
'Hue Effect',
(_) => GameWidget(game: HueEffectExample()),
codeLink: baseLink('effects/hue_effect_example.dart'),
info: HueEffectExample.description,
)
..add(
'Color Effect',
(_) => GameWidget(game: ColorEffectExample()),
Expand Down
30 changes: 30 additions & 0 deletions examples/lib/stories/effects/hue_effect_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:math';

import 'package:examples/commons/ember.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';

class HueEffectExample extends FlameGame {
static const String description = '''
In this example we show how the `HueEffect` can be used.
Ember will shift its hue over time.
''';

@override
Future<void> onLoad() async {
add(
Ember(
position: size / 2,
size: Vector2.all(100),
)..add(
HueEffect.by(
2 * pi,
EffectController(
duration: 3,
infinite: true,
),
),
),
);
}
}
54 changes: 54 additions & 0 deletions examples/lib/stories/rendering/decorator_hue_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'dart:math';

import 'package:examples/commons/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flame/rendering.dart';

class DecoratorHueExample extends FlameGame with TapCallbacks {
static const String description = '''
This example demonstrates the usage of `HueDecorator` to shift the
colors of a component.

Basi A373 c `HueDecorator` shifting the hue of an Ember component.

Click to cycle through hue shifts.
''';

late final HueDecorator decorator;
int step = 0;

@override
Future<void> onLoad() async {
decorator = HueDecorator();
world.add(
_buildItem('HueDecorator', decorator),
);
}

PositionComponent _buildItem(String title, HueDecorator decorator) {
return PositionComponent(
size: Vector2(150, 120),
children: [
Ember(
size: Vector2.all(80),
position: Vector2(75, 40),
),
TextComponent(
text: title,
position: Vector2(75, 100),
anchor: Anchor.center,
),
],
)..decorator.addLast(decorator);
}

@override
void onTapDown(TapDownEvent event) {
step++;
final hues = [0.0, pi / 4, pi / 2, pi, 0.0];
final hue = hues[step % hues.length];
decorator.hue = hue;
}
}
Loading
Loading
0