Skip to content

Instantly share code, notes, and snippets.

@marcs-feh
Created February 26, 2025 01:49
Show Gist options
  • Save marcs-feh/d83458e2bed4ce51f9e6da78dc837da7 to your computer and use it in GitHub Desktop.
Save marcs-feh/d83458e2bed4ce51f9e6da78dc837da7 to your computer and use it in GitHub Desktop.
package fnt
import "core:mem"
import "core:fmt"
import "core:math"
import "core:time"
import rl "vendor:raylib"
import ttf "vendor:stb/truetype"
FONT_DATA :: #load("jetbrains.ttf", []byte)
Color :: [4]u8
Bitmap :: struct {
pixels: []Color,
width: int,
height: int,
offset: [2]int,
}
DistanceField :: struct {
values: []u8,
width, height: int,
offset: [2]int,
}
Font :: struct {
info: ttf.fontinfo,
atlas: GlyphAtlas,
height: f32,
// SDF Rendering parameters
padding: i32,
edge_value: f32,
dist_scale: f32,
sharpness: f32,
shift: f32,
}
GLYPHS_PER_ATLAS :: 256
AtlasPos :: struct {
atlas_offset: [2]int,
glyph_offset: [2]int,
width, height: int,
}
GlyphAtlas :: struct {
pixels: []Color,
width, height: int,
base: rune,
glyphs: [GLYPHS_PER_ATLAS]AtlasPos,
}
bmp_pack_rows :: proc(rects: []Bitmap) -> (container_width: int, container_height: int){
}
create_glyph_atlas :: proc(font: ^Font, base: rune) -> (atlas: GlyphAtlas, err: mem.Allocator_Error) {
base := mem.align_backward_int(int(base), GLYPHS_PER_ATLAS)
bitmaps := [GLYPHS_PER_ATLAS]Bitmap{}
for i in 0..<GLYPHS_PER_ATLAS {
bitmaps[i] = render_codepoint_bitmap(font, rune(int(base) + 1), context.temp_allocator) or_return
atlas.width += bitmaps[i].width
atlas.height = max(atlas.height, bitmaps[i].height)
}
atlas.pixels = make([]Color, atlas.width * atlas.height)
return
}
sdf_activation :: proc "contextless" (x: f32, a: f32, t: f32) -> f32 {
return 1.0 / (1 + math.exp(- a * (x - t)))
}
bitmap_create :: proc(width, height: int, allocator := context.allocator) -> (bm: Bitmap, error: mem.Allocator_Error) {
assert(width >= 0 && height >= 0, "Bad width/height")
bm.pixels = make([]Color, width * height, allocator) or_return
bm.width = width
bm.height = height
return
}
bitmap_destroy :: proc(bm: ^Bitmap, allocator := context.allocator){
delete(bm.pixels, allocator)
}
DEFAULT_PADDING :: 0
DEFAULT_SIG_ALPHA :: 9.25
DEFAULT_EDGE_VALUE :: 0.63
DEFAULT_DIST_SCALE :: 0.61
font_load :: proc(data: []byte, height: f32) -> (font: Font, ok := true) {
err : mem.Allocator_Error
bool(ttf.InitFont(&font.info, raw_data(data), 0)) or_return
font.height = height
font.padding = DEFAULT_PADDING
font.sharpness = DEFAULT_SIG_ALPHA
font.dist_scale = DEFAULT_DIST_SCALE
font.edge_value = DEFAULT_EDGE_VALUE
ok = err == nil
return
}
font_flush_cache :: proc(font: ^Font){
// for _, &bmap in font.cache {
// bitmap_destroy(&bmap)
// }
// clear(&font.cache)
}
render_codepoint_sdf :: proc(font: ^Font, codepoint: rune) -> DistanceField {
scale := ttf.ScaleForPixelHeight(&font.info, font.height)
padding := font.padding
on_edge := u8(clamp(0, font.edge_value * 255, 0xff))
dist_scale := (font.edge_value * font.dist_scale) * 255
width, height, x_off, y_off : i32
sdf_pixels := ttf.GetCodepointSDF(&font.info, scale, i32(codepoint), padding, on_edge, dist_scale,
&width, &height, &x_off, &y_off)
ensure(sdf_pixels != nil, "STB failed to allocate SDF")
sdf := DistanceField {
values = sdf_pixels[:width * height],
width = int(width),
height = int(height),
offset = {int(x_off), int(y_off)},
}
return sdf
}
render_codepoint_bitmap :: proc(font: ^Font, codepoint: rune, allocator := context.allocator) -> (bmap: Bitmap, err: mem.Allocator_Error) {
sdf := render_codepoint_sdf(font, codepoint)
defer ttf.FreeSDF(raw_data(sdf.values), nil)
bmap = bitmap_create(sdf.width, sdf.height, allocator) or_return
bmap.offset = sdf.offset
for y in 0..<bmap.height {
for x in 0..<bmap.width {
v := f32(sdf.values[y * sdf.width + x]) / 255.0
px := sdf_activation(v, font.sharpness, 0.5) * 255
if px > 20 {
bmap.pixels[y * bmap.width + x] = u8(px)
}
}
}
return
}
to_rl_texture :: proc(bmap: Bitmap) -> rl.Texture {
img := rl.Image{
width = i32(bmap.width),
height = i32(bmap.height),
data = raw_data(bmap.pixels),
mipmaps = 1,
format = .UNCOMPRESSED_R8G8B8A8,
}
tex := rl.LoadTextureFromImage(img)
return tex
}
render_text :: proc(font: ^Font, text: string, origin: [2]int){
x := origin.x
line_offset := 0
scale := ttf.ScaleForPixelHeight(&font.info, font.height)
for r, i in text {
bmap, _ := render_codepoint_bitmap(font, r)
tex := get_codepoint_texture(font, r)
if r == '\n' {
line_offset += int(font.height) + 4
x = origin.x
continue
}
advance, lsb : i32
ttf.GetCodepointHMetrics(&font.info, r, &advance, &lsb)
next_i := min(len(text) - 1, i + 1)
kern_adv := f32(ttf.GetGlyphKernAdvance(&font.info, i32(r), i32(text[next_i]))) * scale
defer x += int(math.round(kern_adv))
x += int(f32(advance) * scale) + int(f32(lsb) * scale)
y := origin.y + bmap.offset.y + line_offset
TEXTURE_SCALE :: 1
rl.DrawTextureEx(tex, {f32(x) ,f32(y)}, 0, TEXTURE_SCALE, {0xff, 0xff, 0xff, 0xff})
}
}
texture_cache := make(map[rune]rl.Texture)
get_codepoint_texture :: proc(font: ^Font, r: rune) -> rl.Texture {
if tex, ok := texture_cache[r]; ok {
return tex
}
else {
bmap, _ := render_codepoint_bitmap(font, r)
tex = to_rl_texture(bmap)
texture_cache[r] = tex
return tex
}
}
flush_cache :: proc(font: ^Font){
for _, tex in texture_cache {
rl.UnloadTexture(tex)
}
clear(&texture_cache)
// for _, &bmap in font.cache {
// bitmap_destroy(&bmap)
// }
// clear(&font.cache)
}
main :: proc(){
rl.InitWindow(800, 600, "fntest")
rl.SetTargetFPS(60)
defer rl.CloseWindow()
font, font_ok := font_load(FONT_DATA, 16)
ensure(font_ok, "Font is fucked")
bmap, _ := render_codepoint_bitmap(&font, 'G')
for !rl.WindowShouldClose(){
defer free_all(context.temp_allocator)
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
mouse_pos := rl.GetMousePosition()
msg := fmt.tprintf("Sig Alpha: %v\nEdge Val: %.3f\nDist Scale: %.3f\nHeight: %.2fpx", font.sharpness, font.edge_value, font.dist_scale, font.height)
rl.DrawText(transmute(cstring)raw_data(msg), 20, 320, 18, rl.WHITE)
if rl.IsKeyDown(.ONE){ font.sharpness -= 0.25; flush_cache(&font) }
if rl.IsKeyDown(.TWO){ font.sharpness += 0.25; flush_cache(&font) }
if rl.IsKeyDown(.THREE){ font.edge_value -= 0.01; flush_cache(&font) }
if rl.IsKeyDown(.FOUR) { font.edge_value += 0.01; flush_cache(&font) }
if rl.IsKeyDown(.FIVE){ font.dist_scale -= 0.01; flush_cache(&font) }
if rl.IsKeyDown(.SIX) { font.dist_scale += 0.01; flush_cache(&font) }
if rl.IsKeyDown(.SEVEN){ font.height -= 0.5; flush_cache(&font) }
if rl.IsKeyDown(.EIGHT){ font.height += 0.5; flush_cache(&font) }
// render_text(&font, "The quick brown fox jumped over the lazy dog", {20, 40})
// render_text(&font, "(0 + 1) / 2 * 3 - (4 ^ 5) / ((6 % 7) + 8 - 9)", {20, 80})
// render_text(&font, "Hoje à noite, sem luz, decidi xeretar a quinta gaveta da vovó: achei linguiça, pão e fubá", {20, 120})
// render_text(&font,
// `#include <stdio.h>
//
// int main(){
// printf("Hello, world!\n");
// return 0;
// }
// `, {20, 160})
rl.EndDrawing()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment