-
-
Notifications
You must be signed in to change notification settings - Fork 899
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added ability to render spritesheet-based fonts (#1634)
This PR adds [SpriteFontRenderer], which is a new implementation of [TextRenderer], allowing to render text based on fonts embedded into a spritesheet. See the T-Rex game example, where the score is now rendered based on a spritesheet font. (Also added highscore tracking for the TRex game).
- Loading branch information
Showing
9 changed files
with
290 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ build/ | |
_build/ | ||
|
||
coverage | ||
**/failures | ||
|
||
.fvm | ||
Makefile | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import 'dart:typed_data'; | ||
import 'dart:ui'; | ||
|
||
import 'package:flame/src/anchor.dart'; | ||
import 'package:flame/src/text/text_renderer.dart'; | ||
import 'package:vector_math/vector_math_64.dart'; | ||
|
||
/// [TextRenderer] implementation that uses a spritesheet of various font glyphs | ||
/// to render text. | ||
/// | ||
/// For example, suppose there is a spritesheet with sprites for characters from | ||
/// A to Z. Mapping these sprites into a [SpriteFontRenderer] allows then to | ||
/// write text using these sprite images as the font. | ||
/// | ||
/// Currently, this class supports monospace fonts only -- the widths and the | ||
/// heights of all characters must be the same. | ||
/// Extra space between letters can be added via the [letterSpacing] parameter | ||
/// (it can also be negative to "squish" characters together). Finally, the | ||
/// [scale] parameter allows scaling the font to be bigger/smaller relative to | ||
/// its size in the source image. | ||
/// | ||
/// The [paint] parameter is used to composite the character images onto the | ||
/// canvas. Its default value will draw the character images as-is. Changing | ||
/// the opacity of the paint's color will make the text semi-transparent. | ||
class SpriteFontRenderer extends TextRenderer { | ||
SpriteFontRenderer({ | ||
required this.source, | ||
required double charWidth, | ||
required double charHeight, | ||
required Map<String, GlyphData> glyphs, | ||
this.scale = 1, | ||
this.letterSpacing = 0, | ||
}) : scaledCharWidth = charWidth * scale, | ||
scaledCharHeight = charHeight * scale, | ||
_glyphs = glyphs.map((char, rect) { | ||
assert( | ||
char.length == 1, | ||
'A glyph must have a single character: "$char"', | ||
); | ||
final info = _GlyphInfo(); | ||
info.srcLeft = rect.left; | ||
info.srcTop = rect.top; | ||
info.srcRight = rect.right ?? rect.left + charWidth; | ||
info.srcBottom = rect.bottom ?? rect.top + charHeight; | ||
info.rstSCos = scale; | ||
info.rstTy = (charHeight - (info.srcBottom - info.srcTop)) * scale; | ||
info.width = charWidth * scale; | ||
info.height = charHeight * scale; | ||
return MapEntry(char.codeUnitAt(0), info); | ||
}); | ||
|
||
final Image source; | ||
final Map<int, _GlyphInfo> _glyphs; | ||
final double letterSpacing; | ||
final double scale; | ||
final double scaledCharWidth; | ||
final double scaledCharHeight; | ||
bool get isMonospace => true; | ||
|
||
Paint paint = Paint()..color = const Color(0xFFFFFFFF); | ||
|
||
@override | ||
double measureTextHeight(String text) => scaledCharHeight; | ||
|
||
@override | ||
double measureTextWidth(String text) { | ||
final n = text.length; | ||
return n > 0 ? scaledCharWidth * n + letterSpacing * (n - 1) : 0; | ||
} | ||
|
||
@override | ||
Vector2 measureText(String text) { | ||
return Vector2(measureTextWidth(text), measureTextHeight(text)); | ||
} | ||
|
||
@override | ||
void render( | ||
Canvas canvas, | ||
String text, | ||
Vector2 position, { | ||
Anchor anchor = Anchor.topLeft, | ||
}) { | ||
final rstTransforms = Float32List(4 * text.length); | ||
final rects = Float32List(4 * text.length); | ||
var j = 0; | ||
var x0 = position.x; | ||
final y0 = position.y; | ||
for (final glyph in _textToGlyphs(text)) { | ||
rects[j + 0] = glyph.srcLeft; | ||
rects[j + 1] = glyph.srcTop; | ||
rects[j + 2] = glyph.srcRight; | ||
rects[j + 3] = glyph.srcBottom; | ||
rstTransforms[j + 0] = glyph.rstSCos; | ||
rstTransforms[j + 1] = glyph.rstSSin; | ||
rstTransforms[j + 2] = x0 + glyph.rstTx; | ||
rstTransforms[j + 3] = y0 + glyph.rstTy; | ||
x0 += glyph.width + letterSpacing; | ||
j += 4; | ||
} | ||
canvas.drawRawAtlas(source, rstTransforms, rects, null, null, null, paint); | ||
} | ||
|
||
Iterable<_GlyphInfo> _textToGlyphs(String text) { | ||
return text.codeUnits.map(_getGlyphFromCodeUnit); | ||
} | ||
|
||
_GlyphInfo _getGlyphFromCodeUnit(int i) { | ||
final glyph = _glyphs[i]; | ||
if (glyph == null) { | ||
throw ArgumentError('No glyph for character "${String.fromCharCode(i)}"'); | ||
} | ||
return glyph; | ||
} | ||
} | ||
|
||
class GlyphData { | ||
const GlyphData({ | ||
required this.left, | ||
required this.top, | ||
this.right, | ||
this.bottom, | ||
}); | ||
|
||
const GlyphData.fromLTWH(this.left, this.top, double width, double height) | ||
: right = left + width, | ||
bottom = top + height; | ||
|
||
const GlyphData.fromLTRB(this.left, this.top, this.right, this.bottom); | ||
|
||
final double left; | ||
final double top; | ||
final double? right; | ||
final double? bottom; | ||
} | ||
|
||
class _GlyphInfo { | ||
double srcLeft = 0; | ||
double srcTop = 0; | ||
double srcRight = 0; | ||
double srcBottom = 0; | ||
double rstSCos = 1; | ||
double rstSSin = 0; | ||
double rstTx = 0; | ||
double rstTy = 0; | ||
double width = 0; | ||
double height = 0; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export 'src/text/sprite_font_renderer.dart' show SpriteFontRenderer, GlyphData; | ||
export 'src/text/text_paint.dart' show TextPaint; | ||
export 'src/text/text_renderer.dart' show TextRenderer; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions
112
packages/flame/test/text/sprite_font_renderer_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import 'dart:ui'; | ||
|
||
import 'package:canvas_test/canvas_test.dart'; | ||
import 'package:flame/components.dart'; | ||
import 'package:flame/text.dart'; | ||
import 'package:flame_test/flame_test.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
import '../_resources/load_image.dart'; | ||
|
||
void main() { | ||
group('SpriteFontRenderer', () { | ||
test('creating SpriteFontRenderer', () async { | ||
final renderer = await createRenderer(); | ||
expect(renderer.source, isA<Image>()); | ||
expect(renderer.scaledCharWidth, 6); | ||
expect(renderer.scaledCharHeight, 6); | ||
expect(renderer.letterSpacing, 0); | ||
expect(renderer.isMonospace, true); | ||
|
||
expect( | ||
() => renderer.render(MockCanvas(), 'Π', Vector2.zero()), | ||
throwsArgumentError, | ||
); | ||
}); | ||
|
||
testGolden( | ||
'text rendering at different scales', | ||
(game) async { | ||
game.addAll([ | ||
RectangleComponent(size: Vector2(800, 600)), | ||
TextBoxComponent( | ||
text: textSample, | ||
textRenderer: await createRenderer(letterSpacing: 1), | ||
boxConfig: TextBoxConfig(maxWidth: 800), | ||
), | ||
TextBoxComponent( | ||
text: textSample, | ||
textRenderer: await createRenderer(scale: 2), | ||
boxConfig: TextBoxConfig(maxWidth: 800), | ||
position: Vector2(0, 100), | ||
), | ||
TextComponent( | ||
text: 'FLAME', | ||
textRenderer: (await createRenderer(scale: 25)) | ||
..paint.color = const Color(0x44000000), | ||
position: Vector2(400, 500), | ||
anchor: Anchor.center, | ||
), | ||
]); | ||
}, | ||
goldenFile: '../_goldens/sprite_font_renderer_1.png', | ||
); | ||
|
||
test('errors', () async { | ||
const rect = GlyphData.fromLTWH(0, 0, 6, 6); | ||
final image = await loadImage('alphabet.png'); | ||
expect( | ||
() => SpriteFontRenderer( | ||
source: image, | ||
charWidth: 6, | ||
charHeight: 6, | ||
glyphs: {'π₯': rect}, | ||
), | ||
failsAssert('A glyph must have a single character: "π₯"'), | ||
); | ||
}); | ||
}); | ||
} | ||
|
||
const textSample = 'We hold these truths to be self-evident, that all men are ' | ||
'created equal, that they are endowed by their Creator with certain ' | ||
'unalienable Rights, that among these are Life, Liberty and the pursuit of ' | ||
'Happiness. β That to secure these rights, Governments are instituted ' | ||
'among Men, deriving their just powers from the consent of the governed, ' | ||
'β That whenever any Form of Government becomes destructive of these ends, ' | ||
'it is the Right of the People to alter or to abolish it, and to institute ' | ||
'new Government, laying its foundation on such principles and organizing ' | ||
'its powers in such form, as to them shall seem most likely to effect ' | ||
'their Safety and Happiness. Prudence, indeed, will dictate that ' | ||
'Governments long established should not be changed for light and ' | ||
'transient causes; and accordingly all experience hath shewn, that mankind ' | ||
'are more disposed to suffer, while evils are sufferable, than to right ' | ||
'themselves by abolishing the forms to which they are accustomed. But when ' | ||
'a long train of abuses and usurpations, pursuing invariably the same ' | ||
'Object evinces a design to reduce them under absolute Despotism, it is ' | ||
'their right, it is their duty, to throw off such Government, and to ' | ||
'provide new Guards for their future security.'; | ||
|
||
Future<SpriteFontRenderer> createRenderer({ | ||
double scale = 1, | ||
double letterSpacing = 0, | ||
}) async { | ||
const lines = [ | ||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ', | ||
'abcdefghijklmnopqrstuvwxyz', | ||
'0123456789.,:;β_!?@\$%+-=/*', | ||
'#^&()[]{}<>|\\\'"`~ββββ ', | ||
]; | ||
return SpriteFontRenderer( | ||
source: await loadImage('alphabet.png'), | ||
charHeight: 6, | ||
charWidth: 6, | ||
scale: scale, | ||
glyphs: { | ||
for (var j = 0; j < lines.length; j++) | ||
for (var i = 0; i < lines[j].length; i++) | ||
lines[j][i]: GlyphData(left: i * 6, top: 1 + j * 6) | ||
}, | ||
letterSpacing: letterSpacing, | ||
); | ||
} |