Files
game_engine/crates/voltex_editor/src/font.rs
2026-03-25 15:36:19 +09:00

314 lines
12 KiB
Rust

/// Bitmap font atlas for ASCII 32-126.
/// 8x12 pixel glyphs arranged in 16 columns x 6 rows = 128x72 texture.
pub struct FontAtlas {
pub width: u32,
pub height: u32,
pub glyph_width: u32,
pub glyph_height: u32,
pub pixels: Vec<u8>, // R8 grayscale
}
/// Classic 8x12 PC BIOS bitmap font data for ASCII 32-126 (95 characters).
/// Each character is 12 bytes (one byte per row, MSB = leftmost pixel).
#[rustfmt::skip]
const FONT_DATA: [u8; 95 * 12] = [
// 32: Space
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 33: !
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00,
// 34: "
0x00, 0x6C, 0x6C, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 35: #
0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00,
// 36: $
0x00, 0x18, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 37: %
0x00, 0x00, 0x62, 0x64, 0x08, 0x10, 0x26, 0x46, 0x00, 0x00, 0x00, 0x00,
// 38: &
0x00, 0x30, 0x48, 0x48, 0x30, 0x4A, 0x44, 0x3A, 0x00, 0x00, 0x00, 0x00,
// 39: '
0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 40: (
0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00,
// 41: )
0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
// 42: *
0x00, 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
// 43: +
0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
// 44: ,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
// 45: -
0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 46: .
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 47: /
0x00, 0x02, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00,
// 48: 0
0x00, 0x3C, 0x66, 0x6E, 0x7E, 0x76, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 49: 1
0x00, 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 50: 2
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 51: 3
0x00, 0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 52: 4
0x00, 0x0C, 0x1C, 0x2C, 0x4C, 0x7E, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x00,
// 53: 5
0x00, 0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 54: 6
0x00, 0x1C, 0x30, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 55: 7
0x00, 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
// 56: 8
0x00, 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 57: 9
0x00, 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x0C, 0x38, 0x00, 0x00, 0x00, 0x00,
// 58: :
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
// 59: ;
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
// 60: <
0x00, 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00,
// 61: =
0x00, 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 62: >
0x00, 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00,
// 63: ?
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
// 64: @
0x00, 0x3C, 0x66, 0x6E, 0x6A, 0x6E, 0x60, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 65: A
0x00, 0x18, 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 66: B
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 67: C
0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 68: D
0x00, 0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00, 0x00, 0x00, 0x00,
// 69: E
0x00, 0x7E, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 70: F
0x00, 0x7E, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 71: G
0x00, 0x3C, 0x66, 0x60, 0x6E, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 72: H
0x00, 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 73: I
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 74: J
0x00, 0x06, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 75: K
0x00, 0x66, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 76: L
0x00, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 77: M
0x00, 0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 78: N
0x00, 0x66, 0x76, 0x7E, 0x7E, 0x6E, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 79: O
0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 80: P
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 81: Q
0x00, 0x3C, 0x66, 0x66, 0x66, 0x6A, 0x6C, 0x36, 0x00, 0x00, 0x00, 0x00,
// 82: R
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 83: S
0x00, 0x3C, 0x66, 0x60, 0x3C, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 84: T
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 85: U
0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 86: V
0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 87: W
0x00, 0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 88: X
0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 89: Y
0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 90: Z
0x00, 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 91: [
0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 92: backslash
0x00, 0x40, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00,
// 93: ]
0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 94: ^
0x00, 0x18, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 95: _
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
// 96: `
0x00, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 97: a
0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 98: b
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 99: c
0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 100: d
0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 101: e
0x00, 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 102: f
0x00, 0x1C, 0x30, 0x30, 0x7C, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
// 103: g
0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x3C, 0x00, 0x00, 0x00,
// 104: h
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 105: i
0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 106: j
0x00, 0x0C, 0x00, 0x1C, 0x0C, 0x0C, 0x0C, 0x0C, 0x78, 0x00, 0x00, 0x00,
// 107: k
0x00, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 108: l
0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 109: m
0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 110: n
0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 111: o
0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 112: p
0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00, 0x00,
// 113: q
0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x00, 0x00, 0x00,
// 114: r
0x00, 0x00, 0x00, 0x7C, 0x66, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 115: s
0x00, 0x00, 0x00, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 116: t
0x00, 0x30, 0x30, 0x7C, 0x30, 0x30, 0x30, 0x1C, 0x00, 0x00, 0x00, 0x00,
// 117: u
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 118: v
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 119: w
0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, 0x00,
// 120: x
0x00, 0x00, 0x00, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 121: y
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x3C, 0x00, 0x00, 0x00,
// 122: z
0x00, 0x00, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 123: {
0x00, 0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, 0x00,
// 124: |
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 125: }
0x00, 0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, 0x00,
// 126: ~
0x00, 0x32, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
impl FontAtlas {
/// Generate a bitmap font atlas with real 8x12 PC BIOS-style glyphs.
/// Each glyph is 8x12 pixels. Atlas is 16 cols x 6 rows = 128x72.
pub fn generate() -> Self {
let glyph_width: u32 = 8;
let glyph_height: u32 = 12;
let cols: u32 = 16;
let rows: u32 = 6;
let width = cols * glyph_width; // 128
let height = rows * glyph_height; // 72
let mut pixels = vec![0u8; (width * height) as usize];
// Unpack 1-bit-per-pixel FONT_DATA into R8 pixel atlas
for code in 32u8..=126u8 {
let index = (code - 32) as u32;
let col = index % cols;
let row = index / cols;
let base_x = col * glyph_width;
let base_y = row * glyph_height;
let data_offset = (index as usize) * 12;
for py in 0..12u32 {
let row_byte = FONT_DATA[data_offset + py as usize];
for px in 0..8u32 {
if (row_byte >> (7 - px)) & 1 != 0 {
let pixel_idx = ((base_y + py) * width + (base_x + px)) as usize;
pixels[pixel_idx] = 255;
}
}
}
}
// Ensure pixel (0,0) is white (255) so solid-color rects can sample
// UV (0,0) and get full brightness for tinting.
pixels[0] = 255;
FontAtlas {
width,
height,
glyph_width,
glyph_height,
pixels,
}
}
/// Returns (u0, v0, u1, v1) UV coordinates for a given character.
/// Returns coordinates for space if character is out of ASCII range.
pub fn glyph_uv(&self, ch: char) -> (f32, f32, f32, f32) {
let code = ch as u32;
let index = if code >= 32 && code <= 126 {
code - 32
} else {
0 // space
};
let cols = self.width / self.glyph_width;
let col = index % cols;
let row = index / cols;
let u0 = (col * self.glyph_width) as f32 / self.width as f32;
let v0 = (row * self.glyph_height) as f32 / self.height as f32;
let u1 = u0 + self.glyph_width as f32 / self.width as f32;
let v1 = v0 + self.glyph_height as f32 / self.height as f32;
(u0, v0, u1, v1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_size() {
let atlas = FontAtlas::generate();
assert_eq!(atlas.width, 128);
assert_eq!(atlas.height, 72);
assert_eq!(atlas.pixels.len(), (128 * 72) as usize);
}
#[test]
fn test_glyph_uv_space() {
let atlas = FontAtlas::generate();
let (u0, v0, u1, v1) = atlas.glyph_uv(' ');
assert!((u0 - 0.0).abs() < 1e-6);
assert!((v0 - 0.0).abs() < 1e-6);
assert!((u1 - 8.0 / 128.0).abs() < 1e-6);
assert!((v1 - 12.0 / 72.0).abs() < 1e-6);
}
#[test]
fn test_glyph_uv_a() {
let atlas = FontAtlas::generate();
// 'A' = ASCII 65, index = 65 - 32 = 33
// col = 33 % 16 = 1, row = 33 / 16 = 2
let (u0, v0, u1, v1) = atlas.glyph_uv('A');
let expected_u0 = 1.0 * 8.0 / 128.0;
let expected_v0 = 2.0 * 12.0 / 72.0;
let expected_u1 = expected_u0 + 8.0 / 128.0;
let expected_v1 = expected_v0 + 12.0 / 72.0;
assert!((u0 - expected_u0).abs() < 1e-6, "u0 mismatch: {} vs {}", u0, expected_u0);
assert!((v0 - expected_v0).abs() < 1e-6, "v0 mismatch: {} vs {}", v0, expected_v0);
assert!((u1 - expected_u1).abs() < 1e-6);
assert!((v1 - expected_v1).abs() < 1e-6);
}
}