Harbor

branch main
showing the latest snapshot on main
msdf.odin 15.1 KB · Plain text
msdf.odin 0644 Raw
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..<h {
for x in 0..<w {
px := f32(x) + origin_x + 0.5
py := f32(y) + origin_y + 0.5

// Find closest distance per channel using pseudo-distance
min_r := Dist_Result{dist = 1e6, pseudo_dist = 1e6}
min_g := Dist_Result{dist = 1e6, pseudo_dist = 1e6}
min_b := Dist_Result{dist = 1e6, pseudo_dist = 1e6}
min_true := Dist_Result{dist = 1e6, pseudo_dist = 1e6}

for ei in 0..<edge_count {
e := &edges[ei]
dr := msdf_edge_distance(e, px, py)

if abs(dr.dist) < abs(min_true.dist) {
min_true = dr
}
if u8(e.color) & u8(Edge_Color.Red) != 0 {
if abs(dr.pseudo_dist) < abs(min_r.pseudo_dist) do min_r = dr
}
if u8(e.color) & u8(Edge_Color.Green) != 0 {
if abs(dr.pseudo_dist) < abs(min_g.pseudo_dist) do min_g = dr
}
if u8(e.color) & u8(Edge_Color.Blue) != 0 {
if abs(dr.pseudo_dist) < abs(min_b.pseudo_dist) do min_b = dr
}
}

// Determine sign from winding
crossings := compute_crossings_x(px, py, num_verts, verts_arr)
sign: f32 = -1.0 if crossings != 0 else 1.0

// Apply sign to pseudo-distances
pr := apply_sign(min_r.pseudo_dist, sign)
pg := apply_sign(min_g.pseudo_dist, sign)
pb := apply_sign(min_b.pseudo_dist, sign)

idx := (y * w + x) * channels
pixels[idx + 0] = dist_to_pixel(pr, pixel_dist_scale, onedge_value)
pixels[idx + 1] = dist_to_pixel(pg, pixel_dist_scale, onedge_value)
pixels[idx + 2] = dist_to_pixel(pb, pixel_dist_scale, onedge_value)

if channels == 4 {
pt := apply_sign(min_true.dist, sign)
pixels[idx + 3] = dist_to_pixel(pt, pixel_dist_scale, onedge_value)
}
}
}

return pixels
}

@(private)
apply_sign :: #force_inline proc(d: f32, sign: f32) -> 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..<num_verts {
v := verts[i]
vx := f32(v.x) * scale
vy := f32(v.y) * scale
switch v.type {
case VMOVE:
if contour_count^ < i32(len(contour_starts)) {
contour_starts[contour_count^] = count
contour_count^ += 1
}
cx = vx; cy = vy
case VLINE:
if count < i32(len(edges)) {
edges[count] = {p0 = {cx, cy}, p1 = {vx, vy}, kind = VLINE, color = .White}
count += 1
}
cx = vx; cy = vy
case VCURVE:
if count < i32(len(edges)) {
edges[count] = {
p0 = {cx, cy},
p1 = {f32(v.cx) * scale, f32(v.cy) * scale},
p2 = {vx, vy},
kind = VCURVE,
color = .White,
}
count += 1
}
cx = vx; cy = vy
case VCUBIC:
if count < i32(len(edges)) {
edges[count] = {
p0 = {cx, cy},
p1 = {f32(v.cx) * scale, f32(v.cy) * scale},
p2 = {f32(v.cx1) * scale, f32(v.cy1) * scale},
p3 = {vx, vy},
kind = VCUBIC,
color = .White,
}
count += 1
}
cx = vx; cy = vy
}
}
return count
}

// ============================================================================
// Edge coloring: per-contour, corner-aware
// ============================================================================

@(private)
msdf_color_edges_by_contour :: proc(edges: []Edge_Segment, contour_starts: []i32, total_edges: i32) {
colors := [?]Edge_Color{.Cyan, .Magenta, .Yellow}

for ci in 0..<i32(len(contour_starts)) {
start := contour_starts[ci]
end := total_edges if ci + 1 >= 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..<end {
edges[ei].color = colors[color_idx % 3]

if ei + 1 < end {
// Check for corner between this edge and next
d1 := edge_end_direction(&edges[ei])
d2 := edge_start_direction(&edges[ei + 1])
cross := d1[0] * d2[1] - d1[1] * d2[0]
dot := d1[0] * d2[0] + d1[1] * d2[1]

// Corner: significant direction change
if abs(cross) > 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}
}