Add text outline support for SDL2/WebGL backend

Implements SFML-compatible text outlines using multi-pass rendering:
- Draws text 8 times at offset positions (N, NE, E, SE, S, SW, W, NW)
  with outline color, then draws fill text on top
- Uses existing outlineThickness_ and outlineColor_ properties
- Refactored Text::draw() with helper lambda for code reuse
- Removed debug logging code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-31 14:01:50 -05:00
commit 1abec8f808

View file

@ -1602,8 +1602,6 @@ void Sprite::draw(RenderTarget& target, RenderStates states) const {
// Static cache for font atlases - keyed by (font data pointer, character size) // Static cache for font atlases - keyed by (font data 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;
static int textDebugCount = 0;
void Text::draw(RenderTarget& target, RenderStates states) const { void Text::draw(RenderTarget& target, RenderStates states) const {
if (!font_ || string_.empty() || !font_->isLoaded()) return; if (!font_ || string_.empty() || !font_->isLoaded()) return;
@ -1613,40 +1611,28 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
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_))) { if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast<float>(characterSize_))) {
std::cerr << "Text::draw: Failed to create font atlas!" << std::endl;
return; // Failed to create atlas return; // Failed to create atlas
} }
std::cout << "Text::draw: Created font atlas, textureId=" << atlas.getTextureId() << std::endl;
it = s_fontAtlasCache.emplace(key, std::move(atlas)).first; it = s_fontAtlasCache.emplace(key, std::move(atlas)).first;
} }
const FontAtlas& atlas = it->second; const FontAtlas& atlas = it->second;
// Debug: log first few text draws
if (textDebugCount < 3) {
std::cout << "Text::draw: string='" << string_.substr(0, 20) << "' atlasTexId=" << atlas.getTextureId()
<< " fillColor=(" << (int)fillColor_.r << "," << (int)fillColor_.g << "," << (int)fillColor_.b << ")" << std::endl;
textDebugCount++;
}
Transform combined = states.transform * getTransform(); Transform combined = states.transform * getTransform();
// Build vertex data for all glyphs // Helper lambda to build glyph geometry with a given color and offset
std::vector<float> vertices; auto buildGlyphs = [&](const Color& color, float offsetX, float offsetY,
std::vector<float> texcoords; std::vector<float>& verts, std::vector<float>& uvs, std::vector<float>& cols) {
std::vector<float> colors;
float x = 0; float x = 0;
float y = atlas.getAscent(); // Start at baseline float y = atlas.getAscent();
float r = fillColor_.r / 255.0f; float r = color.r / 255.0f;
float g = fillColor_.g / 255.0f; float g = color.g / 255.0f;
float b = fillColor_.b / 255.0f; float b = color.b / 255.0f;
float a = fillColor_.a / 255.0f; float a = color.a / 255.0f;
for (size_t i = 0; i < string_.size(); ++i) { for (size_t i = 0; i < string_.size(); ++i) {
char c = string_[i]; char c = string_[i];
// Handle newlines
if (c == '\n') { if (c == '\n') {
x = 0; x = 0;
y += atlas.getLineHeight(); y += atlas.getLineHeight();
@ -1655,15 +1641,14 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
FontAtlas::GlyphInfo glyph; FontAtlas::GlyphInfo glyph;
if (!atlas.getGlyph(static_cast<uint32_t>(c), glyph)) { if (!atlas.getGlyph(static_cast<uint32_t>(c), glyph)) {
// Try space for unknown glyphs
if (!atlas.getGlyph(' ', glyph)) { if (!atlas.getGlyph(' ', glyph)) {
continue; continue;
} }
} }
// Calculate quad corners in local space // Calculate quad corners with offset
float x0 = x + glyph.xoff; float x0 = x + glyph.xoff + offsetX;
float y0 = y + glyph.yoff; float y0 = y + glyph.yoff + offsetY;
float x1 = x0 + glyph.width; float x1 = x0 + glyph.width;
float y1 = y0 + glyph.height; float y1 = y0 + glyph.height;
@ -1673,36 +1658,55 @@ void Text::draw(RenderTarget& target, RenderStates states) const {
Vector2f p2 = combined.transformPoint(x1, y1); Vector2f p2 = combined.transformPoint(x1, y1);
Vector2f p3 = combined.transformPoint(x0, y1); Vector2f p3 = combined.transformPoint(x0, y1);
// Two triangles for quad verts.insert(verts.end(), {
vertices.insert(vertices.end(), { p0.x, p0.y, p1.x, p1.y, p2.x, p2.y,
p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, // Triangle 1 p0.x, p0.y, p2.x, p2.y, p3.x, p3.y
p0.x, p0.y, p2.x, p2.y, p3.x, p3.y // Triangle 2
}); });
texcoords.insert(texcoords.end(), { uvs.insert(uvs.end(), {
glyph.u0, glyph.v0, glyph.u1, glyph.v0, glyph.u1, glyph.v1, // Triangle 1 glyph.u0, glyph.v0, glyph.u1, glyph.v0, glyph.u1, glyph.v1,
glyph.u0, glyph.v0, glyph.u1, glyph.v1, glyph.u0, glyph.v1 // Triangle 2 glyph.u0, glyph.v0, glyph.u1, glyph.v1, glyph.u0, glyph.v1
}); });
// 6 vertices * 4 color components
for (int v = 0; v < 6; ++v) { for (int v = 0; v < 6; ++v) {
colors.insert(colors.end(), {r, g, b, a}); cols.insert(cols.end(), {r, g, b, a});
} }
x += glyph.xadvance; x += glyph.xadvance;
} }
};
if (!vertices.empty()) { // Draw outline first (if any)
// Debug: log first glyph's UVs if (outlineThickness_ > 0 && outlineColor_.a > 0) {
static int uvDebugCount = 0; std::vector<float> outlineVerts, outlineUVs, outlineCols;
if (uvDebugCount < 2 && texcoords.size() >= 12) {
std::cout << "Text UVs for first glyph: u0=" << texcoords[0] << " v0=" << texcoords[1] // Draw at 8 positions around each glyph for outline effect
<< " u1=" << texcoords[4] << " v1=" << texcoords[5] float t = outlineThickness_;
<< " (vertexCount=" << (vertices.size()/2) << ")" << std::endl; float offsets[][2] = {
uvDebugCount++; {-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);
} }
// Use text shader (uses alpha from texture, not full RGBA multiply) if (!outlineVerts.empty()) {
SDL2Renderer::getInstance().drawTriangles(
outlineVerts.data(), outlineVerts.size() / 2,
outlineCols.data(), outlineUVs.data(),
atlas.getTextureId(),
SDL2Renderer::ShaderType::Text
);
}
}
// Draw fill text on top
std::vector<float> vertices, texcoords, colors;
buildGlyphs(fillColor_, 0, 0, vertices, texcoords, colors);
if (!vertices.empty()) {
SDL2Renderer::getInstance().drawTriangles( SDL2Renderer::getInstance().drawTriangles(
vertices.data(), vertices.size() / 2, vertices.data(), vertices.size() / 2,
colors.data(), texcoords.data(), colors.data(), texcoords.data(),