diff --git a/CMakeLists.txt b/CMakeLists.txt index 265f910..49d3625 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -269,6 +269,10 @@ if(EMSCRIPTEN) --preload-file=${CMAKE_SOURCE_DIR}/src/scripts@/scripts # Preload assets --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets + # Use custom HTML shell for crisp pixel rendering + --shell-file=${CMAKE_SOURCE_DIR}/src/shell.html + # Pre-JS to fix browser zoom causing undefined values in events + --pre-js=${CMAKE_SOURCE_DIR}/src/emscripten_pre.js ) # Add SDL2 options if using SDL2 backend @@ -288,6 +292,9 @@ if(EMSCRIPTEN) target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS}) + # Output as HTML to use the shell file + set_target_properties(mcrogueface PROPERTIES SUFFIX ".html") + # Set Python home for the embedded interpreter target_compile_definitions(mcrogueface PRIVATE MCRF_WASM_PYTHON_HOME="/lib/python3.14" diff --git a/src/emscripten_pre.js b/src/emscripten_pre.js new file mode 100644 index 0000000..a4b4108 --- /dev/null +++ b/src/emscripten_pre.js @@ -0,0 +1,69 @@ +// Pre-JS file for McRogueFace Emscripten build +// This runs BEFORE Emscripten's code, allowing us to patch browser quirks + +// Fix for browser zoom causing undefined values in resize events +// When browser zoom changes, some event/window properties can become undefined +// which causes Emscripten's HEAP32 writes to fail with assertion errors + +(function() { + 'use strict'; + + // Store original addEventListener + var originalAddEventListener = EventTarget.prototype.addEventListener; + + // Properties that Emscripten's uiEventHandlerFunc reads + // These need to be integers, not undefined + var windowIntegerProps = [ + 'innerWidth', 'innerHeight', + 'outerWidth', 'outerHeight', + 'pageXOffset', 'pageYOffset' + ]; + + // Ensure window properties return integers even during zoom transitions + windowIntegerProps.forEach(function(prop) { + var descriptor = Object.getOwnPropertyDescriptor(window, prop); + if (descriptor && descriptor.get) { + var originalGetter = descriptor.get; + Object.defineProperty(window, prop, { + get: function() { + var val = originalGetter.call(this); + // Return 0 if undefined/null, otherwise floor to integer + return (val === undefined || val === null) ? 0 : Math.floor(val); + }, + configurable: true + }); + } + }); + + // Wrap addEventListener to intercept resize/scroll events + EventTarget.prototype.addEventListener = function(type, listener, options) { + if (type === 'resize' || type === 'scroll') { + var wrappedListener = function(e) { + // Ensure e.detail is an integer + if (e.detail === undefined || e.detail === null) { + // Create a new event with detail = 0 + try { + Object.defineProperty(e, 'detail', { + value: 0, + writable: false + }); + } catch (ex) { + // If we can't modify, create a proxy event + e = new Proxy(e, { + get: function(target, prop) { + if (prop === 'detail') return 0; + var val = target[prop]; + return typeof val === 'function' ? val.bind(target) : val; + } + }); + } + } + return listener.call(this, e); + }; + return originalAddEventListener.call(this, type, wrappedListener, options); + } + return originalAddEventListener.call(this, type, listener, options); + }; + + console.log('McRogueFace: Emscripten pre-JS patches applied'); +})(); diff --git a/src/platform/SDL2Renderer.cpp b/src/platform/SDL2Renderer.cpp index 24c1c68..fa7bc01 100644 --- a/src/platform/SDL2Renderer.cpp +++ b/src/platform/SDL2Renderer.cpp @@ -96,7 +96,7 @@ void main() { } )"; -// Text shader is same as sprite for now +// Text shader - uses alpha from texture, color from vertex static const char* TEXT_VERTEX_SHADER = SPRITE_VERTEX_SHADER; static const char* TEXT_FRAGMENT_SHADER = R"( #ifdef GL_ES @@ -107,9 +107,11 @@ varying vec2 v_texcoord; uniform sampler2D u_texture; void main() { - // Text rendering: use texture alpha as coverage - float alpha = texture2D(u_texture, v_texcoord).a; - gl_FragColor = vec4(v_color.rgb, v_color.a * alpha); + // Font atlas stores glyph alpha in texture alpha channel + // RGB is white (255,255,255), alpha varies per glyph pixel + vec4 texSample = texture2D(u_texture, v_texcoord); + // Use vertex color for RGB, texture alpha for transparency + gl_FragColor = vec4(v_color.rgb, v_color.a * texSample.a); } )"; @@ -1600,8 +1602,6 @@ void Sprite::draw(RenderTarget& target, RenderStates states) const { // Static cache for font atlases - keyed by (font data pointer, character size) static std::map, FontAtlas> s_fontAtlasCache; -static int textDebugCount = 0; - void Text::draw(RenderTarget& target, RenderStates states) const { if (!font_ || string_.empty() || !font_->isLoaded()) return; @@ -1611,96 +1611,102 @@ void Text::draw(RenderTarget& target, RenderStates states) const { if (it == s_fontAtlasCache.end()) { FontAtlas atlas; if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast(characterSize_))) { - std::cerr << "Text::draw: Failed to create font atlas!" << std::endl; 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; } 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(); - // Build vertex data for all glyphs - std::vector vertices; - std::vector texcoords; - std::vector colors; + // Helper lambda to build glyph geometry with a given color and offset + auto buildGlyphs = [&](const Color& color, float offsetX, float offsetY, + std::vector& verts, std::vector& uvs, std::vector& cols) { + float x = 0; + float y = atlas.getAscent(); - float x = 0; - float y = atlas.getAscent(); // Start at baseline + float r = color.r / 255.0f; + float g = color.g / 255.0f; + float b = color.b / 255.0f; + float a = color.a / 255.0f; - float r = fillColor_.r / 255.0f; - float g = fillColor_.g / 255.0f; - float b = fillColor_.b / 255.0f; - float a = fillColor_.a / 255.0f; + for (size_t i = 0; i < string_.size(); ++i) { + char c = string_[i]; - for (size_t i = 0; i < string_.size(); ++i) { - char c = string_[i]; - - // Handle newlines - if (c == '\n') { - x = 0; - y += atlas.getLineHeight(); - continue; - } - - FontAtlas::GlyphInfo glyph; - if (!atlas.getGlyph(static_cast(c), glyph)) { - // Try space for unknown glyphs - if (!atlas.getGlyph(' ', glyph)) { + if (c == '\n') { + x = 0; + y += atlas.getLineHeight(); continue; } + + FontAtlas::GlyphInfo glyph; + if (!atlas.getGlyph(static_cast(c), glyph)) { + if (!atlas.getGlyph(' ', glyph)) { + continue; + } + } + + // Calculate quad corners with offset + float x0 = x + glyph.xoff + offsetX; + float y0 = y + glyph.yoff + offsetY; + float x1 = x0 + glyph.width; + float y1 = y0 + glyph.height; + + // Transform to world space + Vector2f p0 = combined.transformPoint(x0, y0); + Vector2f p1 = combined.transformPoint(x1, y0); + Vector2f p2 = combined.transformPoint(x1, y1); + Vector2f p3 = combined.transformPoint(x0, y1); + + verts.insert(verts.end(), { + p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, + p0.x, p0.y, p2.x, p2.y, p3.x, p3.y + }); + + uvs.insert(uvs.end(), { + glyph.u0, glyph.v0, glyph.u1, glyph.v0, glyph.u1, glyph.v1, + glyph.u0, glyph.v0, glyph.u1, glyph.v1, glyph.u0, glyph.v1 + }); + + for (int v = 0; v < 6; ++v) { + cols.insert(cols.end(), {r, g, b, a}); + } + + x += glyph.xadvance; + } + }; + + // Draw outline first (if any) + if (outlineThickness_ > 0 && outlineColor_.a > 0) { + std::vector outlineVerts, outlineUVs, outlineCols; + + // Draw at 8 positions around each glyph for outline effect + float t = outlineThickness_; + 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); } - // Calculate quad corners in local space - float x0 = x + glyph.xoff; - float y0 = y + glyph.yoff; - float x1 = x0 + glyph.width; - float y1 = y0 + glyph.height; - - // Transform to world space - Vector2f p0 = combined.transformPoint(x0, y0); - Vector2f p1 = combined.transformPoint(x1, y0); - Vector2f p2 = combined.transformPoint(x1, y1); - Vector2f p3 = combined.transformPoint(x0, y1); - - // Two triangles for quad - vertices.insert(vertices.end(), { - 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 // Triangle 2 - }); - - texcoords.insert(texcoords.end(), { - glyph.u0, glyph.v0, glyph.u1, glyph.v0, glyph.u1, glyph.v1, // Triangle 1 - glyph.u0, glyph.v0, glyph.u1, glyph.v1, glyph.u0, glyph.v1 // Triangle 2 - }); - - // 6 vertices * 4 color components - for (int v = 0; v < 6; ++v) { - colors.insert(colors.end(), {r, g, b, a}); + if (!outlineVerts.empty()) { + SDL2Renderer::getInstance().drawTriangles( + outlineVerts.data(), outlineVerts.size() / 2, + outlineCols.data(), outlineUVs.data(), + atlas.getTextureId(), + SDL2Renderer::ShaderType::Text + ); } - - x += glyph.xadvance; } - if (!vertices.empty()) { - // Debug: log first glyph's UVs - static int uvDebugCount = 0; - if (uvDebugCount < 2 && texcoords.size() >= 12) { - std::cout << "Text UVs for first glyph: u0=" << texcoords[0] << " v0=" << texcoords[1] - << " u1=" << texcoords[4] << " v1=" << texcoords[5] - << " (vertexCount=" << (vertices.size()/2) << ")" << std::endl; - uvDebugCount++; - } + // Draw fill text on top + std::vector vertices, texcoords, colors; + buildGlyphs(fillColor_, 0, 0, vertices, texcoords, colors); - // Use text shader (uses alpha from texture, not full RGBA multiply) + if (!vertices.empty()) { SDL2Renderer::getInstance().drawTriangles( vertices.data(), vertices.size() / 2, colors.data(), texcoords.data(), @@ -1862,6 +1868,46 @@ bool Shader::isAvailable() { FontAtlas::FontAtlas() = default; +FontAtlas::FontAtlas(FontAtlas&& other) noexcept + : textureId_(other.textureId_) + , fontSize_(other.fontSize_) + , ascent_(other.ascent_) + , descent_(other.descent_) + , lineHeight_(other.lineHeight_) + , glyphCache_(std::move(other.glyphCache_)) + , stbFontInfo_(other.stbFontInfo_) +{ + // Clear source to prevent double-deletion + other.textureId_ = 0; + other.stbFontInfo_ = nullptr; +} + +FontAtlas& FontAtlas::operator=(FontAtlas&& other) noexcept { + if (this != &other) { + // Clean up existing resources + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + if (stbFontInfo_) { + delete static_cast(stbFontInfo_); + } + + // Transfer ownership + textureId_ = other.textureId_; + fontSize_ = other.fontSize_; + ascent_ = other.ascent_; + descent_ = other.descent_; + lineHeight_ = other.lineHeight_; + glyphCache_ = std::move(other.glyphCache_); + stbFontInfo_ = other.stbFontInfo_; + + // Clear source to prevent double-deletion + other.textureId_ = 0; + other.stbFontInfo_ = nullptr; + } + return *this; +} + FontAtlas::~FontAtlas() { if (textureId_) { SDL2Renderer::getInstance().deleteTexture(textureId_); diff --git a/src/platform/SDL2Renderer.h b/src/platform/SDL2Renderer.h index 8246175..3a34238 100644 --- a/src/platform/SDL2Renderer.h +++ b/src/platform/SDL2Renderer.h @@ -154,6 +154,14 @@ public: FontAtlas(); ~FontAtlas(); + // Move semantics - transfer ownership of GPU resources + FontAtlas(FontAtlas&& other) noexcept; + FontAtlas& operator=(FontAtlas&& other) noexcept; + + // Disable copy - texture resources can't be shared + FontAtlas(const FontAtlas&) = delete; + FontAtlas& operator=(const FontAtlas&) = delete; + // Load font data bool load(const unsigned char* fontData, size_t dataSize, float fontSize); diff --git a/src/shell.html b/src/shell.html new file mode 100644 index 0000000..a1ab612 --- /dev/null +++ b/src/shell.html @@ -0,0 +1,162 @@ + + + + + + McRogueFace - WebGL + + + +

McRogueFace

+
Downloading...
+
+ +
+ + + {{{ SCRIPT }}} + +