/* 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 . * */ #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 tokens; Common::Array 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 &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 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