Replace stb_truetype with FreeType for proper text outline rendering
- Add -sUSE_FREETYPE=1 to Emscripten build flags - Extend Font class with FT_Library, FT_Face, and FT_Stroker handles - Rewrite FontAtlas to use FreeType with on-demand stroked glyph loading - Text outlines now use FT_Stroker for vector-based stroking before rasterization, eliminating gaps at corners with thick outlines - Use glTexSubImage2D for incremental atlas updates (major perf fix) - Disable canvas border in shell.html per Emscripten docs (alignment fix) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1be2714240
commit
67aa413a78
5 changed files with 538 additions and 128 deletions
|
|
@ -283,12 +283,14 @@ if(EMSCRIPTEN)
|
||||||
-sFULL_ES2=1
|
-sFULL_ES2=1
|
||||||
-sMIN_WEBGL_VERSION=2
|
-sMIN_WEBGL_VERSION=2
|
||||||
-sMAX_WEBGL_VERSION=2
|
-sMAX_WEBGL_VERSION=2
|
||||||
|
-sUSE_FREETYPE=1
|
||||||
)
|
)
|
||||||
# SDL2 flags are also needed at compile time for headers
|
# SDL2 and FreeType flags are also needed at compile time for headers
|
||||||
target_compile_options(mcrogueface PRIVATE
|
target_compile_options(mcrogueface PRIVATE
|
||||||
-sUSE_SDL=2
|
-sUSE_SDL=2
|
||||||
|
-sUSE_FREETYPE=1
|
||||||
)
|
)
|
||||||
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sFULL_ES2=1")
|
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sFULL_ES2=1 -sUSE_FREETYPE=1")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
|
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,16 @@
|
||||||
#include <GL/gl.h>
|
#include <GL/gl.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// stb libraries for image/font loading (from deps/stb/)
|
// stb_image for image loading (from deps/stb/)
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
#include <stb_image.h>
|
#include <stb_image.h>
|
||||||
|
|
||||||
#define STB_TRUETYPE_IMPLEMENTATION
|
// FreeType for font loading and text rendering with proper outline support
|
||||||
#include <stb_truetype.h>
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
#include FT_GLYPH_H
|
||||||
|
#include FT_STROKER_H
|
||||||
|
#include FT_OUTLINE_H
|
||||||
|
|
||||||
namespace sf {
|
namespace sf {
|
||||||
|
|
||||||
|
|
@ -1205,12 +1209,29 @@ bool Image::saveToFile(const std::string& filename) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Font Implementation
|
// Font Implementation (FreeType-based)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
Font::~Font() {
|
||||||
|
if (ftStroker_) {
|
||||||
|
FT_Stroker_Done(static_cast<FT_Stroker>(ftStroker_));
|
||||||
|
ftStroker_ = nullptr;
|
||||||
|
}
|
||||||
|
if (ftFace_) {
|
||||||
|
FT_Done_Face(static_cast<FT_Face>(ftFace_));
|
||||||
|
ftFace_ = nullptr;
|
||||||
|
}
|
||||||
|
if (ftLibrary_) {
|
||||||
|
FT_Done_FreeType(static_cast<FT_Library>(ftLibrary_));
|
||||||
|
ftLibrary_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Font::loadFromFile(const std::string& filename) {
|
bool Font::loadFromFile(const std::string& filename) {
|
||||||
|
// Read file into memory first (FreeType needs persistent data)
|
||||||
FILE* file = fopen(filename.c_str(), "rb");
|
FILE* file = fopen(filename.c_str(), "rb");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
std::cerr << "Font: Failed to open file: " << filename << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1222,13 +1243,81 @@ bool Font::loadFromFile(const std::string& filename) {
|
||||||
fread(fontData_.data(), 1, size, file);
|
fread(fontData_.data(), 1, size, file);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
|
// Initialize FreeType library
|
||||||
|
FT_Library library;
|
||||||
|
if (FT_Init_FreeType(&library) != 0) {
|
||||||
|
std::cerr << "Font: Failed to initialize FreeType library" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftLibrary_ = library;
|
||||||
|
|
||||||
|
// Create face from memory (font data must persist!)
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_New_Memory_Face(library, fontData_.data(), fontData_.size(), 0, &face) != 0) {
|
||||||
|
std::cerr << "Font: Failed to create FreeType face from: " << filename << std::endl;
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
ftLibrary_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftFace_ = face;
|
||||||
|
|
||||||
|
// Select Unicode charmap
|
||||||
|
FT_Select_Charmap(face, FT_ENCODING_UNICODE);
|
||||||
|
|
||||||
|
// Create stroker for outline rendering
|
||||||
|
FT_Stroker stroker;
|
||||||
|
if (FT_Stroker_New(library, &stroker) != 0) {
|
||||||
|
std::cerr << "Font: Failed to create FreeType stroker" << std::endl;
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
ftFace_ = nullptr;
|
||||||
|
ftLibrary_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftStroker_ = stroker;
|
||||||
|
|
||||||
loaded_ = true;
|
loaded_ = true;
|
||||||
|
std::cout << "Font: Loaded " << filename << " with FreeType" << std::endl;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Font::loadFromMemory(const void* data, size_t sizeInBytes) {
|
bool Font::loadFromMemory(const void* data, size_t sizeInBytes) {
|
||||||
fontData_.resize(sizeInBytes);
|
fontData_.resize(sizeInBytes);
|
||||||
memcpy(fontData_.data(), data, sizeInBytes);
|
memcpy(fontData_.data(), data, sizeInBytes);
|
||||||
|
|
||||||
|
// Initialize FreeType library
|
||||||
|
FT_Library library;
|
||||||
|
if (FT_Init_FreeType(&library) != 0) {
|
||||||
|
std::cerr << "Font: Failed to initialize FreeType library" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftLibrary_ = library;
|
||||||
|
|
||||||
|
// Create face from memory
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_New_Memory_Face(library, fontData_.data(), fontData_.size(), 0, &face) != 0) {
|
||||||
|
std::cerr << "Font: Failed to create FreeType face from memory" << std::endl;
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
ftLibrary_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftFace_ = face;
|
||||||
|
|
||||||
|
// Select Unicode charmap
|
||||||
|
FT_Select_Charmap(face, FT_ENCODING_UNICODE);
|
||||||
|
|
||||||
|
// Create stroker for outline rendering
|
||||||
|
FT_Stroker stroker;
|
||||||
|
if (FT_Stroker_New(library, &stroker) != 0) {
|
||||||
|
std::cerr << "Font: Failed to create FreeType stroker" << std::endl;
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
ftFace_ = nullptr;
|
||||||
|
ftLibrary_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ftStroker_ = stroker;
|
||||||
|
|
||||||
loaded_ = true;
|
loaded_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1599,7 +1688,7 @@ void Sprite::draw(RenderTarget& target, RenderStates states) const {
|
||||||
texture_->getNativeHandle(), SDL2Renderer::ShaderType::Sprite);
|
texture_->getNativeHandle(), SDL2Renderer::ShaderType::Sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static cache for font atlases - keyed by (font data pointer, character size)
|
// Static cache for font atlases - keyed by (font pointer, character size)
|
||||||
static std::map<std::pair<const Font*, unsigned int>, FontAtlas> s_fontAtlasCache;
|
static std::map<std::pair<const Font*, unsigned int>, FontAtlas> s_fontAtlasCache;
|
||||||
|
|
||||||
void Text::draw(RenderTarget& target, RenderStates states) const {
|
void Text::draw(RenderTarget& target, RenderStates states) const {
|
||||||
|
|
@ -1610,17 +1699,24 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
|
||||||
auto it = s_fontAtlasCache.find(key);
|
auto it = s_fontAtlasCache.find(key);
|
||||||
if (it == s_fontAtlasCache.end()) {
|
if (it == s_fontAtlasCache.end()) {
|
||||||
FontAtlas atlas;
|
FontAtlas atlas;
|
||||||
if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast<float>(characterSize_))) {
|
// Use the new Font-based loader if FreeType is available, fall back to legacy
|
||||||
return; // Failed to create atlas
|
if (font_->getFTFace()) {
|
||||||
|
if (!atlas.load(font_, static_cast<float>(characterSize_))) {
|
||||||
|
return; // Failed to create atlas
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast<float>(characterSize_))) {
|
||||||
|
return; // Failed to create atlas
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it = s_fontAtlasCache.emplace(key, std::move(atlas)).first;
|
it = s_fontAtlasCache.emplace(key, std::move(atlas)).first;
|
||||||
}
|
}
|
||||||
const FontAtlas& atlas = it->second;
|
FontAtlas& atlas = it->second; // Non-const for on-demand glyph loading
|
||||||
|
|
||||||
Transform combined = states.transform * getTransform();
|
Transform combined = states.transform * getTransform();
|
||||||
|
|
||||||
// Helper lambda to build glyph geometry with a given color and offset
|
// Helper lambda to build glyph geometry with a given color and outline thickness
|
||||||
auto buildGlyphs = [&](const Color& color, float offsetX, float offsetY,
|
auto buildGlyphs = [&](const Color& color, float outlineThickness,
|
||||||
std::vector<float>& verts, std::vector<float>& uvs, std::vector<float>& cols) {
|
std::vector<float>& verts, std::vector<float>& uvs, std::vector<float>& cols) {
|
||||||
float x = 0;
|
float x = 0;
|
||||||
float y = atlas.getAscent();
|
float y = atlas.getAscent();
|
||||||
|
|
@ -1640,15 +1736,34 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
FontAtlas::GlyphInfo glyph;
|
FontAtlas::GlyphInfo glyph;
|
||||||
if (!atlas.getGlyph(static_cast<uint32_t>(c), glyph)) {
|
// Use stroked glyph lookup for outlines, regular for fill
|
||||||
if (!atlas.getGlyph(' ', glyph)) {
|
bool found = false;
|
||||||
continue;
|
if (outlineThickness > 0) {
|
||||||
}
|
found = atlas.getGlyph(static_cast<uint32_t>(c), outlineThickness, glyph);
|
||||||
|
} else {
|
||||||
|
found = atlas.getGlyph(static_cast<uint32_t>(c), glyph);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate quad corners with offset
|
if (!found) {
|
||||||
float x0 = x + glyph.xoff + offsetX;
|
// Try space as fallback
|
||||||
float y0 = y + glyph.yoff + offsetY;
|
if (outlineThickness > 0) {
|
||||||
|
found = atlas.getGlyph(' ', outlineThickness, glyph);
|
||||||
|
} else {
|
||||||
|
found = atlas.getGlyph(' ', glyph);
|
||||||
|
}
|
||||||
|
if (!found) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glyph.width == 0 || glyph.height == 0) {
|
||||||
|
// Invisible character (space), just advance
|
||||||
|
x += glyph.xadvance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate quad corners
|
||||||
|
// For stroked glyphs, the bitmap is larger and offset differently
|
||||||
|
float x0 = x + glyph.xoff;
|
||||||
|
float y0 = y + glyph.yoff;
|
||||||
float x1 = x0 + glyph.width;
|
float x1 = x0 + glyph.width;
|
||||||
float y1 = y0 + glyph.height;
|
float y1 = y0 + glyph.height;
|
||||||
|
|
||||||
|
|
@ -1676,21 +1791,12 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw outline first (if any)
|
// Draw outline first using stroked glyphs (if any)
|
||||||
if (outlineThickness_ > 0 && outlineColor_.a > 0) {
|
if (outlineThickness_ > 0 && outlineColor_.a > 0) {
|
||||||
std::vector<float> outlineVerts, outlineUVs, outlineCols;
|
std::vector<float> outlineVerts, outlineUVs, outlineCols;
|
||||||
|
|
||||||
// Draw at 8 positions around each glyph for outline effect
|
// Use FreeType stroker for proper vector-based outlines
|
||||||
float t = outlineThickness_;
|
buildGlyphs(outlineColor_, outlineThickness_, outlineVerts, outlineUVs, outlineCols);
|
||||||
float offsets[][2] = {
|
|
||||||
{-t, -t}, {0, -t}, {t, -t},
|
|
||||||
{-t, 0}, {t, 0},
|
|
||||||
{-t, t}, {0, t}, {t, t}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto& off : offsets) {
|
|
||||||
buildGlyphs(outlineColor_, off[0], off[1], outlineVerts, outlineUVs, outlineCols);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!outlineVerts.empty()) {
|
if (!outlineVerts.empty()) {
|
||||||
SDL2Renderer::getInstance().drawTriangles(
|
SDL2Renderer::getInstance().drawTriangles(
|
||||||
|
|
@ -1702,9 +1808,9 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw fill text on top
|
// Draw fill text on top using regular (non-stroked) glyphs
|
||||||
std::vector<float> vertices, texcoords, colors;
|
std::vector<float> vertices, texcoords, colors;
|
||||||
buildGlyphs(fillColor_, 0, 0, vertices, texcoords, colors);
|
buildGlyphs(fillColor_, 0.0f, vertices, texcoords, colors);
|
||||||
|
|
||||||
if (!vertices.empty()) {
|
if (!vertices.empty()) {
|
||||||
SDL2Renderer::getInstance().drawTriangles(
|
SDL2Renderer::getInstance().drawTriangles(
|
||||||
|
|
@ -1726,8 +1832,15 @@ FloatRect Text::getLocalBounds() const {
|
||||||
auto it = s_fontAtlasCache.find(key);
|
auto it = s_fontAtlasCache.find(key);
|
||||||
if (it == s_fontAtlasCache.end()) {
|
if (it == s_fontAtlasCache.end()) {
|
||||||
FontAtlas atlas;
|
FontAtlas atlas;
|
||||||
if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast<float>(characterSize_))) {
|
// Use the new Font-based loader if FreeType is available
|
||||||
return FloatRect(0, 0, 0, 0);
|
if (font_->getFTFace()) {
|
||||||
|
if (!atlas.load(font_, static_cast<float>(characterSize_))) {
|
||||||
|
return FloatRect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast<float>(characterSize_))) {
|
||||||
|
return FloatRect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it = s_fontAtlasCache.emplace(key, std::move(atlas)).first;
|
it = s_fontAtlasCache.emplace(key, std::move(atlas)).first;
|
||||||
}
|
}
|
||||||
|
|
@ -1863,7 +1976,7 @@ bool Shader::isAvailable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// FontAtlas Implementation
|
// FontAtlas Implementation (FreeType-based)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
FontAtlas::FontAtlas() = default;
|
FontAtlas::FontAtlas() = default;
|
||||||
|
|
@ -1874,12 +1987,17 @@ FontAtlas::FontAtlas(FontAtlas&& other) noexcept
|
||||||
, ascent_(other.ascent_)
|
, ascent_(other.ascent_)
|
||||||
, descent_(other.descent_)
|
, descent_(other.descent_)
|
||||||
, lineHeight_(other.lineHeight_)
|
, lineHeight_(other.lineHeight_)
|
||||||
|
, font_(other.font_)
|
||||||
|
, atlasPixels_(std::move(other.atlasPixels_))
|
||||||
|
, atlasX_(other.atlasX_)
|
||||||
|
, atlasY_(other.atlasY_)
|
||||||
|
, atlasRowHeight_(other.atlasRowHeight_)
|
||||||
, glyphCache_(std::move(other.glyphCache_))
|
, glyphCache_(std::move(other.glyphCache_))
|
||||||
, stbFontInfo_(other.stbFontInfo_)
|
, simpleGlyphCache_(std::move(other.simpleGlyphCache_))
|
||||||
{
|
{
|
||||||
// Clear source to prevent double-deletion
|
// Clear source to prevent double-deletion
|
||||||
other.textureId_ = 0;
|
other.textureId_ = 0;
|
||||||
other.stbFontInfo_ = nullptr;
|
other.font_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
FontAtlas& FontAtlas::operator=(FontAtlas&& other) noexcept {
|
FontAtlas& FontAtlas::operator=(FontAtlas&& other) noexcept {
|
||||||
|
|
@ -1888,9 +2006,6 @@ FontAtlas& FontAtlas::operator=(FontAtlas&& other) noexcept {
|
||||||
if (textureId_) {
|
if (textureId_) {
|
||||||
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
||||||
}
|
}
|
||||||
if (stbFontInfo_) {
|
|
||||||
delete static_cast<stbtt_fontinfo*>(stbFontInfo_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer ownership
|
// Transfer ownership
|
||||||
textureId_ = other.textureId_;
|
textureId_ = other.textureId_;
|
||||||
|
|
@ -1898,12 +2013,17 @@ FontAtlas& FontAtlas::operator=(FontAtlas&& other) noexcept {
|
||||||
ascent_ = other.ascent_;
|
ascent_ = other.ascent_;
|
||||||
descent_ = other.descent_;
|
descent_ = other.descent_;
|
||||||
lineHeight_ = other.lineHeight_;
|
lineHeight_ = other.lineHeight_;
|
||||||
|
font_ = other.font_;
|
||||||
|
atlasPixels_ = std::move(other.atlasPixels_);
|
||||||
|
atlasX_ = other.atlasX_;
|
||||||
|
atlasY_ = other.atlasY_;
|
||||||
|
atlasRowHeight_ = other.atlasRowHeight_;
|
||||||
glyphCache_ = std::move(other.glyphCache_);
|
glyphCache_ = std::move(other.glyphCache_);
|
||||||
stbFontInfo_ = other.stbFontInfo_;
|
simpleGlyphCache_ = std::move(other.simpleGlyphCache_);
|
||||||
|
|
||||||
// Clear source to prevent double-deletion
|
// Clear source to prevent double-deletion
|
||||||
other.textureId_ = 0;
|
other.textureId_ = 0;
|
||||||
other.stbFontInfo_ = nullptr;
|
other.font_ = nullptr;
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
@ -1912,107 +2032,361 @@ FontAtlas::~FontAtlas() {
|
||||||
if (textureId_) {
|
if (textureId_) {
|
||||||
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
SDL2Renderer::getInstance().deleteTexture(textureId_);
|
||||||
}
|
}
|
||||||
if (stbFontInfo_) {
|
|
||||||
delete static_cast<stbtt_fontinfo*>(stbFontInfo_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FontAtlas::load(const unsigned char* fontData, size_t dataSize, float fontSize) {
|
uint64_t FontAtlas::makeKey(uint32_t codepoint, float outlineThickness) {
|
||||||
fontSize_ = fontSize;
|
// Quantize outline thickness to 0.5px increments for cache key
|
||||||
|
uint32_t outlineKey = static_cast<uint32_t>(outlineThickness * 2.0f);
|
||||||
|
return (static_cast<uint64_t>(outlineKey) << 32) | codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
stbtt_fontinfo* info = new stbtt_fontinfo();
|
bool FontAtlas::load(const Font* font, float fontSize) {
|
||||||
if (!stbtt_InitFont(info, fontData, 0)) {
|
if (!font || !font->isLoaded() || !font->getFTFace()) {
|
||||||
delete info;
|
std::cerr << "FontAtlas: Invalid font or font not loaded" << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
stbFontInfo_ = info;
|
font_ = font;
|
||||||
|
fontSize_ = fontSize;
|
||||||
|
|
||||||
// Get font metrics
|
FT_Face face = static_cast<FT_Face>(font->getFTFace());
|
||||||
int ascent, descent, lineGap;
|
|
||||||
stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap);
|
|
||||||
|
|
||||||
float scale = stbtt_ScaleForPixelHeight(info, fontSize);
|
// Set pixel size
|
||||||
ascent_ = ascent * scale;
|
if (FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(fontSize)) != 0) {
|
||||||
descent_ = descent * scale;
|
std::cerr << "FontAtlas: Failed to set pixel size" << std::endl;
|
||||||
lineHeight_ = (ascent - descent + lineGap) * scale;
|
return false;
|
||||||
|
|
||||||
// Create glyph atlas (simple ASCII for now)
|
|
||||||
const int atlasSize = 512;
|
|
||||||
std::vector<unsigned char> atlasPixels(atlasSize * atlasSize, 0);
|
|
||||||
|
|
||||||
int x = 1, y = 1;
|
|
||||||
int rowHeight = 0;
|
|
||||||
|
|
||||||
for (uint32_t c = 32; c < 128; ++c) {
|
|
||||||
int advance, lsb;
|
|
||||||
stbtt_GetCodepointHMetrics(info, c, &advance, &lsb);
|
|
||||||
|
|
||||||
int x0, y0, x1, y1;
|
|
||||||
stbtt_GetCodepointBitmapBox(info, c, scale, scale, &x0, &y0, &x1, &y1);
|
|
||||||
|
|
||||||
int w = x1 - x0;
|
|
||||||
int h = y1 - y0;
|
|
||||||
|
|
||||||
if (x + w + 1 >= atlasSize) {
|
|
||||||
x = 1;
|
|
||||||
y += rowHeight + 1;
|
|
||||||
rowHeight = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y + h + 1 >= atlasSize) {
|
|
||||||
break; // Atlas full
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render glyph to atlas
|
|
||||||
stbtt_MakeCodepointBitmap(info, &atlasPixels[y * atlasSize + x], w, h, atlasSize, scale, scale, c);
|
|
||||||
|
|
||||||
GlyphInfo glyph;
|
|
||||||
glyph.u0 = x / (float)atlasSize;
|
|
||||||
glyph.v0 = y / (float)atlasSize;
|
|
||||||
glyph.u1 = (x + w) / (float)atlasSize;
|
|
||||||
glyph.v1 = (y + h) / (float)atlasSize;
|
|
||||||
glyph.xoff = x0;
|
|
||||||
glyph.yoff = y0;
|
|
||||||
glyph.xadvance = advance * scale;
|
|
||||||
glyph.width = w;
|
|
||||||
glyph.height = h;
|
|
||||||
|
|
||||||
glyphCache_[c] = glyph;
|
|
||||||
|
|
||||||
x += w + 1;
|
|
||||||
rowHeight = std::max(rowHeight, h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert single-channel to RGBA
|
// Get font metrics (in 26.6 fixed-point format)
|
||||||
std::vector<unsigned char> rgbaPixels(atlasSize * atlasSize * 4);
|
ascent_ = face->size->metrics.ascender / 64.0f;
|
||||||
for (int i = 0; i < atlasSize * atlasSize; ++i) {
|
descent_ = face->size->metrics.descender / 64.0f;
|
||||||
|
lineHeight_ = face->size->metrics.height / 64.0f;
|
||||||
|
|
||||||
|
// Initialize atlas
|
||||||
|
atlasPixels_.resize(ATLAS_SIZE * ATLAS_SIZE, 0);
|
||||||
|
atlasX_ = 1;
|
||||||
|
atlasY_ = 1;
|
||||||
|
atlasRowHeight_ = 0;
|
||||||
|
|
||||||
|
// Pre-load ASCII glyphs without outline (directly, not via loadGlyph to avoid texture updates)
|
||||||
|
for (uint32_t c = 32; c < 128; ++c) {
|
||||||
|
FT_UInt glyphIndex = FT_Get_Char_Index(face, c);
|
||||||
|
if (glyphIndex == 0) continue;
|
||||||
|
|
||||||
|
if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_RENDER) != 0) continue;
|
||||||
|
|
||||||
|
FT_Bitmap& bitmap = face->glyph->bitmap;
|
||||||
|
int w = bitmap.width;
|
||||||
|
int h = bitmap.rows;
|
||||||
|
|
||||||
|
GlyphInfo info;
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
info.u0 = info.v0 = info.u1 = info.v1 = 0;
|
||||||
|
info.xoff = 0;
|
||||||
|
info.yoff = 0;
|
||||||
|
info.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
info.width = 0;
|
||||||
|
info.height = 0;
|
||||||
|
} else {
|
||||||
|
if (atlasX_ + w + 1 >= ATLAS_SIZE) {
|
||||||
|
atlasX_ = 1;
|
||||||
|
atlasY_ += atlasRowHeight_ + 1;
|
||||||
|
atlasRowHeight_ = 0;
|
||||||
|
}
|
||||||
|
if (atlasY_ + h + 1 >= ATLAS_SIZE) break;
|
||||||
|
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
atlasPixels_[(atlasY_ + y) * ATLAS_SIZE + atlasX_ + x] = bitmap.buffer[y * bitmap.pitch + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.u0 = atlasX_ / (float)ATLAS_SIZE;
|
||||||
|
info.v0 = atlasY_ / (float)ATLAS_SIZE;
|
||||||
|
info.u1 = (atlasX_ + w) / (float)ATLAS_SIZE;
|
||||||
|
info.v1 = (atlasY_ + h) / (float)ATLAS_SIZE;
|
||||||
|
info.xoff = face->glyph->bitmap_left;
|
||||||
|
info.yoff = -face->glyph->bitmap_top;
|
||||||
|
info.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
info.width = w;
|
||||||
|
info.height = h;
|
||||||
|
|
||||||
|
atlasX_ += w + 1;
|
||||||
|
atlasRowHeight_ = std::max(atlasRowHeight_, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleGlyphCache_[c] = info;
|
||||||
|
glyphCache_[makeKey(c, 0.0f)] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert single-channel to RGBA and create texture ONCE
|
||||||
|
std::vector<unsigned char> rgbaPixels(ATLAS_SIZE * ATLAS_SIZE * 4);
|
||||||
|
for (int i = 0; i < ATLAS_SIZE * ATLAS_SIZE; ++i) {
|
||||||
rgbaPixels[i * 4 + 0] = 255;
|
rgbaPixels[i * 4 + 0] = 255;
|
||||||
rgbaPixels[i * 4 + 1] = 255;
|
rgbaPixels[i * 4 + 1] = 255;
|
||||||
rgbaPixels[i * 4 + 2] = 255;
|
rgbaPixels[i * 4 + 2] = 255;
|
||||||
rgbaPixels[i * 4 + 3] = atlasPixels[i];
|
rgbaPixels[i * 4 + 3] = atlasPixels_[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
textureId_ = SDL2Renderer::getInstance().createTexture(atlasSize, atlasSize, rgbaPixels.data());
|
textureId_ = SDL2Renderer::getInstance().createTexture(ATLAS_SIZE, ATLAS_SIZE, rgbaPixels.data());
|
||||||
|
|
||||||
// Debug: Check if any glyph pixels were actually rendered
|
std::cout << "FontAtlas: created " << ATLAS_SIZE << "x" << ATLAS_SIZE
|
||||||
int nonZeroPixels = 0;
|
<< " atlas with " << simpleGlyphCache_.size() << " glyphs, textureId=" << textureId_ << std::endl;
|
||||||
for (int i = 0; i < atlasSize * atlasSize; ++i) {
|
|
||||||
if (atlasPixels[i] > 0) nonZeroPixels++;
|
|
||||||
}
|
|
||||||
std::cout << "FontAtlas: created " << atlasSize << "x" << atlasSize
|
|
||||||
<< " atlas with " << glyphCache_.size() << " glyphs, "
|
|
||||||
<< nonZeroPixels << " non-zero pixels, textureId=" << textureId_ << std::endl;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FontAtlas::getGlyph(uint32_t codepoint, GlyphInfo& info) const {
|
// Legacy interface using raw font data - creates temporary FreeType objects
|
||||||
auto it = glyphCache_.find(codepoint);
|
// Note: This path doesn't support on-demand stroked glyph loading since FreeType
|
||||||
|
// objects are freed after initialization. Use Font-based load() for full features.
|
||||||
|
bool FontAtlas::load(const unsigned char* fontData, size_t dataSize, float fontSize) {
|
||||||
|
fontSize_ = fontSize;
|
||||||
|
|
||||||
|
// Initialize FreeType for this atlas
|
||||||
|
FT_Library library;
|
||||||
|
if (FT_Init_FreeType(&library) != 0) {
|
||||||
|
std::cerr << "FontAtlas: Failed to initialize FreeType" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_New_Memory_Face(library, fontData, dataSize, 0, &face) != 0) {
|
||||||
|
std::cerr << "FontAtlas: Failed to create FreeType face" << std::endl;
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set pixel size
|
||||||
|
FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(fontSize));
|
||||||
|
|
||||||
|
// Get font metrics
|
||||||
|
ascent_ = face->size->metrics.ascender / 64.0f;
|
||||||
|
descent_ = face->size->metrics.descender / 64.0f;
|
||||||
|
lineHeight_ = face->size->metrics.height / 64.0f;
|
||||||
|
|
||||||
|
// Create glyph atlas - pre-load all ASCII glyphs
|
||||||
|
atlasPixels_.resize(ATLAS_SIZE * ATLAS_SIZE, 0);
|
||||||
|
atlasX_ = 1;
|
||||||
|
atlasY_ = 1;
|
||||||
|
atlasRowHeight_ = 0;
|
||||||
|
|
||||||
|
for (uint32_t c = 32; c < 128; ++c) {
|
||||||
|
FT_UInt glyphIndex = FT_Get_Char_Index(face, c);
|
||||||
|
if (glyphIndex == 0) continue;
|
||||||
|
|
||||||
|
if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_RENDER) != 0) continue;
|
||||||
|
|
||||||
|
FT_Bitmap& bitmap = face->glyph->bitmap;
|
||||||
|
int w = bitmap.width;
|
||||||
|
int h = bitmap.rows;
|
||||||
|
|
||||||
|
GlyphInfo glyph;
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
glyph.u0 = glyph.v0 = glyph.u1 = glyph.v1 = 0;
|
||||||
|
glyph.xoff = 0;
|
||||||
|
glyph.yoff = 0;
|
||||||
|
glyph.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
glyph.width = 0;
|
||||||
|
glyph.height = 0;
|
||||||
|
} else {
|
||||||
|
if (atlasX_ + w + 1 >= ATLAS_SIZE) {
|
||||||
|
atlasX_ = 1;
|
||||||
|
atlasY_ += atlasRowHeight_ + 1;
|
||||||
|
atlasRowHeight_ = 0;
|
||||||
|
}
|
||||||
|
if (atlasY_ + h + 1 >= ATLAS_SIZE) break;
|
||||||
|
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
atlasPixels_[(atlasY_ + y) * ATLAS_SIZE + atlasX_ + x] = bitmap.buffer[y * bitmap.pitch + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glyph.u0 = atlasX_ / (float)ATLAS_SIZE;
|
||||||
|
glyph.v0 = atlasY_ / (float)ATLAS_SIZE;
|
||||||
|
glyph.u1 = (atlasX_ + w) / (float)ATLAS_SIZE;
|
||||||
|
glyph.v1 = (atlasY_ + h) / (float)ATLAS_SIZE;
|
||||||
|
glyph.xoff = face->glyph->bitmap_left;
|
||||||
|
glyph.yoff = -face->glyph->bitmap_top;
|
||||||
|
glyph.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
glyph.width = w;
|
||||||
|
glyph.height = h;
|
||||||
|
|
||||||
|
atlasX_ += w + 1;
|
||||||
|
atlasRowHeight_ = std::max(atlasRowHeight_, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleGlyphCache_[c] = glyph;
|
||||||
|
glyphCache_[makeKey(c, 0.0f)] = glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temporary FreeType objects
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
|
||||||
|
// Convert to RGBA and create texture ONCE
|
||||||
|
std::vector<unsigned char> rgbaPixels(ATLAS_SIZE * ATLAS_SIZE * 4);
|
||||||
|
for (int i = 0; i < ATLAS_SIZE * ATLAS_SIZE; ++i) {
|
||||||
|
rgbaPixels[i * 4 + 0] = 255;
|
||||||
|
rgbaPixels[i * 4 + 1] = 255;
|
||||||
|
rgbaPixels[i * 4 + 2] = 255;
|
||||||
|
rgbaPixels[i * 4 + 3] = atlasPixels_[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
textureId_ = SDL2Renderer::getInstance().createTexture(ATLAS_SIZE, ATLAS_SIZE, rgbaPixels.data());
|
||||||
|
|
||||||
|
std::cout << "FontAtlas: created " << ATLAS_SIZE << "x" << ATLAS_SIZE
|
||||||
|
<< " atlas with " << simpleGlyphCache_.size() << " glyphs, textureId=" << textureId_ << std::endl;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlas::loadGlyph(uint32_t codepoint, float outlineThickness) {
|
||||||
|
if (!font_ || !font_->getFTFace()) return false;
|
||||||
|
|
||||||
|
FT_Face face = static_cast<FT_Face>(font_->getFTFace());
|
||||||
|
|
||||||
|
// Make sure pixel size is set
|
||||||
|
FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(fontSize_));
|
||||||
|
|
||||||
|
FT_UInt glyphIndex = FT_Get_Char_Index(face, codepoint);
|
||||||
|
if (glyphIndex == 0) return false;
|
||||||
|
|
||||||
|
// Load glyph without rendering (we may need to stroke it first)
|
||||||
|
if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_DEFAULT) != 0) return false;
|
||||||
|
|
||||||
|
FT_Glyph glyph;
|
||||||
|
if (FT_Get_Glyph(face->glyph, &glyph) != 0) return false;
|
||||||
|
|
||||||
|
// Apply stroking if outline thickness > 0
|
||||||
|
if (outlineThickness > 0.0f && glyph->format == FT_GLYPH_FORMAT_OUTLINE) {
|
||||||
|
FT_Stroker stroker = static_cast<FT_Stroker>(font_->getFTStroker());
|
||||||
|
if (stroker) {
|
||||||
|
// Set stroker parameters (thickness is in 26.6 fixed-point)
|
||||||
|
FT_Stroker_Set(stroker,
|
||||||
|
static_cast<FT_Fixed>(outlineThickness * 64.0f),
|
||||||
|
FT_STROKER_LINECAP_ROUND,
|
||||||
|
FT_STROKER_LINEJOIN_ROUND,
|
||||||
|
0);
|
||||||
|
|
||||||
|
// Stroke the glyph outline (replaces outline with stroked version)
|
||||||
|
FT_Glyph_Stroke(&glyph, stroker, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to bitmap
|
||||||
|
if (FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, nullptr, 1) != 0) {
|
||||||
|
FT_Done_Glyph(glyph);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_BitmapGlyph bitmapGlyph = reinterpret_cast<FT_BitmapGlyph>(glyph);
|
||||||
|
FT_Bitmap& bitmap = bitmapGlyph->bitmap;
|
||||||
|
|
||||||
|
int w = bitmap.width;
|
||||||
|
int h = bitmap.rows;
|
||||||
|
|
||||||
|
GlyphInfo info;
|
||||||
|
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
// Space or invisible character
|
||||||
|
info.u0 = info.v0 = info.u1 = info.v1 = 0;
|
||||||
|
info.xoff = 0;
|
||||||
|
info.yoff = 0;
|
||||||
|
info.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
info.width = 0;
|
||||||
|
info.height = 0;
|
||||||
|
} else {
|
||||||
|
// Check if we need to move to next row
|
||||||
|
if (atlasX_ + w + 1 >= ATLAS_SIZE) {
|
||||||
|
atlasX_ = 1;
|
||||||
|
atlasY_ += atlasRowHeight_ + 1;
|
||||||
|
atlasRowHeight_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atlasY_ + h + 1 >= ATLAS_SIZE) {
|
||||||
|
std::cerr << "FontAtlas: Atlas full" << std::endl;
|
||||||
|
FT_Done_Glyph(glyph);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy bitmap to atlas pixel buffer
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
atlasPixels_[(atlasY_ + y) * ATLAS_SIZE + atlasX_ + x] = bitmap.buffer[y * bitmap.pitch + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.u0 = atlasX_ / (float)ATLAS_SIZE;
|
||||||
|
info.v0 = atlasY_ / (float)ATLAS_SIZE;
|
||||||
|
info.u1 = (atlasX_ + w) / (float)ATLAS_SIZE;
|
||||||
|
info.v1 = (atlasY_ + h) / (float)ATLAS_SIZE;
|
||||||
|
info.xoff = bitmapGlyph->left;
|
||||||
|
info.yoff = -bitmapGlyph->top; // FreeType uses bottom-up
|
||||||
|
info.xadvance = face->glyph->advance.x / 64.0f;
|
||||||
|
info.width = w;
|
||||||
|
info.height = h;
|
||||||
|
|
||||||
|
// Update ONLY the region of the texture that changed (not the whole atlas!)
|
||||||
|
if (textureId_) {
|
||||||
|
// Convert just this glyph region to RGBA
|
||||||
|
std::vector<unsigned char> glyphRGBA(w * h * 4);
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
int srcIdx = (atlasY_ + y) * ATLAS_SIZE + atlasX_ + x;
|
||||||
|
int dstIdx = (y * w + x) * 4;
|
||||||
|
glyphRGBA[dstIdx + 0] = 255;
|
||||||
|
glyphRGBA[dstIdx + 1] = 255;
|
||||||
|
glyphRGBA[dstIdx + 2] = 255;
|
||||||
|
glyphRGBA[dstIdx + 3] = atlasPixels_[srcIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use glTexSubImage2D to update just the glyph region
|
||||||
|
SDL2Renderer::getInstance().updateTexture(textureId_, atlasX_, atlasY_, w, h, glyphRGBA.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
atlasX_ += w + 1;
|
||||||
|
atlasRowHeight_ = std::max(atlasRowHeight_, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in appropriate cache
|
||||||
|
if (outlineThickness == 0.0f) {
|
||||||
|
simpleGlyphCache_[codepoint] = info;
|
||||||
|
}
|
||||||
|
uint64_t key = makeKey(codepoint, outlineThickness);
|
||||||
|
glyphCache_[key] = info;
|
||||||
|
|
||||||
|
FT_Done_Glyph(glyph);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlas::getGlyph(uint32_t codepoint, float outlineThickness, GlyphInfo& info) {
|
||||||
|
uint64_t key = makeKey(codepoint, outlineThickness);
|
||||||
|
|
||||||
|
auto it = glyphCache_.find(key);
|
||||||
if (it != glyphCache_.end()) {
|
if (it != glyphCache_.end()) {
|
||||||
info = it->second;
|
info = it->second;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to load the glyph on-demand
|
||||||
|
if (loadGlyph(codepoint, outlineThickness)) {
|
||||||
|
it = glyphCache_.find(key);
|
||||||
|
if (it != glyphCache_.end()) {
|
||||||
|
info = it->second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlas::getGlyph(uint32_t codepoint, GlyphInfo& info) const {
|
||||||
|
auto it = simpleGlyphCache_.find(codepoint);
|
||||||
|
if (it != simpleGlyphCache_.end()) {
|
||||||
|
info = it->second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,10 @@ public:
|
||||||
FontAtlas(const FontAtlas&) = delete;
|
FontAtlas(const FontAtlas&) = delete;
|
||||||
FontAtlas& operator=(const FontAtlas&) = delete;
|
FontAtlas& operator=(const FontAtlas&) = delete;
|
||||||
|
|
||||||
// Load font data
|
// Load font using Font's FreeType handles
|
||||||
|
bool load(const class Font* font, float fontSize);
|
||||||
|
|
||||||
|
// Legacy interface for backwards compatibility (uses global FT library)
|
||||||
bool load(const unsigned char* fontData, size_t dataSize, float fontSize);
|
bool load(const unsigned char* fontData, size_t dataSize, float fontSize);
|
||||||
|
|
||||||
// Get texture atlas
|
// Get texture atlas
|
||||||
|
|
@ -176,6 +179,10 @@ public:
|
||||||
float width, height; // Glyph dimensions in pixels
|
float width, height; // Glyph dimensions in pixels
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get glyph with optional outline thickness (0 = no outline)
|
||||||
|
bool getGlyph(uint32_t codepoint, float outlineThickness, GlyphInfo& info);
|
||||||
|
|
||||||
|
// Legacy interface (no outline)
|
||||||
bool getGlyph(uint32_t codepoint, GlyphInfo& info) const;
|
bool getGlyph(uint32_t codepoint, GlyphInfo& info) const;
|
||||||
|
|
||||||
// Get font metrics
|
// Get font metrics
|
||||||
|
|
@ -190,11 +197,28 @@ private:
|
||||||
float descent_ = 0;
|
float descent_ = 0;
|
||||||
float lineHeight_ = 0;
|
float lineHeight_ = 0;
|
||||||
|
|
||||||
// Glyph cache - maps codepoint to glyph info
|
// FreeType handles (stored for on-demand glyph loading)
|
||||||
std::unordered_map<uint32_t, GlyphInfo> glyphCache_;
|
const class Font* font_ = nullptr;
|
||||||
|
|
||||||
// stb_truetype font info (opaque pointer to avoid header inclusion)
|
// Atlas packing state for on-demand glyph loading
|
||||||
void* stbFontInfo_ = nullptr;
|
static const int ATLAS_SIZE = 1024;
|
||||||
|
std::vector<unsigned char> atlasPixels_;
|
||||||
|
int atlasX_ = 1;
|
||||||
|
int atlasY_ = 1;
|
||||||
|
int atlasRowHeight_ = 0;
|
||||||
|
|
||||||
|
// Glyph cache - maps (codepoint, outlineThickness) to glyph info
|
||||||
|
// Key: (outlineThickness bits << 32) | codepoint
|
||||||
|
std::unordered_map<uint64_t, GlyphInfo> glyphCache_;
|
||||||
|
|
||||||
|
// Simple glyph cache for backwards compatibility (no outline)
|
||||||
|
std::unordered_map<uint32_t, GlyphInfo> simpleGlyphCache_;
|
||||||
|
|
||||||
|
// Helper to make cache key
|
||||||
|
static uint64_t makeKey(uint32_t codepoint, float outlineThickness);
|
||||||
|
|
||||||
|
// Load a glyph on-demand with optional stroking
|
||||||
|
bool loadGlyph(uint32_t codepoint, float outlineThickness);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sf
|
} // namespace sf
|
||||||
|
|
|
||||||
|
|
@ -722,16 +722,22 @@ protected:
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
class Font {
|
class Font {
|
||||||
// SDL2-specific: font data for stb_truetype
|
// SDL2-specific: font data for FreeType
|
||||||
std::vector<unsigned char> fontData_;
|
std::vector<unsigned char> fontData_;
|
||||||
bool loaded_ = false;
|
bool loaded_ = false;
|
||||||
|
|
||||||
|
// FreeType handles (void* to avoid header dependency in .h)
|
||||||
|
void* ftLibrary_ = nullptr; // FT_Library
|
||||||
|
void* ftFace_ = nullptr; // FT_Face
|
||||||
|
void* ftStroker_ = nullptr; // FT_Stroker
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct Info {
|
struct Info {
|
||||||
std::string family;
|
std::string family;
|
||||||
};
|
};
|
||||||
|
|
||||||
Font() = default;
|
Font() = default;
|
||||||
|
~Font(); // Destructor for FreeType cleanup - implemented in SDL2Renderer.cpp
|
||||||
bool loadFromFile(const std::string& filename); // Implemented in SDL2Renderer.cpp
|
bool loadFromFile(const std::string& filename); // Implemented in SDL2Renderer.cpp
|
||||||
bool loadFromMemory(const void* data, size_t sizeInBytes); // Implemented in SDL2Renderer.cpp
|
bool loadFromMemory(const void* data, size_t sizeInBytes); // Implemented in SDL2Renderer.cpp
|
||||||
const Info& getInfo() const { static Info info; return info; }
|
const Info& getInfo() const { static Info info; return info; }
|
||||||
|
|
@ -740,6 +746,11 @@ public:
|
||||||
const unsigned char* getData() const { return fontData_.data(); }
|
const unsigned char* getData() const { return fontData_.data(); }
|
||||||
size_t getDataSize() const { return fontData_.size(); }
|
size_t getDataSize() const { return fontData_.size(); }
|
||||||
bool isLoaded() const { return loaded_; }
|
bool isLoaded() const { return loaded_; }
|
||||||
|
|
||||||
|
// FreeType accessors
|
||||||
|
void* getFTFace() const { return ftFace_; }
|
||||||
|
void* getFTStroker() const { return ftStroker_; }
|
||||||
|
void* getFTLibrary() const { return ftLibrary_; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class Text : public Drawable, public Transformable {
|
class Text : public Drawable, public Transformable {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
#canvas {
|
#canvas {
|
||||||
border: 2px solid #e94560;
|
/* border: 2px solid #e94560; -- disabled per Emscripten docs, causes alignment issues */
|
||||||
background: #000;
|
background: #000;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
@ -56,7 +56,6 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#canvas:focus {
|
#canvas:focus {
|
||||||
border-color: #4ecca3;
|
|
||||||
box-shadow: 0 0 10px rgba(78, 204, 163, 0.5);
|
box-shadow: 0 0 10px rgba(78, 204, 163, 0.5);
|
||||||
}
|
}
|
||||||
.repl-panel {
|
.repl-panel {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue