diff --git a/doc/flame/effects.md b/doc/flame/effects.md index 75d186b1b3c..130f6ec698a 100644 --- a/doc/flame/effects.md +++ b/doc/flame/effects.md @@ -82,17 +82,17 @@ The base `Effect` class is not usable on its own (it is abstract), but it provid functionality inherited by all other effects. This includes: - The ability to pause/resume the effect using `effect.pause()` and `effect.resume()`. You can - check whether the effect is currently paused using `effect.isPaused`. + check whether the effect is currently paused using `effect.isPaused`. - The ability to reverse the effect's time direction using `effect.reverse()`. Use - `effect.isReversed` to check if the effect is currently running back in time. + `effect.isReversed` to check if the effect is currently running back in time. - Property `removeOnFinish` (which is true by default) will cause the effect component to be - removed from the game tree and garbage-collected once the effect completes. Set this to false - if you plan to reuse the effect after it is finished. + removed from the game tree and garbage-collected once the effect completes. Set this to false + if you plan to reuse the effect after it is finished. - Optional user-provided `onComplete`, which will be invoked when the effect has just - completed its execution but before it is removed from the game. + completed its execution but before it is removed from the game. - The `reset()` method reverts the effect to its original state, allowing it to run once again. @@ -431,6 +431,30 @@ Currently this effect can only be applied to components that have a `HasPaint` m uses multiple paints, the effect can target any individual color using the `paintId` parameter. +### GlowEffect + +This effect will apply the glowing shade around target relative to the specified +`glow-strength`. The color of shade will be targets paint color. For example, the following effect +will apply the glowing shade around target by strength of `10`: + +```{flutter-app} +:sources: ../flame/examples +:page: glow_effect +:show: widget code infobox +:width: 180 +:height: 160 +``` + +```dart +final effect = GlowEffect( + 10.0, + EffectController(duration: 3), +); +``` + +Currently this effect can only be applied to components that have a `HasPaint` mixin. + + ### `SequenceEffect` This effect can be used to run multiple other effects one after another. The constituent effects diff --git a/doc/flame/examples/lib/glow_effect.dart b/doc/flame/examples/lib/glow_effect.dart new file mode 100644 index 00000000000..7ec5fdd05df --- /dev/null +++ b/doc/flame/examples/lib/glow_effect.dart @@ -0,0 +1,29 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; + +import 'package:flutter/material.dart'; + +class GlowEffectExample extends FlameGame { + @override + Future onLoad() async { + final paint = Paint()..color = const Color(0xff39FF14); + + add( + CircleComponent( + radius: size.y / 4, + position: size / 2, + anchor: Anchor.center, + paint: paint, + )..add( + GlowEffect( + 10.0, + EffectController( + duration: 2, + infinite: true, + ), + ), + ), + ); + } +} diff --git a/doc/flame/examples/lib/main.dart b/doc/flame/examples/lib/main.dart index 015eab70557..482795ed73d 100644 --- a/doc/flame/examples/lib/main.dart +++ b/doc/flame/examples/lib/main.dart @@ -10,6 +10,7 @@ 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/move_along_path_effect.dart'; import 'package:doc_flame_examples/move_by_effect.dart'; import 'package:doc_flame_examples/move_to_effect.dart'; @@ -67,6 +68,7 @@ void main() { 'rive_example': RiveExampleGame.new, 'ray_cast': RayCastExample.new, 'ray_trace': RayTraceExample.new, + 'glow_effect': GlowEffectExample.new, 'remove_effect': RemoveEffectGame.new, 'color_effect': ColorEffectExample.new, }; diff --git a/examples/lib/stories/effects/glow_effect_example.dart b/examples/lib/stories/effects/glow_effect_example.dart new file mode 100644 index 00000000000..f66b6201a9d --- /dev/null +++ b/examples/lib/stories/effects/glow_effect_example.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(GameWidget(game: GlowEffectExample())); +} + +class GlowEffectExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how the `GlowEffect` can be used. + '''; + + @override + Future onLoad() async { + final paint = Paint() + ..color = const Color(0xff39FF14) + ..style = PaintingStyle.stroke; + + add( + CircleComponent( + radius: 50, + position: Vector2(300, 400), + paint: paint, + )..add( + GlowEffect( + 10.0, + EffectController( + duration: 3, + reverseDuration: 1.5, + infinite: true, + ), + ), + ), + ); + } +} diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 3abf214f13e..4b12306ccef 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -21,6 +21,7 @@ export 'src/effects/controllers/speed_effect_controller.dart'; export 'src/effects/controllers/zigzag_effect_controller.dart'; export 'src/effects/effect.dart'; export 'src/effects/effect_target.dart'; +export 'src/effects/glow_effect.dart'; export 'src/effects/move_along_path_effect.dart'; export 'src/effects/move_by_effect.dart'; export 'src/effects/move_effect.dart'; diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index 295e9e9a89b..ccb8f46b870 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; +import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:flame/src/palette.dart'; import 'package:meta/meta.dart'; @@ -14,8 +15,11 @@ import 'package:meta/meta.dart'; /// [T], that can be omitted if the component only has one paint. /// [paintLayers] paints should be drawn in list order during the render. The /// main Paint is the first element. -mixin HasPaint on Component implements OpacityProvider { +mixin HasPaint on Component + implements OpacityProvider, PaintProvider { late final Map _paints = {}; + + @override Paint paint = BasicPalette.white.paint(); @internal diff --git a/packages/flame/lib/src/effects/glow_effect.dart b/packages/flame/lib/src/effects/glow_effect.dart new file mode 100644 index 00000000000..672025ea5ed --- /dev/null +++ b/packages/flame/lib/src/effects/glow_effect.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import 'package:flame/effects.dart'; +import 'package:flame/src/effects/provider_interfaces.dart'; + +/// Change the MaskFilter on Paint of a component over time. +/// +/// This effect applies incremental changes to the MaskFilter on Paint of a +/// component and requires that any other effect or update logic applied to the +/// same component also used incremental updates. +class GlowEffect extends Effect with EffectTarget { + GlowEffect(this.strength, super.controller, {this.style = BlurStyle.outer}); + + final BlurStyle style; + final double strength; + + @override + void apply(double progress) { + final _value = strength * progress; + + target.paint.maskFilter = MaskFilter.blur(style, _value); + } + + @override + void reset() { + super.reset(); + target.paint.maskFilter = null; + } +} diff --git a/packages/flame/lib/src/effects/provider_interfaces.dart b/packages/flame/lib/src/effects/provider_interfaces.dart index 031b70a1d10..74a076fe361 100644 --- a/packages/flame/lib/src/effects/provider_interfaces.dart +++ b/packages/flame/lib/src/effects/provider_interfaces.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flame/components.dart'; /// Interface for a component that can be affected by move effects. @@ -64,3 +66,11 @@ abstract class OpacityProvider { double get opacity; set opacity(double value); } + +/// Interface for a component that can be affected by Paint effects. +/// +/// See [HasPaint] for an example implementation. +abstract class PaintProvider { + Paint get paint; + set paint(Paint value); +} diff --git a/packages/flame/test/effects/glow_effect.dart b/packages/flame/test/effects/glow_effect.dart new file mode 100644 index 00000000000..78717a3bb2c --- /dev/null +++ b/packages/flame/test/effects/glow_effect.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('GlowEffect', () { + testWithFlameGame('can apply to component having HasPaint', (game) async { + final component = _PaintComponent(); + + await game.ensureAdd(component); + + await component.add( + GlowEffect(1, EffectController(duration: 1)), + ); + + game.update(0); + + expect(component.children.length, 1); + expect(component.paint.maskFilter, isNotNull); + + expect( + component.paint.maskFilter.toString(), + 'MaskFilter.blur(BlurStyle.outer, 0.0)', + ); + + game.update(1); + + expect( + component.paint.maskFilter.toString(), + 'MaskFilter.blur(BlurStyle.outer, 1.0)', + ); + }); + }); +} + +class _PaintComponent extends Component with HasPaint {}