package font import "core:mem" import "core:math" // ============================================================================ // MSDF (MULTI-CHANNEL SIGNED DISTANCE FIELD) // ============================================================================ // Edge colors for multi-channel assignment Edge_Color :: enum u8 { Red = 1, Green = 2, Blue = 4, Cyan = 6, Magenta = 5, Yellow = 3, White = 7, } // Edge segment extracted from glyph vertices Edge_Segment :: struct { p0: [2]f32, p1: [2]f32, // endpoint (line) or control point (curve) p2: [2]f32, // endpoint for curves p3: [2]f32, // endpoint for cubics color: Edge_Color, kind: u8, } // Signed distance with pseudo-distance extension Dist_Result :: struct { dist: f32, // true signed distance pseudo_dist: f32, // pseudo-distance (extends past endpoints along tangent) } // Generate a multi-channel SDF (3 channels: R, G, B) for a glyph. // Returns a 3-channel bitmap (RGB interleaved, 3 bytes per pixel). get_glyph_msdf :: proc( info: ^Font_Info, scale: f32, glyph: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { return msdf_generate(info, scale, glyph, padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff, 3, allocator) } // Generate MTSDF (4 channels: RGB for multi-channel + Alpha for true distance) get_glyph_mtsdf :: proc( info: ^Font_Info, scale: f32, glyph: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { return msdf_generate(info, scale, glyph, padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff, 4, allocator) } get_codepoint_msdf :: proc( info: ^Font_Info, scale: f32, codepoint: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { return get_glyph_msdf(info, scale, find_glyph_index(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff, allocator) } get_codepoint_mtsdf :: proc( info: ^Font_Info, scale: f32, codepoint: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { return get_glyph_mtsdf(info, scale, find_glyph_index(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff, allocator) } free_msdf :: proc(bitmap: [^]u8) { mem.free(bitmap) } // ============================================================================ // Core generation // ============================================================================ @(private) msdf_generate :: proc( info: ^Font_Info, scale: f32, glyph: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, channels: i32, allocator := context.allocator, ) -> [^]u8 { verts: ^Vertex num_verts := get_glyph_shape(info, glyph, &verts) if num_verts == 0 || verts == nil { if width != nil do width^ = 0 if height != nil do height^ = 0 return nil } defer free_shape(info, verts) ix0, iy0, ix1, iy1: i32 get_glyph_bitmap_box_subpixel(info, glyph, scale, scale, 0, 0, &ix0, &iy0, &ix1, &iy1) w := ix1 - ix0 + padding * 2 h := iy1 - iy0 + padding * 2 if w <= 0 || h <= 0 { if width != nil do width^ = 0 if height != nil do height^ = 0 return nil } if width != nil do width^ = w if height != nil do height^ = h if xoff != nil do xoff^ = ix0 - padding if yoff != nil do yoff^ = iy0 - padding pixels_raw, _ := mem.alloc(int(w * h * channels), allocator = allocator) if pixels_raw == nil do return nil pixels := ([^]u8)(pixels_raw) // Extract edges with contour tracking edges: [512]Edge_Segment contour_starts: [64]i32 contour_count: i32 = 0 edge_count: i32 = 0 verts_arr := ([^]Vertex)(verts) edge_count = msdf_extract_edges(verts_arr, num_verts, edges[:], scale, contour_starts[:], &contour_count) // Color edges per contour msdf_color_edges_by_contour(edges[:edge_count], contour_starts[:contour_count], edge_count) origin_x := f32(ix0 - padding) origin_y := f32(iy0 - padding) for y in 0.. f32 { return -abs(d) if sign < 0 else abs(d) } @(private) dist_to_pixel :: #force_inline proc(dist: f32, pixel_dist_scale: f32, onedge_value: u8) -> u8 { val := f32(onedge_value) + dist * pixel_dist_scale return u8(clamp(val, 0, 255)) } // ============================================================================ // Edge extraction with contour tracking // ============================================================================ @(private) msdf_extract_edges :: proc(verts: [^]Vertex, num_verts: i32, edges: []Edge_Segment, scale: f32, contour_starts: []i32, contour_count: ^i32) -> i32 { count: i32 = 0 cx, cy: f32 for i in 0..= i32(len(contour_starts)) else contour_starts[ci + 1] n := end - start if n <= 0 do continue if n == 1 { edges[start].color = .White continue } if n == 2 { edges[start].color = .Cyan edges[start + 1].color = .Magenta continue } // Detect corners and assign colors with forced transitions at corners color_idx := 0 for ei in start.. 0.05 || dot < 0.5 { color_idx += 1 // Ensure adjacent edges at corners have different colors if colors[(color_idx) % 3] == edges[ei].color { color_idx += 1 } } } color_idx += 1 } } } @(private) edge_start_direction :: #force_inline proc(e: ^Edge_Segment) -> [2]f32 { switch e.kind { case VLINE: return normalize2({e.p1[0] - e.p0[0], e.p1[1] - e.p0[1]}) case VCURVE: d := [2]f32{e.p1[0] - e.p0[0], e.p1[1] - e.p0[1]} if d[0]*d[0] + d[1]*d[1] < 1e-12 { return normalize2({e.p2[0] - e.p0[0], e.p2[1] - e.p0[1]}) } return normalize2(d) case VCUBIC: d := [2]f32{e.p1[0] - e.p0[0], e.p1[1] - e.p0[1]} if d[0]*d[0] + d[1]*d[1] < 1e-12 { d = {e.p2[0] - e.p0[0], e.p2[1] - e.p0[1]} } if d[0]*d[0] + d[1]*d[1] < 1e-12 { return normalize2({e.p3[0] - e.p0[0], e.p3[1] - e.p0[1]}) } return normalize2(d) } return {1, 0} } @(private) edge_end_direction :: #force_inline proc(e: ^Edge_Segment) -> [2]f32 { switch e.kind { case VLINE: return normalize2({e.p1[0] - e.p0[0], e.p1[1] - e.p0[1]}) case VCURVE: d := [2]f32{e.p2[0] - e.p1[0], e.p2[1] - e.p1[1]} if d[0]*d[0] + d[1]*d[1] < 1e-12 { return normalize2({e.p2[0] - e.p0[0], e.p2[1] - e.p0[1]}) } return normalize2(d) case VCUBIC: d := [2]f32{e.p3[0] - e.p2[0], e.p3[1] - e.p2[1]} if d[0]*d[0] + d[1]*d[1] < 1e-12 { d = {e.p3[0] - e.p1[0], e.p3[1] - e.p1[1]} } if d[0]*d[0] + d[1]*d[1] < 1e-12 { return normalize2({e.p3[0] - e.p0[0], e.p3[1] - e.p0[1]}) } return normalize2(d) } return {1, 0} } @(private) normalize2 :: #force_inline proc(v: [2]f32) -> [2]f32 { l := math.sqrt(v[0]*v[0] + v[1]*v[1]) if l < 1e-12 do return {0, 0} return {v[0] / l, v[1] / l} } // ============================================================================ // Distance computation with pseudo-distance // ============================================================================ @(private) msdf_edge_distance :: proc(e: ^Edge_Segment, px, py: f32) -> Dist_Result { switch e.kind { case VLINE: return line_dist_pseudo(e.p0, e.p1, {px, py}) case VCURVE: return quad_dist_pseudo(e.p0, e.p1, e.p2, {px, py}) case VCUBIC: return cubic_dist_pseudo(e.p0, e.p1, e.p2, e.p3, {px, py}) } return {dist = 1e6, pseudo_dist = 1e6} } // Line segment distance with pseudo-distance extension @(private) line_dist_pseudo :: proc(a, b, p: [2]f32) -> Dist_Result { ab := [2]f32{b[0] - a[0], b[1] - a[1]} ap := [2]f32{p[0] - a[0], p[1] - a[1]} ab_len2 := ab[0]*ab[0] + ab[1]*ab[1] if ab_len2 < 1e-10 { d := math.sqrt(ap[0]*ap[0] + ap[1]*ap[1]) return {dist = d, pseudo_dist = d} } t := (ap[0]*ab[0] + ap[1]*ab[1]) / ab_len2 cross := ab[0] * ap[1] - ab[1] * ap[0] sign: f32 = 1.0 if cross >= 0 else -1.0 tc := clamp(t, 0, 1) dx := ap[0] - tc * ab[0] dy := ap[1] - tc * ab[1] true_dist := math.sqrt(dx*dx + dy*dy) * sign // Pseudo-distance: perpendicular distance to the infinite line perp_dist := cross / math.sqrt(ab_len2) pseudo := perp_dist if t >= 0 && t <= 1 else true_dist return {dist = true_dist, pseudo_dist = pseudo} } // Quadratic Bézier distance with pseudo-distance @(private) quad_dist_pseudo :: proc(p0, p1, p2, p: [2]f32) -> Dist_Result { // Find closest t by sampling + Newton refinement min_d2: f32 = 1e10 best_t: f32 = 0 for si in 0..=8 { t := f32(si) / 8.0 mt := 1 - t qx := mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0] qy := mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1] dx := p[0] - qx; dy := p[1] - qy d2 := dx*dx + dy*dy if d2 < min_d2 { min_d2 = d2; best_t = t } } // Newton refinement (3 iterations) for _ in 0..<3 { t := best_t; mt := 1 - t qx := mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0] qy := mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1] dqx := 2*(mt*(p1[0]-p0[0]) + t*(p2[0]-p1[0])) dqy := 2*(mt*(p1[1]-p0[1]) + t*(p2[1]-p1[1])) dx := qx - p[0]; dy := qy - p[1] num := dx*dqx + dy*dqy ddqx := 2*(p0[0] - 2*p1[0] + p2[0]) ddqy := 2*(p0[1] - 2*p1[1] + p2[1]) den := dqx*dqx + dqy*dqy + dx*ddqx + dy*ddqy if abs(den) > 1e-10 { best_t = clamp(t - num/den, 0, 1) } } t := best_t; mt := 1 - t qx := mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0] qy := mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1] dqx := 2*(mt*(p1[0]-p0[0]) + t*(p2[0]-p1[0])) dqy := 2*(mt*(p1[1]-p0[1]) + t*(p2[1]-p1[1])) dx := p[0] - qx; dy := p[1] - qy dist := math.sqrt(dx*dx + dy*dy) cross := dqx * dy - dqy * dx sign: f32 = 1.0 if cross >= 0 else -1.0 true_dist := dist * sign // Pseudo-distance: extend past endpoints along tangent pseudo := true_dist if t <= 0 || t >= 1 { // At endpoint — compute perpendicular distance to tangent line tan_x, tan_y: f32 if t <= 0 { tan_x = 2*(p1[0]-p0[0]); tan_y = 2*(p1[1]-p0[1]) dx2 := p[0] - p0[0]; dy2 := p[1] - p0[1] tan_len := math.sqrt(tan_x*tan_x + tan_y*tan_y) if tan_len > 1e-10 { perp := (tan_x * dy2 - tan_y * dx2) / tan_len pseudo = perp } } else { tan_x = 2*(p2[0]-p1[0]); tan_y = 2*(p2[1]-p1[1]) dx2 := p[0] - p2[0]; dy2 := p[1] - p2[1] tan_len := math.sqrt(tan_x*tan_x + tan_y*tan_y) if tan_len > 1e-10 { perp := (tan_x * dy2 - tan_y * dx2) / tan_len pseudo = perp } } } return {dist = true_dist, pseudo_dist = pseudo} } // Cubic Bézier distance with pseudo-distance @(private) cubic_dist_pseudo :: proc(p0, p1, p2, p3, p: [2]f32) -> Dist_Result { // Find closest t by sampling + Newton refinement min_d2: f32 = 1e10 best_t: f32 = 0 for si in 0..=12 { t := f32(si) / 12.0 mt := 1 - t qx := mt*mt*mt*p0[0] + 3*mt*mt*t*p1[0] + 3*mt*t*t*p2[0] + t*t*t*p3[0] qy := mt*mt*mt*p0[1] + 3*mt*mt*t*p1[1] + 3*mt*t*t*p2[1] + t*t*t*p3[1] dx := p[0] - qx; dy := p[1] - qy d2 := dx*dx + dy*dy if d2 < min_d2 { min_d2 = d2; best_t = t } } // Newton refinement for _ in 0..<4 { t := best_t; mt := 1 - t qx := mt*mt*mt*p0[0] + 3*mt*mt*t*p1[0] + 3*mt*t*t*p2[0] + t*t*t*p3[0] qy := mt*mt*mt*p0[1] + 3*mt*mt*t*p1[1] + 3*mt*t*t*p2[1] + t*t*t*p3[1] dqx := 3*(mt*mt*(p1[0]-p0[0]) + 2*mt*t*(p2[0]-p1[0]) + t*t*(p3[0]-p2[0])) dqy := 3*(mt*mt*(p1[1]-p0[1]) + 2*mt*t*(p2[1]-p1[1]) + t*t*(p3[1]-p2[1])) dx := qx - p[0]; dy := qy - p[1] num := dx*dqx + dy*dqy ddqx := 6*(mt*(p2[0]-2*p1[0]+p0[0]) + t*(p3[0]-2*p2[0]+p1[0])) ddqy := 6*(mt*(p2[1]-2*p1[1]+p0[1]) + t*(p3[1]-2*p2[1]+p1[1])) den := dqx*dqx + dqy*dqy + dx*ddqx + dy*ddqy if abs(den) > 1e-10 { best_t = clamp(t - num/den, 0, 1) } } t := best_t; mt := 1 - t qx := mt*mt*mt*p0[0] + 3*mt*mt*t*p1[0] + 3*mt*t*t*p2[0] + t*t*t*p3[0] qy := mt*mt*mt*p0[1] + 3*mt*mt*t*p1[1] + 3*mt*t*t*p2[1] + t*t*t*p3[1] dqx := 3*(mt*mt*(p1[0]-p0[0]) + 2*mt*t*(p2[0]-p1[0]) + t*t*(p3[0]-p2[0])) dqy := 3*(mt*mt*(p1[1]-p0[1]) + 2*mt*t*(p2[1]-p1[1]) + t*t*(p3[1]-p2[1])) dx := p[0] - qx; dy := p[1] - qy dist := math.sqrt(dx*dx + dy*dy) cross := dqx * dy - dqy * dx sign: f32 = 1.0 if cross >= 0 else -1.0 true_dist := dist * sign // Pseudo-distance at endpoints pseudo := true_dist if t <= 0 || t >= 1 { tan_x, tan_y, ref_x, ref_y: f32 if t <= 0 { tan_x = 3*(p1[0]-p0[0]); tan_y = 3*(p1[1]-p0[1]) ref_x = p[0] - p0[0]; ref_y = p[1] - p0[1] } else { tan_x = 3*(p3[0]-p2[0]); tan_y = 3*(p3[1]-p2[1]) ref_x = p[0] - p3[0]; ref_y = p[1] - p3[1] } tan_len := math.sqrt(tan_x*tan_x + tan_y*tan_y) if tan_len > 1e-10 { pseudo = (tan_x * ref_y - tan_y * ref_x) / tan_len } } return {dist = true_dist, pseudo_dist = pseudo} }