mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
Abstracting text API to enable custom renderers (#772)
* Abstracting text API to enable custom renderers * Addressing comments * Lint * Update doc/text.md Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com> * Adding dartdoc about TextRenderer Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
37
doc/text.md
37
doc/text.md
@ -2,15 +2,28 @@
|
|||||||
|
|
||||||
Flame has some dedicated classes to help you render text.
|
Flame has some dedicated classes to help you render text.
|
||||||
|
|
||||||
## TextConfig
|
## TextRenderer
|
||||||
|
|
||||||
A Text Config contains all typographical information required to render text; i.e., font size and
|
`TextRenderer` is the abstract class used by Flame to render text. Flame provides one
|
||||||
color, family, etc.
|
implementation for this called `TextPaint` but anyone can implement this abstraction
|
||||||
|
and create a custom way to render text.
|
||||||
|
|
||||||
|
## TextPaint
|
||||||
|
|
||||||
|
A Text Paint is the built in implementation of text rendering on Flame, it is based on top of
|
||||||
|
Flutter's `TextPainter` class (hence the name), it can be configured by its config class
|
||||||
|
`TextPaintConfig` which contains all typographical information required to render text; i.e., font
|
||||||
|
size and color, family, etc.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font');
|
const TextPaint textPaint = TextPaint(
|
||||||
|
config: TextPaintConfig(
|
||||||
|
fontSize: 48.0,
|
||||||
|
fontFamily: 'Awesome Font',
|
||||||
|
),
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
- `fontFamily`: a commonly available font, like Arial (default), or a custom font added in your
|
- `fontFamily`: a commonly available font, like Arial (default), or a custom font added in your
|
||||||
@ -22,17 +35,17 @@ const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font')
|
|||||||
For more information regarding colors and how to create then, see the
|
For more information regarding colors and how to create then, see the
|
||||||
[Colors and the Palette](palette.md) guide.
|
[Colors and the Palette](palette.md) guide.
|
||||||
|
|
||||||
After the creation of the config you can use its `render` method to draw some string on a canvas:
|
After the creation of the text paint you can use its `render` method to draw some string on a canvas:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
config.render(canvas, "Flame is awesome", Position(10, 10));
|
textPaint.render(canvas, "Flame is awesome", Vector2(10, 10));
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to set the anchor of the text you can also do that in the render call, with the optional
|
If you want to set the anchor of the text you can also do that in the render call, with the optional
|
||||||
`anchor` parameter:
|
`anchor` parameter:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
config.render(canvas, 'Flame is awesome', Vector2(10, 10), anchor: Anchor.topCenter);
|
textPaint.render(canvas, 'Flame is awesome', Vector2(10, 10), anchor: Anchor.topCenter);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Text Components
|
## Text Components
|
||||||
@ -47,12 +60,12 @@ Flame provides two text components that make it even easier to render text in yo
|
|||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
TextConfig regular = TextConfig(color: BasicPalette.white.color);
|
TextPaint regular = TextPaint(color: BasicPalette.white.color);
|
||||||
|
|
||||||
class MyGame extends BaseGame {
|
class MyGame extends BaseGame {
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
add(TextComponent('Hello, Flame', config: regular)
|
add(TextComponent('Hello, Flame', textRenderer: regular)
|
||||||
..anchor = Anchor.topCenter
|
..anchor = Anchor.topCenter
|
||||||
..x = size.width / 2 // size is a property from game
|
..x = size.width / 2 // size is a property from game
|
||||||
..y = 32.0);
|
..y = 32.0);
|
||||||
@ -74,15 +87,15 @@ Example usage:
|
|||||||
|
|
||||||
```dart
|
```dart
|
||||||
class MyTextBox extends TextBoxComponent {
|
class MyTextBox extends TextBoxComponent {
|
||||||
MyTextBox(String text) : super(text, config: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05));
|
MyTextBox(String text) : super(text, textRenderer: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void drawBackground(Canvas c) {
|
void drawBackground(Canvas c) {
|
||||||
Rect rect = Rect.fromLTWH(0, 0, width, height);
|
Rect rect = Rect.fromLTWH(0, 0, width, height);
|
||||||
c.drawRect(rect, new Paint()..color = Color(0xFFFF00FF));
|
c.drawRect(rect, Paint()..color = Color(0xFFFF00FF));
|
||||||
c.drawRect(
|
c.drawRect(
|
||||||
rect.deflate(boxConfig.margin),
|
rect.deflate(boxConfig.margin),
|
||||||
new Paint()
|
Paint()
|
||||||
..color = BasicPalette.black.color
|
..color = BasicPalette.black.color
|
||||||
..style = PaintingStyle.stroke);
|
..style = PaintingStyle.stroke);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,11 @@ final R = Random();
|
|||||||
class MovableSquare extends SquareComponent
|
class MovableSquare extends SquareComponent
|
||||||
with Hitbox, Collidable, HasGameRef<CameraAndViewportGame> {
|
with Hitbox, Collidable, HasGameRef<CameraAndViewportGame> {
|
||||||
static const double speed = 300;
|
static const double speed = 300;
|
||||||
static final TextConfig config = TextConfig(fontSize: 12);
|
static final TextPaint textRenderer = TextPaint(
|
||||||
|
config: const TextPaintConfig(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Vector2 velocity = Vector2.zero();
|
final Vector2 velocity = Vector2.zero();
|
||||||
late Timer timer;
|
late Timer timer;
|
||||||
@ -41,7 +45,7 @@ class MovableSquare extends SquareComponent
|
|||||||
void render(Canvas c) {
|
void render(Canvas c) {
|
||||||
super.render(c);
|
super.render(c);
|
||||||
final text = '(${x.toInt()}, ${y.toInt()})';
|
final text = '(${x.toInt()}, ${y.toInt()})';
|
||||||
config.render(c, text, size / 2, anchor: Anchor.center);
|
textRenderer.render(c, text, size / 2, anchor: Anchor.center);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -167,8 +167,10 @@ class CollidableSnowman extends MyCollidable {
|
|||||||
|
|
||||||
class MultipleShapes extends BaseGame
|
class MultipleShapes extends BaseGame
|
||||||
with HasCollidables, HasDraggableComponents {
|
with HasCollidables, HasDraggableComponents {
|
||||||
final TextConfig fpsTextConfig = TextConfig(
|
final TextPaint fpsTextPaint = TextPaint(
|
||||||
|
config: TextPaintConfig(
|
||||||
color: BasicPalette.white.color,
|
color: BasicPalette.white.color,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -229,7 +231,7 @@ class MultipleShapes extends BaseGame
|
|||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
fpsTextConfig.render(
|
fpsTextPaint.render(
|
||||||
canvas,
|
canvas,
|
||||||
'${fps(120).toStringAsFixed(2)}fps',
|
'${fps(120).toStringAsFixed(2)}fps',
|
||||||
Vector2(0, size.y - 24),
|
Vector2(0, size.y - 24),
|
||||||
|
|||||||
@ -34,7 +34,11 @@ class LogoCompomnent extends SpriteComponent with HasGameRef<DebugGame> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DebugGame extends BaseGame {
|
class DebugGame extends BaseGame {
|
||||||
static final fpsTextConfig = TextConfig(color: const Color(0xFFFFFFFF));
|
static final fpsTextPaint = TextPaint(
|
||||||
|
config: const TextPaintConfig(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool debugMode = true;
|
bool debugMode = true;
|
||||||
@ -67,7 +71,7 @@ class DebugGame extends BaseGame {
|
|||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
fpsTextConfig.render(canvas, fps(120).toString(), Vector2(0, 50));
|
fpsTextPaint.render(canvas, fps(120).toString(), Vector2(0, 50));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import 'package:flame/game.dart';
|
|||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
final _regular = TextConfig(color: BasicPalette.white.color);
|
final _regularTextConfig = TextPaintConfig(color: BasicPalette.white.color);
|
||||||
final _tiny = _regular.withFontSize(12.0);
|
final _regular = TextPaint(config: _regularTextConfig);
|
||||||
|
final _tiny = TextPaint(config: _regularTextConfig.withFontSize(12.0));
|
||||||
|
|
||||||
final _white = Paint()
|
final _white = Paint()
|
||||||
..color = BasicPalette.white.color
|
..color = BasicPalette.white.color
|
||||||
@ -16,7 +17,7 @@ class MyTextBox extends TextBoxComponent {
|
|||||||
MyTextBox(String text)
|
MyTextBox(String text)
|
||||||
: super(
|
: super(
|
||||||
text,
|
text,
|
||||||
config: _tiny,
|
textRenderer: _tiny,
|
||||||
boxConfig: TextBoxConfig(
|
boxConfig: TextBoxConfig(
|
||||||
timePerChar: 0.05,
|
timePerChar: 0.05,
|
||||||
growingBox: true,
|
growingBox: true,
|
||||||
@ -43,20 +44,20 @@ class TextGame extends BaseGame {
|
|||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
add(
|
add(
|
||||||
TextComponent('Hello, Flame', config: _regular)
|
TextComponent('Hello, Flame', textRenderer: _regular)
|
||||||
..anchor = Anchor.topCenter
|
..anchor = Anchor.topCenter
|
||||||
..x = size.x / 2
|
..x = size.x / 2
|
||||||
..y = 32.0,
|
..y = 32.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
add(
|
add(
|
||||||
TextComponent('center', config: _tiny)
|
TextComponent('center', textRenderer: _tiny)
|
||||||
..anchor = Anchor.center
|
..anchor = Anchor.center
|
||||||
..position.setFrom(size / 2),
|
..position.setFrom(size / 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
add(
|
add(
|
||||||
TextComponent('bottomRight', config: _tiny)
|
TextComponent('bottomRight', textRenderer: _tiny)
|
||||||
..anchor = Anchor.bottomRight
|
..anchor = Anchor.bottomRight
|
||||||
..position.setFrom(size),
|
..position.setFrom(size),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -21,8 +21,10 @@ class ParticlesGame extends BaseGame {
|
|||||||
final Random rnd = Random();
|
final Random rnd = Random();
|
||||||
final StepTween steppedTween = StepTween(begin: 0, end: 5);
|
final StepTween steppedTween = StepTween(begin: 0, end: 5);
|
||||||
final trafficLight = TrafficLightComponent();
|
final trafficLight = TrafficLightComponent();
|
||||||
final TextConfig fpsTextConfig = TextConfig(
|
final TextPaint fpsTextPaint = TextPaint(
|
||||||
color: const Color(0xFFFFFFFF),
|
config: const TextPaintConfig(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Defines the lifespan of all the particles in these examples
|
/// Defines the lifespan of all the particles in these examples
|
||||||
@ -474,7 +476,7 @@ class ParticlesGame extends BaseGame {
|
|||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
fpsTextConfig.render(
|
fpsTextPaint.render(
|
||||||
canvas,
|
canvas,
|
||||||
'${fps(120).toStringAsFixed(2)}fps',
|
'${fps(120).toStringAsFixed(2)}fps',
|
||||||
Vector2(0, size.y - 24),
|
Vector2(0, size.y - 24),
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import 'package:flame/timer.dart';
|
|||||||
import 'package:flame/gestures.dart';
|
import 'package:flame/gestures.dart';
|
||||||
|
|
||||||
class TimerGame extends Game with TapDetector {
|
class TimerGame extends Game with TapDetector {
|
||||||
final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF));
|
final TextPaint textConfig = TextPaint(
|
||||||
|
config: const TextPaintConfig(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
);
|
||||||
late Timer countdown;
|
late Timer countdown;
|
||||||
late Timer interval;
|
late Timer interval;
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,17 @@ import 'package:flame/timer.dart';
|
|||||||
import 'package:flame/gestures.dart';
|
import 'package:flame/gestures.dart';
|
||||||
|
|
||||||
class RenderedTimeComponent extends TimerComponent {
|
class RenderedTimeComponent extends TimerComponent {
|
||||||
final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF));
|
final TextPaint textPaint = TextPaint(
|
||||||
|
config: const TextPaintConfig(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
RenderedTimeComponent(Timer timer) : super(timer);
|
RenderedTimeComponent(Timer timer) : super(timer);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
textConfig.render(
|
textPaint.render(
|
||||||
canvas,
|
canvas,
|
||||||
'Elapsed time: ${timer.current}',
|
'Elapsed time: ${timer.current}',
|
||||||
Vector2(10, 150),
|
Vector2(10, 150),
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
- Add a new renderRect method to Sprite
|
- Add a new renderRect method to Sprite
|
||||||
- Addresses the TODO to change the camera public APIs to take Anchors for relativePositions
|
- Addresses the TODO to change the camera public APIs to take Anchors for relativePositions
|
||||||
- Adds methods to support moving the camera relative to its current position
|
- Adds methods to support moving the camera relative to its current position
|
||||||
|
- Abstracting the text api to allow custom text renderers on the framework
|
||||||
|
|
||||||
## [1.0.0-rc9]
|
## [1.0.0-rc9]
|
||||||
- Fix input bug with other anchors than center
|
- Fix input bug with other anchors than center
|
||||||
|
|||||||
@ -21,5 +21,5 @@ export 'src/components/sprite_component.dart';
|
|||||||
export 'src/components/text_box_component.dart';
|
export 'src/components/text_box_component.dart';
|
||||||
export 'src/components/text_component.dart';
|
export 'src/components/text_component.dart';
|
||||||
export 'src/extensions/vector2.dart';
|
export 'src/extensions/vector2.dart';
|
||||||
export 'src/text_config.dart';
|
export 'src/text.dart';
|
||||||
export 'src/timer.dart';
|
export 'src/timer.dart';
|
||||||
|
|||||||
@ -6,4 +6,4 @@ export 'src/game/game.dart';
|
|||||||
export 'src/game/game_widget/game_widget.dart';
|
export 'src/game/game_widget/game_widget.dart';
|
||||||
export 'src/game/projector.dart';
|
export 'src/game/projector.dart';
|
||||||
export 'src/game/viewport.dart';
|
export 'src/game/viewport.dart';
|
||||||
export 'src/text_config.dart';
|
export 'src/text.dart';
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import '../../game.dart';
|
|||||||
import '../effects/effects.dart';
|
import '../effects/effects.dart';
|
||||||
import '../effects/effects_handler.dart';
|
import '../effects/effects_handler.dart';
|
||||||
import '../extensions/vector2.dart';
|
import '../extensions/vector2.dart';
|
||||||
import '../text_config.dart';
|
import '../text.dart';
|
||||||
import 'component.dart';
|
import 'component.dart';
|
||||||
import 'mixins/has_game_ref.dart';
|
import 'mixins/has_game_ref.dart';
|
||||||
|
|
||||||
@ -50,7 +50,12 @@ abstract class BaseComponent extends Component {
|
|||||||
..strokeWidth = 1
|
..strokeWidth = 1
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
TextConfig get debugTextConfig => TextConfig(color: debugColor, fontSize: 12);
|
TextPaint get debugTextPaint => TextPaint(
|
||||||
|
config: TextPaintConfig(
|
||||||
|
color: debugColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/// This method is called periodically by the game engine to request that your component updates itself.
|
/// This method is called periodically by the game engine to request that your component updates itself.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -156,7 +156,7 @@ abstract class PositionComponent extends BaseComponent {
|
|||||||
(this as Hitbox).renderShapes(canvas);
|
(this as Hitbox).renderShapes(canvas);
|
||||||
}
|
}
|
||||||
canvas.drawRect(size.toRect(), debugPaint);
|
canvas.drawRect(size.toRect(), debugPaint);
|
||||||
debugTextConfig.render(
|
debugTextPaint.render(
|
||||||
canvas,
|
canvas,
|
||||||
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
|
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
|
||||||
Vector2(-50, -15),
|
Vector2(-50, -15),
|
||||||
@ -165,7 +165,7 @@ abstract class PositionComponent extends BaseComponent {
|
|||||||
final rect = toRect();
|
final rect = toRect();
|
||||||
final dx = rect.right;
|
final dx = rect.right;
|
||||||
final dy = rect.bottom;
|
final dy = rect.bottom;
|
||||||
debugTextConfig.render(
|
debugTextPaint.render(
|
||||||
canvas,
|
canvas,
|
||||||
'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
|
'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
|
||||||
Vector2(width - 50, height),
|
Vector2(width - 50, height),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart' hide Image;
|
|||||||
|
|
||||||
import '../extensions/vector2.dart';
|
import '../extensions/vector2.dart';
|
||||||
import '../palette.dart';
|
import '../palette.dart';
|
||||||
import '../text_config.dart';
|
import '../text.dart';
|
||||||
import 'position_component.dart';
|
import 'position_component.dart';
|
||||||
|
|
||||||
class TextBoxConfig {
|
class TextBoxConfig {
|
||||||
@ -31,7 +31,7 @@ class TextBoxComponent extends PositionComponent {
|
|||||||
Vector2 _gameSize = Vector2.zero();
|
Vector2 _gameSize = Vector2.zero();
|
||||||
|
|
||||||
final String _text;
|
final String _text;
|
||||||
final TextConfig _config;
|
final TextRenderer _textRenderer;
|
||||||
final TextBoxConfig _boxConfig;
|
final TextBoxConfig _boxConfig;
|
||||||
|
|
||||||
late List<String> _lines;
|
late List<String> _lines;
|
||||||
@ -45,37 +45,37 @@ class TextBoxComponent extends PositionComponent {
|
|||||||
|
|
||||||
String get text => _text;
|
String get text => _text;
|
||||||
|
|
||||||
TextConfig get config => _config;
|
TextRenderer get renderer => _textRenderer;
|
||||||
|
|
||||||
TextBoxConfig get boxConfig => _boxConfig;
|
TextBoxConfig get boxConfig => _boxConfig;
|
||||||
|
|
||||||
TextBoxComponent(
|
TextBoxComponent(
|
||||||
String text, {
|
String text, {
|
||||||
TextConfig? config,
|
TextRenderer? textRenderer,
|
||||||
TextBoxConfig? boxConfig,
|
TextBoxConfig? boxConfig,
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
Vector2? size,
|
Vector2? size,
|
||||||
}) : _text = text,
|
}) : _text = text,
|
||||||
_boxConfig = boxConfig ?? TextBoxConfig(),
|
_boxConfig = boxConfig ?? TextBoxConfig(),
|
||||||
_config = config ?? TextConfig(),
|
_textRenderer = textRenderer ?? TextPaint(),
|
||||||
super(position: position, size: size) {
|
super(position: position, size: size) {
|
||||||
_lines = [];
|
_lines = [];
|
||||||
double? lineHeight;
|
double? lineHeight;
|
||||||
text.split(' ').forEach((word) {
|
text.split(' ').forEach((word) {
|
||||||
final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word';
|
final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word';
|
||||||
final painter = _config.toTextPainter(possibleLine);
|
lineHeight ??= _textRenderer.measureTextHeight(possibleLine);
|
||||||
lineHeight ??= painter.height;
|
|
||||||
if (painter.width <=
|
final textWidth = _textRenderer.measureTextWidth(possibleLine);
|
||||||
_boxConfig.maxWidth - _boxConfig.margins.horizontal) {
|
if (textWidth <= _boxConfig.maxWidth - _boxConfig.margins.horizontal) {
|
||||||
if (_lines.isNotEmpty) {
|
if (_lines.isNotEmpty) {
|
||||||
_lines.last = possibleLine;
|
_lines.last = possibleLine;
|
||||||
} else {
|
} else {
|
||||||
_lines.add(possibleLine);
|
_lines.add(possibleLine);
|
||||||
}
|
}
|
||||||
_updateMaxWidth(painter.width);
|
_updateMaxWidth(textWidth);
|
||||||
} else {
|
} else {
|
||||||
_lines.add(word);
|
_lines.add(word);
|
||||||
_updateMaxWidth(_config.toTextPainter(word).width);
|
_updateMaxWidth(textWidth);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_totalLines = _lines.length;
|
_totalLines = _lines.length;
|
||||||
@ -112,9 +112,12 @@ class TextBoxComponent extends PositionComponent {
|
|||||||
Vector2 get size => Vector2(width, height);
|
Vector2 get size => Vector2(width, height);
|
||||||
|
|
||||||
double getLineWidth(String line, int charCount) {
|
double getLineWidth(String line, int charCount) {
|
||||||
return _config
|
return _textRenderer.measureTextWidth(
|
||||||
.toTextPainter(line.substring(0, math.min(charCount, line.length)))
|
line.substring(
|
||||||
.width;
|
0,
|
||||||
|
math.min(charCount, line.length),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double? _cachedWidth;
|
double? _cachedWidth;
|
||||||
@ -192,7 +195,7 @@ class TextBoxComponent extends PositionComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _drawLine(Canvas c, String line, double dy) {
|
void _drawLine(Canvas c, String line, double dy) {
|
||||||
_config.toTextPainter(line).paint(c, Offset(_boxConfig.margins.left, dy));
|
_textRenderer.render(c, line, Vector2(_boxConfig.margins.left, dy));
|
||||||
}
|
}
|
||||||
|
|
||||||
void redrawLater() async {
|
void redrawLater() async {
|
||||||
|
|||||||
@ -4,14 +4,12 @@ import 'package:flutter/painting.dart';
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../extensions/vector2.dart';
|
import '../extensions/vector2.dart';
|
||||||
import '../text_config.dart';
|
import '../text.dart';
|
||||||
import 'position_component.dart';
|
import 'position_component.dart';
|
||||||
|
|
||||||
class TextComponent extends PositionComponent {
|
class TextComponent extends PositionComponent {
|
||||||
String _text;
|
String _text;
|
||||||
TextConfig _config;
|
TextRenderer _textRenderer;
|
||||||
|
|
||||||
late TextPainter _tp;
|
|
||||||
|
|
||||||
String get text => _text;
|
String get text => _text;
|
||||||
|
|
||||||
@ -22,32 +20,32 @@ class TextComponent extends PositionComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextConfig get config => _config;
|
TextRenderer get textRenderer => _textRenderer;
|
||||||
|
|
||||||
set config(TextConfig config) {
|
set textRenderer(TextRenderer textRenderer) {
|
||||||
_config = config;
|
_textRenderer = textRenderer;
|
||||||
_updateBox();
|
_updateBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
TextComponent(
|
TextComponent(
|
||||||
this._text, {
|
this._text, {
|
||||||
TextConfig? config,
|
TextRenderer? textRenderer,
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
Vector2? size,
|
Vector2? size,
|
||||||
}) : _config = config ?? TextConfig(),
|
}) : _textRenderer = textRenderer ?? TextPaint(),
|
||||||
super(position: position, size: size) {
|
super(position: position, size: size) {
|
||||||
_updateBox();
|
_updateBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateBox() {
|
void _updateBox() {
|
||||||
_tp = config.toTextPainter(_text);
|
final size = textRenderer.measureText(_text);
|
||||||
size.setValues(_tp.width, _tp.height);
|
size.setValues(size.x, size.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
@override
|
@override
|
||||||
void render(Canvas c) {
|
void render(Canvas canvas) {
|
||||||
super.render(c);
|
super.render(canvas);
|
||||||
_tp.paint(c, Offset.zero);
|
_textRenderer.render(canvas, text, Vector2.zero());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,22 +8,76 @@ import 'extensions/size.dart';
|
|||||||
import 'extensions/vector2.dart';
|
import 'extensions/vector2.dart';
|
||||||
import 'memory_cache.dart';
|
import 'memory_cache.dart';
|
||||||
|
|
||||||
/// A Text Config contains all typographical information required to render texts; i.e., font size and color, family, etc.
|
/// [TextRenderer] is the abstract API that Flame uses for rendering text in its features
|
||||||
|
/// this class can be extended to provide an implementation of text rendering in the engine.
|
||||||
///
|
///
|
||||||
/// It does not hold information regarding the position of the text to be render neither the text itself (the string).
|
/// See [TextPaint] for the default implementation offered by Flame
|
||||||
/// To hold all those information, use the Text component.
|
abstract class TextRenderer<T extends BaseTextConfig> {
|
||||||
|
final T config;
|
||||||
|
|
||||||
|
TextRenderer({required this.config});
|
||||||
|
|
||||||
|
/// Renders a given [text] in a given position [position] using the provided [canvas] and [anchor].
|
||||||
///
|
///
|
||||||
/// It is used by [TextComponent].
|
/// Renders it in the given position, considering the [anchor] specified.
|
||||||
class TextConfig {
|
/// For example, if [Anchor.center] is specified, it's going to be drawn centered around [position].
|
||||||
|
///
|
||||||
|
/// Example usage (Using TextPaint implementation):
|
||||||
|
///
|
||||||
|
/// const TextPaint config = TextPaint(fontSize: 48.0, fontFamily: 'Awesome Font');
|
||||||
|
/// config.render(canvas, Vector2(size.x - 10, size.y - 10, anchor: Anchor.bottomRight);
|
||||||
|
void render(
|
||||||
|
Canvas canvas,
|
||||||
|
String text,
|
||||||
|
Vector2 position, {
|
||||||
|
Anchor anchor = Anchor.topLeft,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Given a [text] String, returns the width of that [text].
|
||||||
|
double measureTextWidth(String text);
|
||||||
|
|
||||||
|
/// Given a [text] String, returns the height of that [text].
|
||||||
|
double measureTextHeight(String text);
|
||||||
|
|
||||||
|
/// Given a [text] String, returns a Vector2 with the size of that [text] has.
|
||||||
|
Vector2 measureText(String text) {
|
||||||
|
return Vector2(
|
||||||
|
measureTextWidth(text),
|
||||||
|
measureTextHeight(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Text Config contains all typographical information required to render texts; i.e., font size, text direction, etc.
|
||||||
|
abstract class BaseTextConfig {
|
||||||
/// The font size to be used, in points.
|
/// The font size to be used, in points.
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
|
|
||||||
|
/// The direction to render this text (left to right or right to left).
|
||||||
|
///
|
||||||
|
/// Normally, leave this as is for most languages.
|
||||||
|
/// For proper fonts of languages like Hebrew or Arabic, replace this with [TextDirection.rtl].
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
/// The height of line, as a multiple of font size.
|
||||||
|
final double? lineHeight;
|
||||||
|
|
||||||
|
const BaseTextConfig({
|
||||||
|
this.fontSize = 24.0,
|
||||||
|
this.textDirection = TextDirection.ltr,
|
||||||
|
this.lineHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An extension of the BaseTextConfig which includes more configs supported by
|
||||||
|
/// TextPaint
|
||||||
|
class TextPaintConfig extends BaseTextConfig {
|
||||||
/// The font color to be used.
|
/// The font color to be used.
|
||||||
///
|
///
|
||||||
/// Dart's [Color] class is just a plain wrapper on top of ARGB color (0xAARRGGBB).
|
/// Dart's [Color] class is just a plain wrapper on top of ARGB color (0xAARRGGBB).
|
||||||
/// For example,
|
/// For example,
|
||||||
///
|
///
|
||||||
/// const TextConfig config = TextConfig(color: const Color(0xFF00FF00)); // green
|
/// const TextPaint config = TextPaint(color: const Color(0xFF00FF00)); // green
|
||||||
///
|
///
|
||||||
/// You can also use your Palette class to access colors used in your game.
|
/// You can also use your Palette class to access colors used in your game.
|
||||||
final Color color;
|
final Color color;
|
||||||
@ -44,45 +98,97 @@ class TextConfig {
|
|||||||
/// The name you choose for the font family can be any name (it's not inside the TTF file and the filename doesn't need to match).
|
/// The name you choose for the font family can be any name (it's not inside the TTF file and the filename doesn't need to match).
|
||||||
final String fontFamily;
|
final String fontFamily;
|
||||||
|
|
||||||
/// The [TextAlign] to be used when creating the [material.TextPainter].
|
/// Creates a constant [TextPaint] with sensible defaults.
|
||||||
///
|
///
|
||||||
/// Beware: it's recommended to leave this with the default value of [TextAlign.left].
|
/// Every parameter can be specified.
|
||||||
/// Use the anchor parameter to [render] to specify a proper relative position.
|
const TextPaintConfig({
|
||||||
final TextAlign textAlign;
|
this.color = const Color(0xFF000000),
|
||||||
|
this.fontFamily = 'Arial',
|
||||||
|
double fontSize = 24.0,
|
||||||
|
TextDirection textDirection = TextDirection.rtl,
|
||||||
|
double? lineHeight,
|
||||||
|
}) : super(
|
||||||
|
fontSize: fontSize,
|
||||||
|
textDirection: textDirection,
|
||||||
|
lineHeight: lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
/// The direction to render this text (left to right or right to left).
|
/// Creates a new [TextPaintConfig] changing only the [fontSize].
|
||||||
///
|
///
|
||||||
/// Normally, leave this as is for most languages.
|
/// This does not change the original (as it's immutable).
|
||||||
/// For proper fonts of languages like Hebrew or Arabic, replace this with [TextDirection.rtl].
|
TextPaintConfig withFontSize(double fontSize) {
|
||||||
final TextDirection textDirection;
|
return TextPaintConfig(
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: color,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The height of line, as a multiple of font size.
|
/// Creates a new [TextPaintConfig] changing only the [color].
|
||||||
final double? lineHeight;
|
///
|
||||||
|
/// This does not change the original (as it's immutable).
|
||||||
|
TextPaintConfig withColor(Color color) {
|
||||||
|
return TextPaintConfig(
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: color,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [TextPaintConfig] changing only the [fontFamily].
|
||||||
|
///
|
||||||
|
/// This does not change the original (as it's immutable).
|
||||||
|
TextPaintConfig withFontFamily(String fontFamily) {
|
||||||
|
return TextPaintConfig(
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: color,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [TextPaintConfig] changing only the [textAlign].
|
||||||
|
///
|
||||||
|
/// This does not change the original (as it's immutable).
|
||||||
|
TextPaintConfig withTextAlign(TextAlign textAlign) {
|
||||||
|
return TextPaintConfig(
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: color,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [TextPaintConfig] changing only the [textDirection].
|
||||||
|
///
|
||||||
|
/// This does not change the original (as it's immutable).
|
||||||
|
TextPaintConfig withTextDirection(TextDirection textDirection) {
|
||||||
|
return TextPaintConfig(
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: color,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Text Config contains all typographical information required to render texts; i.e., font size and color, family, etc.
|
||||||
|
///
|
||||||
|
/// It does not hold information regarding the position of the text to be render neither the text itself (the string).
|
||||||
|
/// To hold all those information, use the Text component.
|
||||||
|
///
|
||||||
|
/// It is used by [TextComponent].
|
||||||
|
class TextPaint extends TextRenderer<TextPaintConfig> {
|
||||||
final MemoryCache<String, material.TextPainter> _textPainterCache =
|
final MemoryCache<String, material.TextPainter> _textPainterCache =
|
||||||
MemoryCache();
|
MemoryCache();
|
||||||
|
|
||||||
/// Creates a constant [TextConfig] with sensible defaults.
|
TextPaint({
|
||||||
///
|
TextPaintConfig config = const TextPaintConfig(),
|
||||||
/// Every parameter can be specified.
|
}) : super(config: config);
|
||||||
TextConfig({
|
|
||||||
this.fontSize = 24.0,
|
|
||||||
this.color = const Color(0xFF000000),
|
|
||||||
this.fontFamily = 'Arial',
|
|
||||||
this.textAlign = TextAlign.left,
|
|
||||||
this.textDirection = TextDirection.ltr,
|
|
||||||
this.lineHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Renders a given [text] in a given position [p] using the provided [canvas] and [anchor].
|
@override
|
||||||
///
|
|
||||||
/// It creates a [material.TextPainter] instance using the [toTextPainter] method, and renders it in the given position, considering the [anchor] specified.
|
|
||||||
/// For example, if [Anchor.center] is specified, it's going to be drawn centered around [p].
|
|
||||||
///
|
|
||||||
/// Example usage:
|
|
||||||
///
|
|
||||||
/// const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font');
|
|
||||||
/// config.render(c, Offset(size.width - 10, size.height - 10, anchor: Anchor.bottomRight);
|
|
||||||
void render(
|
void render(
|
||||||
Canvas canvas,
|
Canvas canvas,
|
||||||
String text,
|
String text,
|
||||||
@ -94,13 +200,23 @@ class TextConfig {
|
|||||||
tp.paint(canvas, translatedPosition.toOffset());
|
tp.paint(canvas, translatedPosition.toOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double measureTextWidth(String text) {
|
||||||
|
return toTextPainter(text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double measureTextHeight(String text) {
|
||||||
|
return toTextPainter(text).height;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a [material.TextPainter] that allows for text rendering and size measuring.
|
/// Returns a [material.TextPainter] that allows for text rendering and size measuring.
|
||||||
///
|
///
|
||||||
/// A [material.TextPainter] has three important properties: paint, width and height (or size).
|
/// A [material.TextPainter] has three important properties: paint, width and height (or size).
|
||||||
///
|
///
|
||||||
/// Example usage:
|
/// Example usage:
|
||||||
///
|
///
|
||||||
/// const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font');
|
/// const TextPaint config = TextPaint(fontSize: 48.0, fontFamily: 'Awesome Font');
|
||||||
/// final tp = config.toTextPainter('Score: $score');
|
/// final tp = config.toTextPainter('Score: $score');
|
||||||
/// tp.paint(c, Offset(size.width - p.width - 10, size.height - p.height - 10));
|
/// tp.paint(c, Offset(size.width - p.width - 10, size.height - p.height - 10));
|
||||||
///
|
///
|
||||||
@ -109,10 +225,10 @@ class TextConfig {
|
|||||||
material.TextPainter toTextPainter(String text) {
|
material.TextPainter toTextPainter(String text) {
|
||||||
if (!_textPainterCache.containsKey(text)) {
|
if (!_textPainterCache.containsKey(text)) {
|
||||||
final style = material.TextStyle(
|
final style = material.TextStyle(
|
||||||
color: color,
|
color: config.color,
|
||||||
fontSize: fontSize,
|
fontSize: config.fontSize,
|
||||||
fontFamily: fontFamily,
|
fontFamily: config.fontFamily,
|
||||||
height: lineHeight,
|
height: config.lineHeight,
|
||||||
);
|
);
|
||||||
final span = material.TextSpan(
|
final span = material.TextSpan(
|
||||||
style: style,
|
style: style,
|
||||||
@ -120,8 +236,7 @@ class TextConfig {
|
|||||||
);
|
);
|
||||||
final tp = material.TextPainter(
|
final tp = material.TextPainter(
|
||||||
text: span,
|
text: span,
|
||||||
textAlign: textAlign,
|
textDirection: config.textDirection,
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
);
|
||||||
tp.layout();
|
tp.layout();
|
||||||
|
|
||||||
@ -129,69 +244,4 @@ class TextConfig {
|
|||||||
}
|
}
|
||||||
return _textPainterCache.getValue(text)!;
|
return _textPainterCache.getValue(text)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [TextConfig] changing only the [fontSize].
|
|
||||||
///
|
|
||||||
/// This does not change the original (as it's immutable).
|
|
||||||
TextConfig withFontSize(double fontSize) {
|
|
||||||
return TextConfig(
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: color,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
textAlign: textAlign,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new [TextConfig] changing only the [color].
|
|
||||||
///
|
|
||||||
/// This does not change the original (as it's immutable).
|
|
||||||
TextConfig withColor(Color color) {
|
|
||||||
return TextConfig(
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: color,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
textAlign: textAlign,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new [TextConfig] changing only the [fontFamily].
|
|
||||||
///
|
|
||||||
/// This does not change the original (as it's immutable).
|
|
||||||
TextConfig withFontFamily(String fontFamily) {
|
|
||||||
return TextConfig(
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: color,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
textAlign: textAlign,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new [TextConfig] changing only the [textAlign].
|
|
||||||
///
|
|
||||||
/// This does not change the original (as it's immutable).
|
|
||||||
TextConfig withTextAlign(TextAlign textAlign) {
|
|
||||||
return TextConfig(
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: color,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
textAlign: textAlign,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new [TextConfig] changing only the [textDirection].
|
|
||||||
///
|
|
||||||
/// This does not change the original (as it's immutable).
|
|
||||||
TextConfig withTextDirection(TextDirection textDirection) {
|
|
||||||
return TextConfig(
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: color,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
textAlign: textAlign,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user