diff --git a/fx_graph.cm b/fx_graph.cm index e5d899e9..40b36709 100644 --- a/fx_graph.cm +++ b/fx_graph.cm @@ -588,9 +588,12 @@ function collect_drawables(node, camera, parent_tint, parent_opacity, parent_sci text: node.text, font: node.font, size: node.size, - sdf: node.sdf, + mode: node.mode, // 'bitmap', 'sdf', or 'msdf' + sdf: node.sdf, // legacy support outline_width: node.outline_width, outline_color: node.outline_color, + anchor_x: node.anchor_x, + anchor_y: node.anchor_y, color: tint_to_color(world_tint, world_opacity), scissor: current_scissor }) diff --git a/msdf.h b/msdf.h index fdb36562..ceb2efd1 100644 --- a/msdf.h +++ b/msdf.h @@ -16,6 +16,8 @@ #include #include +#include "stb_truetype.h" + #ifdef __cplusplus extern "C" { #endif @@ -828,9 +830,11 @@ int msdf_genGlyph(msdf_Result* result, stbtt_fontinfo *font, int stbttGlyphIndex //float scale = stbtt_ScaleForMappingEmToPixels(font, h); int glyphIdx = stbttGlyphIndex; // get glyph bounding box (scaled later) - int ix0, iy0, ix1, iy1; + int ix0 = 0, iy0 = 0, ix1 = 0, iy1 = 0; float xoff = .0, yoff = .0; - stbtt_GetGlyphBox(font, glyphIdx, &ix0, &iy0, &ix1, &iy1); + if (!stbtt_GetGlyphBox(font, glyphIdx, &ix0, &iy0, &ix1, &iy1)) { + return 0; + } float glyphWidth = ix1 - ix0; float glyphHeight = iy1 - iy0; @@ -839,12 +843,19 @@ int msdf_genGlyph(msdf_Result* result, stbtt_fontinfo *font, int stbttGlyphIndex float hF32 = ceilf(glyphHeight * scale); wF32 += 2.f * borderWidth; hF32 += 2.f * borderWidth; + int w = wF32; int h = hF32; float* bitmap = (float*) allocCtx.alloc(w * h * 3 * sizeof(float), allocCtx.ctx); + if (!bitmap) return 0; memset(bitmap, 0x0, w * h * 3 * sizeof(float)); + result->rgb = bitmap; + result->width = w; + result->height = h; + result->glyphIdx = glyphIdx; + // em scale //scale = stbtt_ScaleForMappingEmToPixels(font, h); diff --git a/sdl_gpu.cm b/sdl_gpu.cm index de606ded..4859e76e 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -36,6 +36,7 @@ var _mask_frag = null var _mask_frag = null var _crt_frag = null var _text_sdf_frag = null +var _text_msdf_frag = null // Pipelines var _pipelines = {} @@ -218,6 +219,18 @@ function _load_shaders() { }) } + var text_msdf_frag_code = io.slurp("shaders/msl/text_msdf.frag.msl") + if (text_msdf_frag_code) { + _text_msdf_frag = new gpu_mod.shader(_gpu, { + code: text_msdf_frag_code, + stage: "fragment", + format: "msl", + entrypoint: "fragment_main", + num_uniform_buffers: 1, + num_samplers: 1 + }) + } + var crt_frag_code = io.slurp("shaders/msl/crt.frag.msl") if (crt_frag_code) { _crt_frag = new gpu_mod.shader(_gpu, { @@ -482,6 +495,78 @@ function _create_pipelines() { } }) } + + // SDF text pipeline + if (_sprite_vert && _text_sdf_frag) { + _pipelines.text_sdf = new gpu_mod.graphics_pipeline(_gpu, { + vertex: _sprite_vert, + fragment: _text_sdf_frag, + primitive: "triangle", + cull: "none", + face: "counter_clockwise", + fill: "fill", + vertex_buffer_descriptions: [{ + slot: 0, + pitch: 32, + input_rate: "vertex" + }], + vertex_attributes: [ + {location: 0, buffer_slot: 0, format: "float2", offset: 0}, + {location: 1, buffer_slot: 0, format: "float2", offset: 8}, + {location: 2, buffer_slot: 0, format: "float4", offset: 16} + ], + target: { + color_targets: [{ + format: _swapchain_format, + blend: { + enabled: true, + src_rgb: "src_alpha", + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "one_minus_src_alpha", + op_alpha: "add" + } + }] + } + }) + } + + // MSDF text pipeline + if (_sprite_vert && _text_msdf_frag) { + _pipelines.text_msdf = new gpu_mod.graphics_pipeline(_gpu, { + vertex: _sprite_vert, + fragment: _text_msdf_frag, + primitive: "triangle", + cull: "none", + face: "counter_clockwise", + fill: "fill", + vertex_buffer_descriptions: [{ + slot: 0, + pitch: 32, + input_rate: "vertex" + }], + vertex_attributes: [ + {location: 0, buffer_slot: 0, format: "float2", offset: 0}, + {location: 1, buffer_slot: 0, format: "float2", offset: 8}, + {location: 2, buffer_slot: 0, format: "float4", offset: 16} + ], + target: { + color_targets: [{ + format: _swapchain_format, + blend: { + enabled: true, + src_rgb: "src_alpha", + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "one_minus_src_alpha", + op_alpha: "add" + } + }] + } + }) + } } // ======================================================================== @@ -1140,21 +1225,15 @@ function _render_batch(cmd_buffer, pass, batch, camera, target) { } function _render_text(cmd_buffer, pass, drawable, camera, target) { - // Get font + // Get font - support mode tag: 'bitmap', 'sdf', 'msdf' var font_path = drawable.font var size = drawable.size || 16 - var is_sdf = drawable.sdf || false - var font = _get_font_cache(font_path, size, is_sdf) + var mode = drawable.mode || (drawable.sdf ? 'sdf' : 'bitmap') + var font = _get_font_cache(font_path, size, mode) if (!font) return // Generate vertices using staef var pos = drawable.pos - // Convert world/camera pos to screen/local - // Note: staef generates raw vertex positions. We usually want to transform them by camera matrix. - // The sprite pipeline applies the camera matrix (proj) to the positions. - // So we should feed "local" positions (or world positions) into the buffer, and let vertex shader transform them. - // staef's make_text_buffer generates vertices relative to the input position. - var text_pos = {x: pos.x, y: pos.y, width: 0, height: 0} var color = drawable.color || {r:1, g:1, b:1, a:1} @@ -1164,10 +1243,9 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { if (ax != 0 || ay != 0) { var dim = font.text_size(drawable.text) - // dim is {x, y} (width, height) if (dim) { text_pos.x -= dim.x * ax - text_pos.y -= dim.y * ay // staef usually draws from top-left, need to verify + text_pos.y -= dim.y * ay } } @@ -1178,7 +1256,7 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { var num_verts = mesh.num_vertices var interleaved = geometry.weave([{data:mesh.xy, stride: mesh.xy_stride}, {data:mesh.uv, stride: mesh.uv_stride}, {data:mesh.color, stride: mesh.color_stride}]) - var indices = mesh.indices // This is a blob of uint16 + var indices = mesh.indices var num_indices = mesh.num_indices // Upload @@ -1204,37 +1282,58 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { // Setup pipeline var proj = _build_camera_matrix(camera, target.width, target.height) - if (is_sdf && _pipelines.text_sdf) { + // Select pipeline based on mode + var is_sdf = (mode == 'sdf') + var is_msdf = (mode == 'msdf') + + if (is_msdf && _pipelines.text_msdf) { + pass.bind_pipeline(_pipelines.text_msdf) + + // Build uniforms for MSDF + // Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color + var u_data = new blob_mod(32) + + // Convert outline_width from pixel-ish units to normalized SDF units + // outline_width in drawable is in "visual" units, we need to normalize + // A typical range is 0.0-0.3 in SDF units + var outline_w = drawable.outline_width || 0 + if (outline_w > 0) outline_w = outline_w / 100.0 // Scale down from user units + + u_data.wf(outline_w) // outline_width + u_data.wf(font.sharpness || 1.0) // sharpness from font + u_data.wf(0) // _pad.x + u_data.wf(0) // _pad.y + + var oc = drawable.outline_color || {r:0, g:0, b:0, a:1} + u_data.wf(oc.r) // outline_color.r + u_data.wf(oc.g) // outline_color.g + u_data.wf(oc.b) // outline_color.b + u_data.wf(oc.a || 1) // outline_color.a + + cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) + + } else if (is_sdf && _pipelines.text_sdf) { pass.bind_pipeline(_pipelines.text_sdf) - // Upload uniforms for SDF (outline) + // Build uniforms for SDF + // Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color var u_data = new blob_mod(32) - u_data.wf(drawable.outline_width || 0) - u_data.wf(0) // padding/unused - u_data.wf(0) // padding - u_data.wf(0) // padding - var oc = drawable.outline_color || {r:0, g:0, b:0, a:0} + var outline_w = drawable.outline_width || 0 + if (outline_w > 0) outline_w = outline_w / 100.0 + + u_data.wf(outline_w) // outline_width + u_data.wf(font.sharpness || 1.0) // sharpness from font + u_data.wf(0) // _pad.x + u_data.wf(0) // _pad.y + + var oc = drawable.outline_color || {r:0, g:0, b:0, a:1} u_data.wf(oc.r) u_data.wf(oc.g) u_data.wf(oc.b) - u_data.wf(oc.a) // Used as alpha?? Shader expects float3 color. Structure has float4 color? - // Shader: float outline_width; float3 outline_color; - // Layout: offset 0 (4 bytes), offset 16 (16 bytes vec4 alignment usually for float3) - // Actually metal float3 is 16 byte aligned/sized often in buffers? - // Let's assume standard packed: float (4), float3 (12 needed, but alignment constraints). - // Uniforms struct: width (4), padding (12) -> size 16. Color (12/16) -> offset 16. + u_data.wf(oc.a || 1) - // Let's rewrite struct in shader or be careful. - // Struct: float outline_width; float3 outline_color; - // If strict metal alignment: - // width at 0. - // float3 at 16 (since it's a type that requires 16 byte alignment? No, float4 does. float3 is usually float4 size/alignment in buffers). - - pass.push_fragment_uniform_data(0, stone(u_data)) // Wait, push_fragment_uniform_data on cmd_buffer? - // The code below uses cmd_buffer.push_vertex_uniform_data(0, proj) - // We need push_fragment_uniform_data. - cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) // Bind to buffer(0) in fragment + cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) } else { pass.bind_pipeline(_pipelines.sprite_alpha) @@ -1243,32 +1342,46 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - // Bind font texture - // staef font has 'texture' property which is pixel blob + dims. We need to upload it to GPU if not already. - var font_tex = _get_font_texture(font, is_sdf) + // Bind font texture - use linear filtering for SDF/MSDF + var font_tex = _get_font_texture(font, mode) + var sampler = (is_sdf || is_msdf) ? _sampler_linear : _sampler_nearest - pass.bind_fragment_samplers(0, [{texture: font_tex, sampler: _sampler_nearest}]) + pass.bind_fragment_samplers(0, [{texture: font_tex, sampler: sampler}]) cmd_buffer.push_vertex_uniform_data(0, proj) pass.draw_indexed(num_indices, 1, 0, 0, 0) } -function _get_font_cache(path, size, is_sdf) { - var key = `${path}.${size}.${is_sdf ? 'sdf' : 'bmp'}` +function _get_font_cache(path, size, mode) { + // mode can be 'bitmap', 'sdf', 'msdf', or boolean (legacy) + if (mode == true) mode = 'sdf' + else if (mode == false || !mode) mode = 'bitmap' + + var key = `${path}.${size}.${mode}` if (_font_cache[key]) return _font_cache[key] - var fullpath = res.find_font(path) // Assuming this resolves correctly + var fullpath = res.find_font(path) if (!fullpath) return null var data = io.slurp(fullpath) if (!data) return null - // Create staef font + // Create staef font based on mode try { - var font = new staef.font(data, size, is_sdf) + var font + if (mode == 'msdf') { + // MSDF: em_px=size, range_px=4, padding_px=6, sharpness=1.0 + font = new staef.msdf_font(data, size, 4.0, 6, 1.0) + } else if (mode == 'sdf') { + // SDF: em_px=size, range_px=12, padding_px=14, sharpness=1.0 + font = new staef.sdf_font(data, size, 12.0, 14, 1.0) + } else { + // Bitmap + font = new staef.font(data, size, false) + } _font_cache[key] = font return font } catch(e) { - log.console(`sdl_gpu: Failed to load font ${path}:${size}: ${e.message}`) + log.console(`sdl_gpu: Failed to load font ${path}:${size}:${mode}: ${e.message}`) return null } } diff --git a/staef.c b/staef.c index a6c4b777..72fb79bf 100644 --- a/staef.c +++ b/staef.c @@ -34,6 +34,12 @@ typedef enum { CHARACTER } TEXT_BREAK; +typedef enum { + FONT_MODE_BITMAP = 0, + FONT_MODE_SDF = 1, + FONT_MODE_MSDF = 2 +} FONT_MODE; + struct text_char { rect pos; rect uv; @@ -55,15 +61,17 @@ struct character { // text data struct sFont { - uint32_t height; /* in pixels */ + uint32_t height; /* em_px: glyph size in atlas pixels */ float ascent; // pixels float descent; // pixels float linegap; //pixels float line_height; // pixels struct character Characters[256]; - unsigned char *pixels; // RGBA8 pixel data + unsigned char *pixels; // RGBA8 pixel data (RGB for MSDF) int atlas_size; // width and height of atlas (square) - int is_sdf; + int mode; // FONT_MODE_BITMAP, FONT_MODE_SDF, or FONT_MODE_MSDF + float range_px; // SDF/MSDF distance range in atlas pixels + float sharpness; // render-time sharpness multiplier (default 1.0) }; typedef struct sFont font; @@ -81,15 +89,269 @@ void font_free(JSRuntime *rt, font *f) free(f); } -struct sFont *MakeFont(void *ttf_buffer, size_t len, int height, int is_sdf) { +// MakeFontSDF: Create SDF font with explicit parameters +// em_px: glyph size in atlas (like 64, 96, 128) +// range_px: distance field range in atlas pixels (typical: 6-16 for SDF, 2-8 for MSDF) +// padding_px: padding around glyphs (should be >= range_px + 2) +struct sFont *MakeFontSDF(void *ttf_buffer, size_t len, int em_px, float range_px, int padding_px) { if (!ttf_buffer) return NULL; int packsize = 1024; + struct sFont *newfont = calloc(1, sizeof(struct sFont)); + newfont->height = em_px; + newfont->mode = FONT_MODE_SDF; + newfont->range_px = range_px; + newfont->sharpness = 1.0f; + newfont->atlas_size = packsize; + + unsigned char *bitmap = calloc(1, packsize * packsize); + + stbtt_fontinfo fontinfo; + if (!stbtt_InitFont(&fontinfo, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer, 0))) { + free(newfont); + free(bitmap); + return NULL; + } + + int ascent, descent, linegap; + stbtt_GetFontVMetrics(&fontinfo, &ascent, &descent, &linegap); + float scale = stbtt_ScaleForPixelHeight(&fontinfo, em_px); + newfont->ascent = ascent * scale; + newfont->descent = descent * scale; + newfont->linegap = linegap * scale; + newfont->line_height = (newfont->ascent - newfont->descent) + newfont->linegap; + + // Manual SDF packing + int x = 0; + int y = 0; + int row_height = 0; + int pad = padding_px; + float onedge_value = 128.0f; + // pixel_dist_scale controls how distance maps to pixel values + // Higher = sharper edges but less range for effects + // Formula: encoded = 0.5 + dist / (2 * range_px) + // So pixel_dist_scale = 128 / range_px gives us proper encoding + float pixel_dist_scale = 128.0f / range_px; + + for (unsigned char c = 32; c < 127; c++) { + int g = stbtt_FindGlyphIndex(&fontinfo, c); + + int width, height, xoff, yoff; + unsigned char *sdf = stbtt_GetGlyphSDF(&fontinfo, scale, g, pad, (unsigned char)onedge_value, pixel_dist_scale, &width, &height, &xoff, &yoff); + + if (!sdf) { + int advance, lsb; + stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); + newfont->Characters[c].advance = advance * scale; + continue; + } + + if (x + width + 1 > packsize) { + x = 0; + y += row_height + 1; + row_height = 0; + } + + if (y + height + 1 > packsize) { + free(sdf); + continue; + } + + for (int sy = 0; sy < height; sy++) { + for (int sx = 0; sx < width; sx++) { + bitmap[(y + sy) * packsize + (x + sx)] = sdf[sy * width + sx]; + } + } + + free(sdf); + + rect uv; + uv.x = (float)x / packsize; + uv.y = (float)(y + height) / packsize; + uv.w = (float)width / packsize; + uv.h = -(float)height / packsize; + newfont->Characters[c].uv = uv; + + rect quad; + quad.x = (float)xoff; + quad.y = (float)(-yoff - height); + quad.w = (float)width; + quad.h = (float)height; + newfont->Characters[c].quad = quad; + + int advance, lsb; + stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); + newfont->Characters[c].advance = advance * scale; + + x += width + 1; + if (height > row_height) row_height = height; + } + + // Convert to RGBA8 + newfont->pixels = malloc(packsize * packsize * 4); + for (int i = 0; i < packsize; i++) { + for (int j = 0; j < packsize; j++) { + int idx = (i * packsize + j) * 4; + newfont->pixels[idx + 0] = 255; + newfont->pixels[idx + 1] = 255; + newfont->pixels[idx + 2] = 255; + newfont->pixels[idx + 3] = bitmap[i * packsize + j]; + } + } + + free(bitmap); + return newfont; +} + +// MakeFontMSDF: Create MSDF font with explicit parameters +struct sFont *MakeFontMSDF(void *ttf_buffer, size_t len, int em_px, float range_px, int padding_px) { + if (!ttf_buffer) + return NULL; + + int packsize = 1024; + + struct sFont *newfont = calloc(1, sizeof(struct sFont)); + newfont->height = em_px; + newfont->mode = FONT_MODE_MSDF; + newfont->range_px = range_px; + newfont->sharpness = 1.0f; + newfont->atlas_size = packsize; + + // MSDF uses RGB channels, so we need float RGB buffer then convert + unsigned char *bitmap_rgb = calloc(1, packsize * packsize * 3); + + stbtt_fontinfo fontinfo; + if (!stbtt_InitFont(&fontinfo, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer, 0))) { + free(newfont); + free(bitmap_rgb); + return NULL; + } + + int ascent, descent, linegap; + stbtt_GetFontVMetrics(&fontinfo, &ascent, &descent, &linegap); + float scale = stbtt_ScaleForPixelHeight(&fontinfo, em_px); + newfont->ascent = ascent * scale; + newfont->descent = descent * scale; + newfont->linegap = linegap * scale; + newfont->line_height = (newfont->ascent - newfont->descent) + newfont->linegap; + + int x = 0; + int y = 0; + int row_height = 0; + int border = padding_px; + + for (unsigned char c = 32; c < 127; c++) { + int g = stbtt_FindGlyphIndex(&fontinfo, c); + + msdf_Result result = {0}; + int ok = msdf_genGlyph(&result, &fontinfo, g, border, scale, range_px, NULL); + + if (!ok || !result.rgb) { + int advance, lsb; + stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); + newfont->Characters[c].advance = advance * scale; + continue; + } + + int width = result.width; + int height = result.height; + + if (x + width + 1 > packsize) { + x = 0; + y += row_height + 1; + row_height = 0; + } + + if (y + height + 1 > packsize) { + free(result.rgb); + continue; + } + + // Copy MSDF RGB data to atlas (convert float to uint8) + for (int sy = 0; sy < height; sy++) { + for (int sx = 0; sx < width; sx++) { + int src_idx = 3 * (sy * width + sx); + int dst_idx = 3 * ((y + sy) * packsize + (x + sx)); + // Clamp float [0,1] to [0,255] + float r = result.rgb[src_idx + 0]; + float g = result.rgb[src_idx + 1]; + float b = result.rgb[src_idx + 2]; + bitmap_rgb[dst_idx + 0] = (unsigned char)(fminf(fmaxf(r * 255.0f, 0.0f), 255.0f)); + bitmap_rgb[dst_idx + 1] = (unsigned char)(fminf(fmaxf(g * 255.0f, 0.0f), 255.0f)); + bitmap_rgb[dst_idx + 2] = (unsigned char)(fminf(fmaxf(b * 255.0f, 0.0f), 255.0f)); + } + } + + free(result.rgb); + + // Get glyph box for positioning + int ix0, iy0, ix1, iy1; + stbtt_GetGlyphBox(&fontinfo, g, &ix0, &iy0, &ix1, &iy1); + + rect uv; + uv.x = (float)x / packsize; + uv.y = (float)(y + height) / packsize; + uv.w = (float)width / packsize; + uv.h = -(float)height / packsize; + newfont->Characters[c].uv = uv; + + // Calculate quad position + // MSDF result includes border, so we need to account for it + float xoff = (ix0 * scale) - border; + float yoff = (iy0 * scale) - border; + + rect quad; + quad.x = xoff; + quad.y = yoff; + quad.w = (float)width; + quad.h = (float)height; + newfont->Characters[c].quad = quad; + + int advance, lsb; + stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); + newfont->Characters[c].advance = advance * scale; + + x += width + 1; + if (height > row_height) row_height = height; + } + + // Convert RGB to RGBA8 (alpha = 255 for MSDF, color channels hold distance) + newfont->pixels = malloc(packsize * packsize * 4); + for (int i = 0; i < packsize; i++) { + for (int j = 0; j < packsize; j++) { + int src_idx = 3 * (i * packsize + j); + int dst_idx = (i * packsize + j) * 4; + newfont->pixels[dst_idx + 0] = bitmap_rgb[src_idx + 0]; + newfont->pixels[dst_idx + 1] = bitmap_rgb[src_idx + 1]; + newfont->pixels[dst_idx + 2] = bitmap_rgb[src_idx + 2]; + newfont->pixels[dst_idx + 3] = 255; + } + } + + free(bitmap_rgb); + return newfont; +} + +// Legacy MakeFont for backward compatibility +struct sFont *MakeFont(void *ttf_buffer, size_t len, int height, int is_sdf) { + if (!ttf_buffer) + return NULL; + + // For SDF mode, use sensible defaults + if (is_sdf) { + // Default: em_px=height, range_px=12, padding_px=14 + return MakeFontSDF(ttf_buffer, len, height, 12.0f, 14); + } + + int packsize = 1024; + struct sFont *newfont = calloc(1, sizeof(struct sFont)); newfont->height = height; - newfont->is_sdf = is_sdf; + newfont->mode = FONT_MODE_BITMAP; + newfont->range_px = 0; + newfont->sharpness = 1.0f; newfont->atlas_size = packsize; unsigned char *bitmap = calloc(1, packsize * packsize); @@ -109,99 +371,8 @@ struct sFont *MakeFont(void *ttf_buffer, size_t len, int height, int is_sdf) { newfont->linegap = linegap * scale; newfont->line_height = (newfont->ascent - newfont->descent) + newfont->linegap; - if (is_sdf) { - // Manual SDF packing - int x = 0; - int y = 0; - int row_height = 0; - int pad = 5; // padding for SDF - float onedge_value = 127.5f; // 128ish - float pixel_dist_scale = 150.f; // Distance field range - - for (unsigned char c = 32; c < 127; c++) { - int glyph_index = c - 29; // Simple ASCII mapping? verify if font has proper map - // Actually standard packing uses PackFontRange which maps ASCII 32..126 - // We should use stbtt_FindGlyphIndex(&fontinfo, c); - int g = stbtt_FindGlyphIndex(&fontinfo, c); - - int width, height, xoff, yoff; - unsigned char *sdf = stbtt_GetGlyphSDF(&fontinfo, scale, g, pad, (unsigned char)onedge_value, pixel_dist_scale, &width, &height, &xoff, &yoff); - - if (!sdf) { - // Handle invisible characters (space) - int advance, lsb; - stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); - newfont->Characters[c].advance = advance * scale; - // Keep quad/uv as 0 - continue; - } - - if (x + width + 1 > packsize) { - x = 0; - y += row_height + 1; - row_height = 0; - } - - if (y + height + 1 > packsize) { - // Out of space - free(sdf); - continue; - } - - // Blit SDF to atlas - for (int sy = 0; sy < height; sy++) { - for (int sx = 0; sx < width; sx++) { - bitmap[(y + sy) * packsize + (x + sx)] = sdf[sy * width + sx]; - } - } - - free(sdf); - - // Store character info - rect uv; - uv.x = (float)x / packsize; - uv.y = (float)(y + height) / packsize; // Bottom (Top of glyph in SDF?) - // To match bitmap path which produces UPSIDE DOWN results if not flipped: - // Bitmap path: uv.h = Negative. - // So UVs are (y1, y0). (Top Texture is at Bottom vertex). - // Here we want to match that. - // Top Vertex (v0) should get Bottom Texture. - // Bottom Vertex (v1) should get Top Texture. - - // Let's make uv.h negative. - // Top Texture is at 'y'. Bottom Texture is at 'y+height'. - // uv.y = (y+height)/size. - // uv.h = -height/size. - // v0 (Top Vert) = uv.y = y+height (Bottom Tex). - // v1 (Bottom Vert) = uv.y + uv.h = y (Top Tex). - // This flips the texture on Y. - - uv.w = (float)width / packsize; - uv.h = -(float)height / packsize; - newfont->Characters[c].uv = uv; - - rect quad; - quad.x = (float)xoff; - // Bitmap path: quad.y = -yoff2. (Bottom in Y-Up). - // SDF path: yoff is Top in Y-Down (-Top in Y-Up). - // height is height. - // We want Bottom in Y-Up. - // Bottom = - (yoff + height). - - quad.y = (float)(-yoff - height); - quad.w = (float)width; - quad.h = (float)height; - newfont->Characters[c].quad = quad; - - int advance, lsb; - stbtt_GetGlyphHMetrics(&fontinfo, g, &advance, &lsb); - newfont->Characters[c].advance = advance * scale; - - x += width + 1; - if (height > row_height) row_height = height; - } - - } else { + // Bitmap-only path + { // Original Bitmap packing stbtt_packedchar glyphs[95]; stbtt_pack_context pc; @@ -259,6 +430,20 @@ struct sFont *MakeFont(void *ttf_buffer, size_t len, int height, int is_sdf) { return newfont; } +// Create SDF font with custom parameters (exposed to JS) +struct sFont *MakeFontSDFParams(void *ttf_buffer, size_t len, int em_px, float range_px, int padding_px, float sharpness) { + struct sFont *f = MakeFontSDF(ttf_buffer, len, em_px, range_px, padding_px); + if (f) f->sharpness = sharpness; + return f; +} + +// Create MSDF font with custom parameters (exposed to JS) +struct sFont *MakeFontMSDFParams(void *ttf_buffer, size_t len, int em_px, float range_px, int padding_px, float sharpness) { + struct sFont *f = MakeFontMSDF(ttf_buffer, len, em_px, range_px, padding_px); + if (f) f->sharpness = sharpness; + return f; +} + layout_lines layout_text_lines(const char *text, font *f, float letter_spacing, float wrap, @@ -492,23 +677,8 @@ struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf col // QuickJS class for font QJSCLASS(font,) -// Font constructor -JSC_CCALL(staef_font_new, - size_t len; - void *data = js_get_blob_data(js, &len, argv[0]); - if (data == -1) return JS_EXCEPTION; - if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data"); - - double height = js2number(js, argv[1]); - int is_sdf = 0; - if (argc > 2) is_sdf = JS_ToBool(js, argv[2]); - - font *f = MakeFont(data, len, (int)height, is_sdf); - if (!f) return JS_ThrowReferenceError(js, "could not create font"); - - ret = font2js(js, f); - - // Create texture data object for the font's atlas +// Helper to attach texture and mode info to font JS object +static void attach_font_texture(JSContext *js, JSValue ret, font *f) { if (f->pixels) { JSValue texData = JS_NewObject(js); JS_SetPropertyStr(js, texData, "width", JS_NewInt32(js, f->atlas_size)); @@ -520,6 +690,72 @@ JSC_CCALL(staef_font_new, JS_SetPropertyStr(js, ret, "texture", texData); } + + // Add mode string + const char *mode_str = "bitmap"; + if (f->mode == FONT_MODE_SDF) mode_str = "sdf"; + else if (f->mode == FONT_MODE_MSDF) mode_str = "msdf"; + JS_SetPropertyStr(js, ret, "mode", JS_NewString(js, mode_str)); + + // Add range_px and sharpness for SDF/MSDF fonts + JS_SetPropertyStr(js, ret, "range_px", JS_NewFloat64(js, f->range_px)); + JS_SetPropertyStr(js, ret, "sharpness", JS_NewFloat64(js, f->sharpness)); +} + +// Font constructor (legacy: data, height, is_sdf) +JSC_CCALL(staef_font_new, + size_t len; + void *data = js_get_blob_data(js, &len, argv[0]); + if (data == (void*)-1) return JS_EXCEPTION; + if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data"); + + double height = js2number(js, argv[1]); + int is_sdf = 0; + if (argc > 2) is_sdf = JS_ToBool(js, argv[2]); + + font *f = MakeFont(data, len, (int)height, is_sdf); + if (!f) return JS_ThrowReferenceError(js, "could not create font"); + + ret = font2js(js, f); + attach_font_texture(js, ret, f); +) + +// SDF font constructor: sdf_font(data, em_px, range_px, padding_px, sharpness) +JSC_CCALL(staef_sdf_font_new, + size_t len; + void *data = js_get_blob_data(js, &len, argv[0]); + if (data == (void*)-1) return JS_EXCEPTION; + if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data"); + + int em_px = argc > 1 ? (int)js2number(js, argv[1]) : 64; + float range_px = argc > 2 ? (float)js2number(js, argv[2]) : 12.0f; + int padding_px = argc > 3 ? (int)js2number(js, argv[3]) : 14; + float sharpness = argc > 4 ? (float)js2number(js, argv[4]) : 1.0f; + + font *f = MakeFontSDFParams(data, len, em_px, range_px, padding_px, sharpness); + if (!f) return JS_ThrowReferenceError(js, "could not create SDF font"); + + ret = font2js(js, f); + attach_font_texture(js, ret, f); +) + +// MSDF font constructor: msdf_font(data, em_px, range_px, padding_px, sharpness) +JSC_CCALL(staef_msdf_font_new, + size_t len; + void *data = js_get_blob_data(js, &len, argv[0]); + if (data == (void*)-1) return JS_EXCEPTION; + if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data"); + + int em_px = argc > 1 ? (int)js2number(js, argv[1]) : 64; + float range_px = argc > 2 ? (float)js2number(js, argv[2]) : 4.0f; + int padding_px = argc > 3 ? (int)js2number(js, argv[3]) : 6; + float sharpness = argc > 4 ? (float)js2number(js, argv[4]) : 1.0f; + + font *f = MakeFontMSDFParams(data, len, em_px, range_px, padding_px, sharpness); + if (!f) return JS_ThrowReferenceError(js, "could not create MSDF font"); + + ret = font2js(js, f); + attach_font_texture(js, ret, f); ) // Calculate text size @@ -565,6 +801,9 @@ JSC_GETSET(font, ascent, number) JSC_GETSET(font, descent, number) JSC_GETSET(font, line_height, number) JSC_GETSET(font, height, number) +JSC_GETSET(font, range_px, number) +JSC_GETSET(font, sharpness, number) +JSC_GETSET(font, mode, number) // Font methods static const JSCFunctionListEntry js_font_funcs[] = { @@ -575,6 +814,9 @@ static const JSCFunctionListEntry js_font_funcs[] = { CGETSET_ADD(font, descent), CGETSET_ADD(font, line_height), CGETSET_ADD(font, height), + CGETSET_ADD(font, range_px), + CGETSET_ADD(font, sharpness), + CGETSET_ADD(font, mode), }; // Font constructor function @@ -583,6 +825,18 @@ static JSValue js_font_constructor(JSContext *ctx, JSValueConst new_target, int return js_staef_font_new(ctx, JS_NULL, argc, argv); } +// SDF font constructor function +static JSValue js_sdf_font_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) +{ + return js_staef_sdf_font_new(ctx, JS_NULL, argc, argv); +} + +// MSDF font constructor function +static JSValue js_msdf_font_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) +{ + return js_staef_msdf_font_new(ctx, JS_NULL, argc, argv); +} + // Initialize the staef module CELL_USE_INIT( JSValue mod = JS_NewObject(js); @@ -595,12 +849,20 @@ CELL_USE_INIT( JS_SetPropertyFunctionList(js, proto, js_font_funcs, countof(js_font_funcs)); JS_SetClassProto(js, js_font_id, proto); - // Create font constructor + // Create font constructor (legacy) JSValue font_ctor = JS_NewCFunction2(js, js_font_constructor, "font", 2, JS_CFUNC_constructor, 0); JS_SetConstructor(js, font_ctor, proto); - - // Add font constructor to module (lowercase to match "new staef.font") JS_SetPropertyStr(js, mod, "font", font_ctor); + // Create SDF font constructor: sdf_font(data, em_px, range_px, padding_px, sharpness) + JSValue sdf_font_ctor = JS_NewCFunction2(js, js_sdf_font_constructor, "sdf_font", 5, JS_CFUNC_constructor, 0); + JS_SetConstructor(js, sdf_font_ctor, proto); + JS_SetPropertyStr(js, mod, "sdf_font", sdf_font_ctor); + + // Create MSDF font constructor: msdf_font(data, em_px, range_px, padding_px, sharpness) + JSValue msdf_font_ctor = JS_NewCFunction2(js, js_msdf_font_constructor, "msdf_font", 5, JS_CFUNC_constructor, 0); + JS_SetConstructor(js, msdf_font_ctor, proto); + JS_SetPropertyStr(js, mod, "msdf_font", msdf_font_ctor); + return mod; )