Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/flame/lib/src/components/text_box_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
line = line.substring(0, nChars);
}

final textElement = textRenderer.format(line);
final textElement = paintedTextRenderer.format(line);
final metrics = textElement.metrics;

final position = Vector2(
Expand Down
30 changes: 28 additions & 2 deletions packages/flame/lib/src/components/text_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class TextComponent<T extends TextRenderer> extends PositionComponent
super.key,
}) : _text = text ?? '',
_textRenderer = textRenderer ?? TextRendererFactory.createDefault<T>() {
_paintedTextRenderer = _textRenderer;
_seedPaintColor();
updateBounds();
}

Expand All @@ -32,17 +34,41 @@ class TextComponent<T extends TextRenderer> 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);
}

Expand All @@ -60,7 +86,7 @@ class TextComponent<T extends TextRenderer> extends PositionComponent

@override
void onChanged() {
_textRenderer = _textRenderer.copyWithPaint(paint) as T;
_paintedTextRenderer = _textRenderer.copyWithPaint(paint) as T;
_updateElement();
}
}
12 changes: 12 additions & 0 deletions packages/flame/lib/src/text/renderers/text_paint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
},
);
Expand Down
69 changes: 68 additions & 1 deletion packages/flame/test/components/text_component_test.dart
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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(
Expand Down
Loading