diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index 519f0003083..f098fb752ab 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -401,7 +401,7 @@ class TextBoxComponent extends TextComponent { line = line.substring(0, nChars); } - final textElement = textRenderer.format(line); + final textElement = paintedTextRenderer.format(line); final metrics = textElement.metrics; final position = Vector2( diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index dcb46eeacab..2d5caa77945 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -20,6 +20,8 @@ class TextComponent extends PositionComponent super.key, }) : _text = text ?? '', _textRenderer = textRenderer ?? TextRendererFactory.createDefault() { + _paintedTextRenderer = _textRenderer; + _seedPaintColor(); updateBounds(); } @@ -32,17 +34,41 @@ class TextComponent extends PositionComponent } } + /// The renderer as given by the user. Setting it also starts [paint] from + /// the style's own color, see [paintedTextRenderer]. T get textRenderer => _textRenderer; T _textRenderer; set textRenderer(T textRenderer) { _textRenderer = textRenderer; + _paintedTextRenderer = textRenderer; + _seedPaintColor(); updateBounds(); } + /// [textRenderer] with the component's [paint] applied, which is what the + /// text is drawn with. It is derived from [textRenderer] again on every + /// paint change, so the paint never compounds across changes. + @internal + T get paintedTextRenderer => _paintedTextRenderer; + late T _paintedTextRenderer; + + /// Starts [paint] from the style's own color, so that the first paint change + /// (for example an `OpacityEffect`) fades the text in its color instead of + /// replacing it with the default white paint. + void _seedPaintColor() { + final renderer = _textRenderer; + if (renderer is TextPaint) { + final color = renderer.style.color; + if (color != null) { + paint.color = color; + } + } + } + late InlineTextElement _textElement; void _updateElement() { - _textElement = _textRenderer.format(_text); + _textElement = _paintedTextRenderer.format(_text); _textElement.translate(0, _textElement.metrics.ascent); } @@ -60,7 +86,7 @@ class TextComponent extends PositionComponent @override void onChanged() { - _textRenderer = _textRenderer.copyWithPaint(paint) as T; + _paintedTextRenderer = _textRenderer.copyWithPaint(paint) as T; _updateElement(); } } diff --git a/packages/flame/lib/src/text/renderers/text_paint.dart b/packages/flame/lib/src/text/renderers/text_paint.dart index 1202523cd83..afa75090018 100644 --- a/packages/flame/lib/src/text/renderers/text_paint.dart +++ b/packages/flame/lib/src/text/renderers/text_paint.dart @@ -58,8 +58,20 @@ class TextPaint extends TextRenderer { TextRenderer copyWithPaint(Paint paint) { return copyWith( (style) { + final shadows = style.shadows; return style.copyWith( foreground: paint, + shadows: shadows + ?.map( + (shadow) => Shadow( + color: shadow.color.withValues( + alpha: shadow.color.a * paint.color.a, + ), + offset: shadow.offset, + blurRadius: shadow.blurRadius, + ), + ) + .toList(), ); }, ); diff --git a/packages/flame/test/components/text_component_test.dart b/packages/flame/test/components/text_component_test.dart index 222ddccdefb..03a51695aff 100644 --- a/packages/flame/test/components/text_component_test.dart +++ b/packages/flame/test/components/text_component_test.dart @@ -1,6 +1,8 @@ import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/text.dart'; -import 'package:flutter/painting.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/rendering.dart'; import 'package:test/test.dart'; void main() { @@ -10,6 +12,71 @@ void main() { expect(t.size, isNot(equals(Vector2.zero()))); }); + testWithFlameGame( + 'fades the text in its own color and the shadows with it under an ' + 'OpacityEffect', + (game) async { + const color = Color(0xFF2E9940); + const shadowColor = Color(0x99FFFFFF); + final component = TextComponent( + text: '+1 kr', + textRenderer: TextPaint( + style: const TextStyle( + color: color, + shadows: [Shadow(color: shadowColor, blurRadius: 4)], + ), + ), + ); + await game.ensureAdd(component); + component.add(OpacityEffect.fadeOut(EffectController(duration: 1))); + + game.update(0.5); + var style = component.paintedTextRenderer.style; + final fill = style.foreground!.color; + expect(fill.toARGB32() & 0xFFFFFF, color.toARGB32() & 0xFFFFFF); + expectDouble(fill.a, 0.5, epsilon: 0.05); + var shadow = style.shadows!.single; + expect( + shadow.color.toARGB32() & 0xFFFFFF, + shadowColor.toARGB32() & 0xFFFFFF, + ); + expectDouble(shadow.color.a, shadowColor.a * 0.5, epsilon: 0.05); + + game.update(0.5); + style = component.paintedTextRenderer.style; + expectDouble(style.foreground!.color.a, 0.0, epsilon: 0.05); + shadow = style.shadows!.single; + expectDouble(shadow.color.a, 0.0, epsilon: 0.05); + }, + ); + + test('applies the paint to the renderer the user set, not to a copy', () { + const color = Color(0xFF2E9940); + const shadowColor = Color(0x99FFFFFF); + final component = TextComponent( + text: '+1 kr', + textRenderer: TextPaint( + style: const TextStyle( + color: color, + shadows: [Shadow(color: shadowColor, blurRadius: 4)], + ), + ), + ); + + component.setOpacity(0.5); + component.textRenderer = component.textRenderer.copyWith( + (style) => style.copyWith(fontSize: 30), + ); + component.setOpacity(0.25); + + expect(component.textRenderer.style.color, color); + expect(component.textRenderer.style.shadows!.single.color, shadowColor); + final painted = component.paintedTextRenderer.style; + expect(painted.fontSize, 30); + expectDouble(painted.foreground!.color.a, 0.25); + expectDouble(painted.shadows!.single.color.a, shadowColor.a * 0.25); + }); + test('keeps the glyphs translated by the ascent after a paint change', () { final elements = <_RecordingTextElement>[]; final component = TextComponent(