scummvm/engines/twp/font.cpp
2024-03-24 22:49:08 +01:00

384 lines
12 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "twp/twp.h"
#include "twp/detection.h"
#include "twp/font.h"
#include "twp/ggpack.h"
#include "twp/resmanager.h"
namespace Twp {
enum TokenId {
tiWhitespace,
tiString,
tiColor,
tiNewLine,
tiEnd
};
typedef struct Token {
TokenId id;
int startOff, endOff;
} Token;
typedef struct CharInfo {
CodePoint chr;
Math::Vector2d pos;
Color color;
Glyph glyph;
} CharInfo;
typedef struct Line {
Common::Array<Token> tokens;
Common::Array<CharInfo> charInfos;
} Line;
class TokenReader {
public:
explicit TokenReader(const Common::U32String &text);
Common::U32String substr(Token tok);
bool readToken(Token &token);
private:
CodePoint readChar();
TokenId readTokenId();
private:
Common::U32String _text;
size_t _off;
};
static Math::Vector2d normalize(Texture *texture, Math::Vector2d v) {
Math::Vector2d textureSize(texture->width, texture->height);
return Math::Vector2d(v.getX() / textureSize.getX(), v.getY() / textureSize.getY());
}
static void addGlyphQuad(Texture *texture, Common::Array<Vertex> &vertices, CharInfo info) {
// Add a glyph quad to the vertex array
float left = info.glyph.bounds.left;
float top = info.glyph.bounds.bottom;
float right = info.glyph.bounds.right;
float bottom = info.glyph.bounds.top;
Math::Vector2d uv1 = normalize(texture, Math::Vector2d(info.glyph.textureRect.left, info.glyph.textureRect.bottom));
Math::Vector2d uv2 = normalize(texture, Math::Vector2d(info.glyph.textureRect.right, info.glyph.textureRect.top));
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + left, info.pos.getY() + top), info.color, Math::Vector2d(uv1.getX(), uv2.getY())});
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + right, info.pos.getY() + top), info.color, Math::Vector2d(uv2.getX(), uv2.getY())});
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + left, info.pos.getY() + bottom), info.color, Math::Vector2d(uv1.getX(), uv1.getY())});
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + left, info.pos.getY() + bottom), info.color, Math::Vector2d(uv1.getX(), uv1.getY())});
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + right, info.pos.getY() + top), info.color, Math::Vector2d(uv2.getX(), uv2.getY())});
vertices.push_back(Vertex{Math::Vector2d(info.pos.getX() + right, info.pos.getY() + bottom), info.color, Math::Vector2d(uv2.getX(), uv1.getY())});
}
// Skips all characters while one char from the set `token` is found.
// Returns number of characters skipped.
static int skipWhile(const Common::U32String &s, const char *toSkip, int start = 0) {
int result = 0;
int len = s.size();
while ((start + result < len) && strchr(toSkip, s[result + start]))
result++;
return result;
}
static int skipUntil(const Common::U32String &s, const char *until, int start = 0) {
int result = 0;
int len = s.size();
while ((start + result < len) && !strchr(until, s[result + start]))
result++;
return result;
}
static float width(Text &text, TokenReader &reader, Token tok) {
float result = 0;
Common::String s = reader.substr(tok);
for (size_t i = 0; i < s.size(); i++) {
char c = s[i];
result += text.getFont()->getGlyph(c).advance;
}
return result;
}
TokenReader::TokenReader(const Common::U32String &text) : _text(text), _off(0) {
}
Common::U32String TokenReader::substr(Token tok) {
return _text.substr(tok.startOff, tok.endOff - tok.startOff + 1);
}
CodePoint TokenReader::readChar() {
char result = _text[_off];
_off++;
return result;
}
TokenId TokenReader::readTokenId() {
const char Whitespace[] = {' ', '\t', '\v', '\r', '\f', '\0'};
const char Whitespace2[] = {' ', '\t', '\v', '\r', '\f', '#', '\n', '\0'};
if (_off < _text.size()) {
char c = readChar();
switch (c) {
case '\n':
return tiNewLine;
case '\t':
case ' ':
_off += skipWhile(_text, Whitespace, _off);
return tiWhitespace;
case '#':
_off += 7;
return tiColor;
default:
_off += skipUntil(_text, Whitespace2, _off);
return tiString;
}
} else {
return tiEnd;
}
}
bool TokenReader::readToken(Token &token) {
int start = _off;
TokenId id = readTokenId();
if (id != tiEnd) {
token.id = id;
token.startOff = start;
token.endOff = _off - 1;
return true;
}
return false;
}
GGFont::~GGFont() {}
void GGFont::load(const Common::String &path) {
SpriteSheet *spritesheet = g_twp->_resManager->spriteSheet(path);
int lineHeight = 0;
for (auto it = spritesheet->_frameTable.begin(); it != spritesheet->_frameTable.end(); it++) {
const SpriteSheetFrame &frame = it->_value;
Glyph glyph;
glyph.advance = MAX(frame.sourceSize.getX() - frame.spriteSourceSize.left - 4, 0.f);
glyph.bounds = Common::Rect(Common::Point(frame.spriteSourceSize.left, frame.sourceSize.getY() - frame.spriteSourceSize.height() - frame.spriteSourceSize.top), frame.spriteSourceSize.width(), frame.spriteSourceSize.height());
lineHeight = MAX(lineHeight, (int)frame.spriteSourceSize.top);
glyph.textureRect = frame.frame;
_glyphs[it->_key.asUint64()] = glyph;
}
_lineHeight = lineHeight;
_name = path;
}
Glyph GGFont::getGlyph(CodePoint chr) {
int key = (int)chr;
if (_glyphs.contains(key)) {
return _glyphs[key];
}
return _glyphs['?'];
}
BmFont::~BmFont() {}
void BmFont::load(const Common::String &name) {
Common::String path = name + ".fnt";
if (!g_twp->_pack->assetExists(path.c_str())) {
path = name + "Font.fnt";
}
debugC(kDebugRes, "Load font %s", path.c_str());
GGPackEntryReader entry;
if (!entry.open(*g_twp->_pack, path)) {
error("error loading font %s", path.c_str());
}
char tmp[80];
while (!entry.eos()) {
Common::String line = entry.readLine();
if (line.hasPrefix("common")) {
sscanf(line.c_str(), "common lineHeight=%d base=%d scaleW=%d scaleH=%d pages=%d packed=%d", &_lnHeight, &_base, &_scaleW, &_scaleH, &_pages, &_packed);
} else if (line.hasPrefix("chars")) {
} else if (line.hasPrefix("char")) {
Char c;
sscanf(line.c_str(), "char id=%d\tx=%d\ty=%d\twidth=%d\theight=%d\txoffset=%d\tyoffset=%d\txadvance=%d\tpage=%d\tchnl=%d\tletter=\"%79s\"", &c.id, &c.x, &c.y, &c.w, &c.h, &c.xoff, &c.yoff, &c.xadv, &c.page, &c.chnl, tmp);
_glyphs[c.id] = Glyph{c.xadv,
Common::Rect(c.xoff, _lnHeight - c.yoff - c.h, c.xoff + c.w, _lnHeight - c.yoff),
Common::Rect(c.x, c.y, c.x + c.w, c.y + c.h)};
} else if (line.hasPrefix("kernings")) {
} else if (line.hasPrefix("kerning")) {
KerningKey key;
int amount = 0;
sscanf(line.c_str(), "kerning\tfirst=%d\tsecond=%d\tamount=%d", &key.first, &key.second, &amount);
_kernings[key] = amount;
}
}
_name = name;
}
Glyph BmFont::getGlyph(CodePoint chr) {
if (_glyphs.contains(chr)) {
return _glyphs[chr];
}
return _glyphs['?'];
}
float BmFont::getKerning(CodePoint prev, CodePoint next) {
return 0.f;
}
bool operator==(const KerningKey &l, const KerningKey &r) {
return l.first == r.first && l.second == r.second;
}
Text::Text(const Common::String &fontName, const Common::String &text, TextHAlignment hAlign, TextVAlignment vAlign, float maxWidth, const Color &color)
: _font(NULL), _fontName(fontName), _texture(NULL), _txt(text), _col(color), _hAlign(hAlign), _vAlign(vAlign), _maxW(maxWidth), _dirty(true) {
update();
}
Text::Text() {}
void Text::setFont(const Common::String &fontName) {
_fontName = fontName;
_dirty = true;
}
Math::Vector2d Text::getBounds() {
update();
return _bnds;
}
void Text::update() {
if (_dirty) {
_dirty = false;
_font = g_twp->_resManager->font(_fontName);
_texture = g_twp->_resManager->texture(_font->getName() + ".png");
// Reset
_vertices.clear();
_quads.clear();
_bnds = Math::Vector2d();
Color color = _col;
// split text by tokens and split tokens by lines
Common::Array<Line> lines;
Line line1;
TokenReader reader(_txt);
float x = 0;
Token tok;
while (reader.readToken(tok)) {
// ignore color token width
float w = tok.id == tiColor || tok.id == tiNewLine ? 0.f : width(*this, reader, tok);
// new line if width > maxWidth or newline character
if (tok.id == tiNewLine || ((_maxW > 0) && (line1.tokens.size() > 0) && (x + w > _maxW))) {
lines.push_back(line1);
line1.tokens.clear();
x = 0;
}
if (tok.id != tiNewLine) {
if (line1.tokens.size() != 0 || tok.id != tiWhitespace) {
line1.tokens.push_back(tok);
x += w;
}
}
}
lines.push_back(line1);
// create quads for all characters
float maxW = 0.f;
float lineHeight = _font->getLineHeight();
float y = 0.f;
for (size_t i = 0; i < lines.size(); i++) {
Line &line = lines[i];
CodePoint prevChar = 0;
x = 0;
for (size_t j = 0; j < line.tokens.size(); j++) {
tok = line.tokens[j];
if (tok.id == tiColor) {
uint iColor;
Common::String s = reader.substr(tok);
sscanf(s.c_str() + 1, "%x", &iColor);
color = Color::withAlpha(Color::rgb((int)(iColor & 0x00FFFFFF)), color.rgba.a);
} else {
Common::U32String s = reader.substr(tok);
for (size_t k = 0; k < s.size(); k++) {
CodePoint c = s[k];
Glyph glyph = _font->getGlyph(c);
float kern = _font->getKerning(prevChar, c);
prevChar = c;
line.charInfos.push_back(CharInfo{c, Math::Vector2d(x + kern, y), color, glyph});
// self.quads.add(rect(x, y, glyph.bounds.x.float32 + glyph.bounds.w.float32, lineHeight));
x += (float)glyph.advance;
}
}
}
_quads.push_back(Common::Rect(Common::Point(0.0f, y), x, lineHeight));
maxW = MAX(maxW, x);
y -= lineHeight;
}
// Align text
if (_hAlign == thRight) {
for (size_t i = 0; i < lines.size(); i++) {
float w = maxW - _quads[i].width();
for (size_t j = 0; j < lines[i].charInfos.size(); j++) {
CharInfo &info = lines[i].charInfos[j];
info.pos.setX(info.pos.getX() + w);
}
}
} else if (_hAlign == thCenter) {
for (size_t i = 0; i < lines.size(); i++) {
float w = maxW - _quads[i].width();
for (size_t j = 0; j < lines[i].charInfos.size(); j++) {
CharInfo &info = lines[i].charInfos[j];
info.pos.setX(info.pos.getX() + w / 2.f);
}
}
}
// Add the glyphs to the vertices
for (size_t i = 0; i < lines.size(); i++) {
for (size_t j = 0; j < lines[i].charInfos.size(); j++) {
const CharInfo &info = lines[i].charInfos[j];
addGlyphQuad(_texture, _vertices, info);
}
}
_bnds = Math::Vector2d(maxW, lines.size() * _font->getLineHeight());
}
}
void Text::draw(Gfx &gfx, const Math::Matrix4 &t) {
Math::Matrix4 trsf(t);
switch (_vAlign) {
case tvTop:
trsf.translate(Math::Vector3d(0.f, 0.f, 0.f));
break;
case tvCenter:
trsf.translate(Math::Vector3d(0.f, _bnds.getY() / 2.f, 0.f));
break;
case tvBottom:
trsf.translate(Math::Vector3d(0.f, _bnds.getY(), 0.f));
break;
}
if (_font && !_txt.empty()) {
update();
gfx.drawPrimitives(GL_TRIANGLES, _vertices.begin(), _vertices.size(), trsf, _texture);
}
}
} // namespace Twp