Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: Added SpriteFont class #1992

Merged
merged 18 commits into from
Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
18 changes: 10 additions & 8 deletions examples/games/trex/lib/trex_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ class TRexGame extends FlameGame
add(gameOverPanel);

const chars = '0123456789HI ';
final renderer = SpriteFontRenderer(
source: spriteImage,
charWidth: 20,
charHeight: 23,
glyphs: {
for (var i = 0; i < chars.length; i++)
chars[i]: GlyphData(left: 954.0 + 20 * i, top: 0)
},
final renderer = SpriteFontRenderer.fromFont(
SpriteFont(
source: spriteImage,
size: 23,
ascent: 23,
glyphs: [
for (var i = 0; i < chars.length; i++)
Glyph(chars[i], left: 954.0 + 20 * i, top: 0, width: 20)
],
),
letterSpacing: 2,
);
add(
Expand Down
104 changes: 104 additions & 0 deletions packages/flame/lib/src/text/common/glyph.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:flame/src/text/common/glyph_data.dart';
import 'package:flame/src/text/common/sprite_font.dart';
import 'package:meta/meta.dart';

/// [Glyph] describes a single character/ligature/icon within a [SpriteFont].
///
/// A glyph has an associated "glyph box", which is the box where the glyph is
/// logically located. Here "logically" means that it includes not only the
/// character itself, but also some padding around it as necessary for the
/// character's to look nice within the text. For all glyphs in a font, their
/// boxes will have the same height (which is the font size), the same ascent
/// and descent, but possibly different widths.
///
/// The properties [left], [top], [width] and [height] describe the location of
/// the glyph box within the source image.
///
/// In addition to the logical "glyph box", a glyph may also have a "source
/// box", which describes a rectangle within the source image where the glyph's
/// pixels are actually located. The source box may be larger or smaller than
/// the glyph box. It will be larger if the glyph has a particularly large
/// flourish that trespasses upon other character's space; or smaller if the
/// characters are packed too tightly in the source image, or if you're trying
/// to improve rendering performance by not copying empty pixels.
class Glyph {
Glyph(
this.char, {
required this.left,
required this.top,
double? width,
double? height,
double? srcLeft,
double? srcTop,
double? srcRight,
double? srcBottom,
}) : assert((width ?? 0) >= 0, 'The `width` parameter cannot be negative'),
assert((height ?? 0) >= 0, 'The `height` parameter cannot be negative'),
assert(
(srcLeft == null &&
srcTop == null &&
srcRight == null &&
srcBottom == null) ||
(srcLeft != null &&
srcTop != null &&
srcRight != null &&
srcBottom != null),
'Either all or none of parameters `srcLeft`, `srcTop`, `srcRight` '
'and `srcBottom` must be specified',
),
width = width ?? -1,
height = height ?? -1,
srcLeft = srcLeft ?? -1,
srcTop = srcTop ?? -1,
srcRight = srcRight ?? -1,
srcBottom = srcBottom ?? -1;

// ignore: deprecated_member_use_from_same_package
Glyph.fromGlyphData(this.char, GlyphData data)
: left = data.left,
top = data.top,
width = data.right == null ? -1 : data.right! - data.left,
height = data.bottom == null ? -1 : data.bottom! - data.top,
srcLeft = -1,
srcTop = -1,
srcRight = -1,
srcBottom = -1;

final String char;
final double left;
final double top;
double width;
double height;
double srcLeft;
double srcTop;
double srcRight;
double srcBottom;

@internal
void initialize(double defaultCharWidth, double charHeight) {
if (width < 0) {
width = defaultCharWidth;
}
if (height < 0) {
height = charHeight;
} else {
assert(
height == charHeight,
'The height of all glyphs must be the same and equal to the font size',
);
}
if (srcLeft < 0) {
srcLeft = left;
srcTop = top;
srcRight = left + width;
srcBottom = top + height;
}
}

@override
String toString() {
return 'Glyph(char="$char", '
'LTWH=[$left, $top, $width, $height], '
'srcLTRB=[$srcLeft, $srcTop, $srcRight, $srcBottom])';
}
}
4 changes: 4 additions & 0 deletions packages/flame/lib/src/text/common/glyph_data.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
@Deprecated('This class will be removed in 1.5.0; use Glyph instead')
class GlyphData {
@Deprecated('Will be removed in 1.5.0; use Glyph class instead')
const GlyphData({
required this.left,
required this.top,
this.right,
this.bottom,
});

@Deprecated('Will be removed in 1.5.0; use Glyph class instead')
const GlyphData.fromLTWH(this.left, this.top, double width, double height)
: right = left + width,
bottom = top + height;

@Deprecated('Will be removed in 1.5.0; use Glyph class instead')
const GlyphData.fromLTRB(this.left, this.top, this.right, this.bottom);

final double left;
Expand Down
14 changes: 0 additions & 14 deletions packages/flame/lib/src/text/common/glyph_info.dart

This file was deleted.

66 changes: 66 additions & 0 deletions packages/flame/lib/src/text/common/sprite_font.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:ui';

import 'package:flame/src/text/common/glyph.dart';
import 'package:meta/meta.dart';

class SpriteFont {
SpriteFont({
required this.source,
required this.size,
required this.ascent,
required List<Glyph> glyphs,
double? defaultCharWidth,
}) : _data = <int, _Chain>{} {
for (final glyph in glyphs) {
var data = _data;
for (var i = 0; i < glyph.char.length - 1; i++) {
final j = glyph.char.codeUnitAt(i);
data = (data[j] ??= _Chain()).followOn ??= <int, _Chain>{};
}
final j = glyph.char.codeUnitAt(glyph.char.length - 1);
final chain = data[j] ??= _Chain();
assert(
chain.glyph == null,
'Duplicate definition for glyph "${glyph.char}"',
);
glyph.initialize(defaultCharWidth ?? size, size);
chain.glyph = glyph;
}
}

final Image source;
final double size;
final double ascent;
final Map<int, _Chain> _data;

@internal
Iterable<Glyph> textToGlyphs(String text) sync* {
for (var i = 0; i < text.length; i++) {
var chain = _data[text.codeUnitAt(i)];
var iNext = i;
var resolvedGlyph = chain?.glyph;
for (var j = i + 1; j < text.length; j++) {
if (chain?.followOn == null) {
break;
}
final jCharCode = text.codeUnitAt(j);
chain = chain!.followOn![jCharCode];
if (chain?.glyph != null) {
iNext = j;
resolvedGlyph = chain?.glyph;
}
}
if (resolvedGlyph == null) {
throw ArgumentError('No glyph data for character "${text[i]}"');
} else {
i = iNext;
yield resolvedGlyph;
}
}
}
}

class _Chain {
Glyph? glyph;
Map<int, _Chain>? followOn;
}
102 changes: 52 additions & 50 deletions packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart
Original file line number Diff line number Diff line change
@@ -1,83 +1,85 @@
import 'dart:typed_data';
import 'dart:ui' hide LineMetrics;

import 'package:flame/src/text/common/glyph.dart';
import 'package:flame/src/text/common/glyph_data.dart';
import 'package:flame/src/text/common/glyph_info.dart';
import 'package:flame/src/text/common/line_metrics.dart';
import 'package:flame/src/text/common/sprite_font.dart';
import 'package:flame/src/text/elements/text_element.dart';
import 'package:flame/src/text/formatters/text_formatter.dart';
import 'package:flame/src/text/inline/sprite_font_text_element.dart';

class SpriteFontTextFormatter extends TextFormatter {
@Deprecated('Use SpriteFontTextFormatter.fromFont() instead; this '
'constructor will be removed in 1.5.0')
SpriteFontTextFormatter({
required this.source,
required Image source,
required double charWidth,
required double charHeight,
// ignore: deprecated_member_use_from_same_package
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);
});
}) : font = SpriteFont(
source: source,
size: charHeight,
ascent: charHeight,
defaultCharWidth: charWidth,
glyphs: [
for (final kv in glyphs.entries)
Glyph.fromGlyphData(kv.key, kv.value)
],
),
paint = Paint();

final Image source;
final paint = Paint()..color = const Color(0xFFFFFFFF);
final double letterSpacing;
SpriteFontTextFormatter.fromFont(
this.font, {
this.scale = 1.0,
this.letterSpacing = 0.0,
Color? color,
}) : paint = Paint() {
if (color != null) {
paint.colorFilter = ColorFilter.mode(color, BlendMode.srcIn);
}
}

final SpriteFont font;
final double scale;
final double scaledCharWidth;
final double scaledCharHeight;
final Map<int, GlyphInfo> _glyphs;
final double letterSpacing;
final Paint paint;

@override
SpriteFontTextElement format(String text) {
final rstTransforms = Float32List(4 * text.length);
final rects = Float32List(4 * text.length);
TextElement format(String text) {
var rects = Float32List(text.length * 4);
var rsts = Float32List(text.length * 4);
var j = 0;
var x0 = 0.0;
final y0 = -scaledCharHeight;
for (final glyph in _textToGlyphs(text)) {
for (final glyph in font.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;
rsts[j + 0] = scale;
rsts[j + 1] = 0;
rsts[j + 2] = x0 + (glyph.srcLeft - glyph.left) * scale;
rsts[j + 3] = (glyph.srcTop - glyph.top - font.ascent) * scale;
j += 4;
x0 += glyph.width * scale + letterSpacing;
}
if (j < text.length * 4) {
rects = rects.sublist(0, j);
rsts = rsts.sublist(0, j);
}
return SpriteFontTextElement(
source: source,
transforms: rstTransforms,
source: font.source,
transforms: rsts,
rects: rects,
paint: paint,
metrics: LineMetrics(width: x0, height: scaledCharHeight, descent: 0),
metrics: LineMetrics(
width: x0 - letterSpacing,
height: font.size * scale,
ascent: font.ascent * scale,
),
);
}

Iterable<GlyphInfo> _textToGlyphs(String text) {
return text.codeUnits.map((int i) {
final glyph = _glyphs[i];
assert(
glyph != null,
'No glyph for character "${String.fromCharCode(i)}"',
);
return glyph!;
});
}
}
Loading