From 5d4bade3cf945d959d30afd7b91020723954a68a Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 1 Jul 2026 17:53:01 -0300 Subject: [PATCH 1/6] feat: OpacityEffect support on text components --- examples/lib/stories/rendering/text_example.dart | 15 +++++++++++++++ .../flame/lib/src/components/text_component.dart | 14 +++++++++++++- .../src/text/renderers/sprite_font_renderer.dart | 10 ++++++++++ .../flame/lib/src/text/renderers/text_paint.dart | 14 ++++++++++++++ .../lib/src/text/renderers/text_renderer.dart | 4 ++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/examples/lib/stories/rendering/text_example.dart b/examples/lib/stories/rendering/text_example.dart index 41fc17aab7c..ca54c00e082 100644 --- a/examples/lib/stories/rendering/text_example.dart +++ b/examples/lib/stories/rendering/text_example.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flame/palette.dart'; import 'package:flame/text.dart'; @@ -82,6 +83,20 @@ class TextExample extends FlameGame { margins: EdgeInsets.fromLTRB(10, 10, 10, 10), ), ), + TextComponent( + text: 'I fade in and fade out', + anchor: Anchor.topRight, + position: Vector2(size.x - 50, 20), + children: [ + SequenceEffect( + [ + OpacityEffect.fadeIn(LinearEffectController(1.5)), + OpacityEffect.fadeOut(LinearEffectController(1.5)), + ], + infinite: true, + ), + ], + ), ], ); } diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index 3ac38b8dfcb..50e859993c5 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -1,11 +1,13 @@ import 'dart:ui'; import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/text.dart'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; -class TextComponent extends PositionComponent { +class TextComponent extends PositionComponent + implements OpacityProvider { TextComponent({ String? text, T? textRenderer, @@ -52,4 +54,14 @@ class TextComponent extends PositionComponent { void render(Canvas canvas) { _textElement.draw(canvas); } + + @override + set opacity(double opacity) { + textRenderer = textRenderer.copyWithOpacity(opacity) as T; + } + + @override + double get opacity { + return textRenderer.opacity; + } } diff --git a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart index a04a0f913cb..ab9e63b6b82 100644 --- a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart @@ -56,4 +56,14 @@ class SpriteFontRenderer extends TextRenderer { ), ); } + + @override + TextRenderer copyWithOpacity(double opacity) { + // TODO(erickzanardo): Implement this, keeping as noop for now + return this; + } + + @override + // TODO(erickzanardo): Implement this, keeping as noop for now + double get opacity => 1; } diff --git a/packages/flame/lib/src/text/renderers/text_paint.dart b/packages/flame/lib/src/text/renderers/text_paint.dart index c9b00070305..0c3b2e9d632 100644 --- a/packages/flame/lib/src/text/renderers/text_paint.dart +++ b/packages/flame/lib/src/text/renderers/text_paint.dart @@ -54,6 +54,17 @@ class TextPaint extends TextRenderer { return _textPainterCache.getValue(text)!; } + @override + TextRenderer copyWithOpacity(double opacity) { + return copyWith( + (style) { + return style.copyWith( + color: style.color?.withValues(alpha: opacity), + ); + }, + ); + } + TextPaint copyWith( TextStyle Function(TextStyle) transform, { TextDirection? textDirection, @@ -96,4 +107,7 @@ class TextPaint extends TextRenderer { } return null; } + + @override + double get opacity => style.color?.a ?? 0; } diff --git a/packages/flame/lib/src/text/renderers/text_renderer.dart b/packages/flame/lib/src/text/renderers/text_renderer.dart index edb295ab0f6..b5272c7510b 100644 --- a/packages/flame/lib/src/text/renderers/text_renderer.dart +++ b/packages/flame/lib/src/text/renderers/text_renderer.dart @@ -11,6 +11,10 @@ abstract class TextRenderer { return format(text).metrics; } + TextRenderer copyWithOpacity(double opacity); + + double get opacity; + void render( Canvas canvas, String text, From 538d73950bc8458fdda1381993b7226db586975c Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 2 Jul 2026 11:14:52 -0300 Subject: [PATCH 2/6] using HasPaint instead --- .../lib/stories/rendering/text_example.dart | 21 +++++++++++++++++-- .../lib/src/components/mixins/has_paint.dart | 10 +++++++++ .../lib/src/components/text_component.dart | 19 ++++++++--------- .../text/renderers/sprite_font_renderer.dart | 8 ++----- .../lib/src/text/renderers/text_paint.dart | 7 ++----- .../lib/src/text/renderers/text_renderer.dart | 6 +++--- 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/examples/lib/stories/rendering/text_example.dart b/examples/lib/stories/rendering/text_example.dart index ca54c00e082..6f3504fc519 100644 --- a/examples/lib/stories/rendering/text_example.dart +++ b/examples/lib/stories/rendering/text_example.dart @@ -90,13 +90,30 @@ class TextExample extends FlameGame { children: [ SequenceEffect( [ - OpacityEffect.fadeIn(LinearEffectController(1.5)), - OpacityEffect.fadeOut(LinearEffectController(1.5)), + OpacityEffect.fadeOut( + LinearEffectController(1.5), + ), + OpacityEffect.fadeIn( + LinearEffectController(1.5), + ), ], infinite: true, ), ], ), + TextComponent( + text: 'I change my color!', + anchor: Anchor.topRight, + position: Vector2(size.x - 50, 50), + children: [ + ColorEffect( + Colors.blue, + InfiniteEffectController( + LinearEffectController(1.5), + ), + ), + ], + ), ], ); } diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index 48a0ce81744..a3704ed9033 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -113,6 +113,7 @@ mixin HasPaint on Component /// Shortcut for changing the color of the paint. void setColor(Color color, {T? paintId}) { getPaint(paintId).color = color; + onChanged(); } /// Applies a color filter to the paint which will make @@ -120,6 +121,7 @@ mixin HasPaint on Component /// tinted with the given color. void tint(Color color, {T? paintId}) { getPaint(paintId).colorFilter = ColorFilter.mode(color, BlendMode.srcATop); + onChanged(); } @override @@ -131,6 +133,7 @@ mixin HasPaint on Component for (final paint in _paints.values) { paint.color = paint.color.withValues(alpha: value); } + onChanged(); } @override @@ -158,6 +161,7 @@ mixin HasPaint on Component layerPaint.colorFilter = filter; } } + onChanged(); } /// Creates an [OpacityProvider] for given [paintId] and can be used as @@ -189,6 +193,12 @@ mixin HasPaint on Component includeLayers: includeLayers, ); } + + /// Can be overriden to react when the [paint] object + /// changed! + /// + /// Default implementation is a no-op. + void onChanged() {} } class _ProxyOpacityProvider implements OpacityProvider { diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index 50e859993c5..30a9bb5760f 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -1,13 +1,12 @@ import 'dart:ui'; import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; import 'package:flame/text.dart'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; class TextComponent extends PositionComponent - implements OpacityProvider { + with HasPaint { TextComponent({ String? text, T? textRenderer, @@ -42,9 +41,13 @@ class TextComponent extends PositionComponent late InlineTextElement _textElement; + void _updateElement() { + _textElement = _textRenderer.format(_text); + } + @internal void updateBounds() { - _textElement = _textRenderer.format(_text); + _updateElement(); final measurements = _textElement.metrics; _textElement.translate(0, measurements.ascent); size.setValues(measurements.width, measurements.height); @@ -56,12 +59,8 @@ class TextComponent extends PositionComponent } @override - set opacity(double opacity) { - textRenderer = textRenderer.copyWithOpacity(opacity) as T; - } - - @override - double get opacity { - return textRenderer.opacity; + void onChanged() { + _textRenderer = _textRenderer.copyWithPaint(paint) as T; + _updateElement(); } } diff --git a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart index ab9e63b6b82..9774a597fed 100644 --- a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart @@ -58,12 +58,8 @@ class SpriteFontRenderer extends TextRenderer { } @override - TextRenderer copyWithOpacity(double opacity) { - // TODO(erickzanardo): Implement this, keeping as noop for now + TextRenderer copyWithPaint(Paint paint) { + // TODO: implement copyWithPaint return this; } - - @override - // TODO(erickzanardo): Implement this, keeping as noop for now - double get opacity => 1; } diff --git a/packages/flame/lib/src/text/renderers/text_paint.dart b/packages/flame/lib/src/text/renderers/text_paint.dart index 0c3b2e9d632..1202523cd83 100644 --- a/packages/flame/lib/src/text/renderers/text_paint.dart +++ b/packages/flame/lib/src/text/renderers/text_paint.dart @@ -55,11 +55,11 @@ class TextPaint extends TextRenderer { } @override - TextRenderer copyWithOpacity(double opacity) { + TextRenderer copyWithPaint(Paint paint) { return copyWith( (style) { return style.copyWith( - color: style.color?.withValues(alpha: opacity), + foreground: paint, ); }, ); @@ -107,7 +107,4 @@ class TextPaint extends TextRenderer { } return null; } - - @override - double get opacity => style.color?.a ?? 0; } diff --git a/packages/flame/lib/src/text/renderers/text_renderer.dart b/packages/flame/lib/src/text/renderers/text_renderer.dart index b5272c7510b..1d442de8757 100644 --- a/packages/flame/lib/src/text/renderers/text_renderer.dart +++ b/packages/flame/lib/src/text/renderers/text_renderer.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flame/extensions.dart'; import 'package:flame/src/anchor.dart'; import 'package:flame/text.dart'; @@ -11,9 +13,7 @@ abstract class TextRenderer { return format(text).metrics; } - TextRenderer copyWithOpacity(double opacity); - - double get opacity; + TextRenderer copyWithPaint(Paint paint); void render( Canvas canvas, From fb8a29a642efc34194b631146c5265db0a1d1aea Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 2 Jul 2026 11:32:18 -0300 Subject: [PATCH 3/6] fixing compilation issues --- packages/flame_test/lib/src/debug_text_renderer.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/flame_test/lib/src/debug_text_renderer.dart b/packages/flame_test/lib/src/debug_text_renderer.dart index a5be123710e..c04f4aa9c54 100644 --- a/packages/flame_test/lib/src/debug_text_renderer.dart +++ b/packages/flame_test/lib/src/debug_text_renderer.dart @@ -25,6 +25,17 @@ class DebugTextRenderer extends TextRenderer { @override InlineTextElement format(String text) => _DebugTextElement(this, text); + + @override + TextRenderer copyWithPaint(Paint paint) { + return DebugTextRenderer( + color: paint.color, + fontSize: fontSize, + lineHeight: lineHeight, + fontWeight: fontWeight, + fontStyle: fontStyle, + ); + } } class _DebugTextElement extends InlineTextElement { From 0bd176639e2d6e2a5c18b37196c27e8b28510eec Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 2 Jul 2026 11:59:03 -0300 Subject: [PATCH 4/6] CI fixes --- examples/lib/stories/rendering/text_example.dart | 16 ++++++++-------- .../lib/src/components/mixins/has_paint.dart | 2 +- packages/flame/test/text/text_renderer_test.dart | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/lib/stories/rendering/text_example.dart b/examples/lib/stories/rendering/text_example.dart index 6f3504fc519..074a26e19ac 100644 --- a/examples/lib/stories/rendering/text_example.dart +++ b/examples/lib/stories/rendering/text_example.dart @@ -146,7 +146,7 @@ final _shaded = TextPaint( ); class MyTextBox extends TextBoxComponent { - late Paint paint; + late Paint bgPaint; late Rect bgRect; MyTextBox( @@ -168,25 +168,25 @@ class MyTextBox extends TextBoxComponent { @override Future onLoad() { - paint = Paint(); + bgPaint = Paint(); bgRect = Rect.fromLTWH(0, 0, width, height); size.addListener(() { bgRect = Rect.fromLTWH(0, 0, width, height); }); - paint.color = Colors.white10; + bgPaint.color = Colors.white10; return super.onLoad(); } @override void render(Canvas canvas) { - canvas.drawRect(bgRect, paint); + canvas.drawRect(bgRect, bgPaint); super.render(canvas); } } class MyScrollTextBox extends ScrollTextBoxComponent { - late Paint paint; + late Paint bgPaint; late Rect backgroundRect; MyScrollTextBox( @@ -199,16 +199,16 @@ class MyScrollTextBox extends ScrollTextBoxComponent { @override FutureOr onLoad() { - paint = Paint(); + bgPaint = Paint(); backgroundRect = Rect.fromLTWH(0, 0, width, height); - paint.color = Colors.white10; + bgPaint.color = Colors.white10; return super.onLoad(); } @override void render(Canvas canvas) { - canvas.drawRect(backgroundRect, paint); + canvas.drawRect(backgroundRect, bgPaint); super.render(canvas); } } diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index a3704ed9033..5942900ff48 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -194,7 +194,7 @@ mixin HasPaint on Component ); } - /// Can be overriden to react when the [paint] object + /// Can be overridden to react when the [paint] object /// changed! /// /// Default implementation is a no-op. diff --git a/packages/flame/test/text/text_renderer_test.dart b/packages/flame/test/text/text_renderer_test.dart index 32c9e5e1ce7..3182c5f4318 100644 --- a/packages/flame/test/text/text_renderer_test.dart +++ b/packages/flame/test/text/text_renderer_test.dart @@ -37,6 +37,11 @@ class _CustomTextRenderer extends TextRenderer { InlineTextElement format(String text) { return _CustomTextElement(); } + + @override + TextRenderer copyWithPaint(Paint paint) { + return _CustomTextRenderer(); + } } class _CustomTextElement extends InlineTextElement { From cdaa8e562f6815db5cafce6fb420f737a3fa5301 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 2 Jul 2026 13:35:29 -0300 Subject: [PATCH 5/6] implementing missing method on SpriteFontRenderer --- .../src/text/renderers/sprite_font_renderer.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart index 9774a597fed..0f3b127d199 100644 --- a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart @@ -17,6 +17,13 @@ class SpriteFontRenderer extends TextRenderer { } } + SpriteFontRenderer.fromPaint( + this.font, { + required this.paint, + this.scale = 1.0, + this.letterSpacing = 0.0, + }); + final SpriteFont font; final double scale; final double letterSpacing; @@ -59,7 +66,9 @@ class SpriteFontRenderer extends TextRenderer { @override TextRenderer copyWithPaint(Paint paint) { - // TODO: implement copyWithPaint - return this; + return SpriteFontRenderer.fromPaint( + font, + paint: paint, + ); } } From 6fe63142f96e37bf6adf6b65058133765aee2a5d Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 2 Jul 2026 17:44:53 -0300 Subject: [PATCH 6/6] Apply suggestion from @erickzanardo --- packages/flame/lib/src/components/mixins/has_paint.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index 5942900ff48..a33bd4c1fed 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -195,7 +195,7 @@ mixin HasPaint on Component } /// Can be overridden to react when the [paint] object - /// changed! + /// changed. /// /// Default implementation is a no-op. void onChanged() {}