Switch to the Dinowyde font with DINOBYTE for missing glyphs
Unicode glyph lookup instead of CP437 codes, rope and double-strand frames with clean title ends, progress bars in eighth blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d99166a746
commit
d76e74b4b0
11 files changed
+225
-65
No files matched your search
+191
-39
@@ -85,53 +85,136 @@ pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
pub sprites: Texture2D,
|
||||
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
|
||||
pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px
|
||||
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
|
||||
pub font_map: FontMap, // which glyphs the two fonts hold
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ bitmap font (CP437)
|
||||
// ------------------------------------------------------------------ bitmap font (Dinowyde)
|
||||
//
|
||||
// assets/dinowyde_1x.png: 6x8 px glyphs, ordered by Unicode in five columns of 16 glyphs
|
||||
// (96 px) and 48 rows each:
|
||||
// column 0: U+0000-02FF (Basic Latin, Latin-1, Latin Extended A/B, IPA, spacing modifiers)
|
||||
// column 1: U+0310-05FF (combining marks from U+0310, Greek, Cyrillic, Armenian, Hebrew;
|
||||
// U+0300-030F have no slot)
|
||||
// column 2: U+0600-06FF (Arabic) in rows 0-15, U+2000-21FF (punctuation ... arrows) in rows 16-47
|
||||
// column 3: U+2200-24FF (mathematical operators, technical, ...)
|
||||
// column 4: U+2500-27FF (box drawing, blocks, geometric shapes, ...)
|
||||
|
||||
pub const GW: f32 = 6.0; // glyph width
|
||||
pub const GH: f32 = 8.0; // glyph height
|
||||
const FONT_COLS: usize = 80; // glyphs per row of the sheet
|
||||
const FONT_ROWS: usize = 48;
|
||||
|
||||
// place of a character in the sheet (glyph index row * FONT_COLS + column), if the sheet has a slot for it
|
||||
pub fn font_slot(ch: char) -> Option<usize> {
|
||||
let u = ch as usize;
|
||||
let (block, off) = match u {
|
||||
0x0000..=0x02FF => (0, u),
|
||||
0x0310..=0x05FF => (1, u - 0x0310),
|
||||
0x0600..=0x06FF => (2, u - 0x0600),
|
||||
0x2000..=0x21FF => (2, 0x100 + u - 0x2000),
|
||||
0x2200..=0x24FF => (3, u - 0x2200),
|
||||
0x2500..=0x27FF => (4, u - 0x2500),
|
||||
_ => return None,
|
||||
};
|
||||
Some((off / 16) * FONT_COLS + block * 16 + off % 16)
|
||||
}
|
||||
|
||||
// Characters Dinowyde does not draw yet (arrows, card suits, smileys, notes, ...) come from the
|
||||
// DINOBYTE font (assets/dbyte_1x.png, CP437 order, 16x16 glyphs), which sits right of the
|
||||
// Dinowyde sheet in the font texture.
|
||||
const DBYTE_X: usize = FONT_COLS * GW as usize; // x of the DINOBYTE sheet in the font texture
|
||||
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
|
||||
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
|
||||
|
||||
pub fn cp437(ch: char) -> u8 {
|
||||
// the CP437 code of a character (DINOBYTE), if it has one
|
||||
fn cp437(ch: char) -> Option<u8> {
|
||||
// DINOBYTE has ö (0x94) and ò (0x95) swapped
|
||||
match ch {
|
||||
'ö' => return 0x95,
|
||||
'ò' => return 0x94,
|
||||
'ö' => return Some(0x95),
|
||||
'ò' => return Some(0x94),
|
||||
'⌂' => return Some(127),
|
||||
_ => {}
|
||||
}
|
||||
let u = ch as u32;
|
||||
if (32..127).contains(&u) {
|
||||
return u as u8;
|
||||
return Some(u as u8);
|
||||
}
|
||||
if ch == '⌂' {
|
||||
return 127;
|
||||
if let Some(i) = CP437_LOW.chars().position(|c| c == ch).filter(|&i| i > 0) {
|
||||
return Some(i as u8);
|
||||
}
|
||||
if let Some(i) = CP437_LOW.chars().position(|c| c == ch) {
|
||||
if i > 0 {
|
||||
return i as u8;
|
||||
CP437_HIGH.chars().position(|c| c == ch).map(|i| 128 + i as u8)
|
||||
}
|
||||
|
||||
// stand-ins for characters neither font draws: similar glyphs
|
||||
fn font_substitute(ch: char) -> Option<char> {
|
||||
Some(match ch {
|
||||
'✓' | '✔' => '√',
|
||||
'ẞ' => 'ß',
|
||||
'–' | '—' => '-',
|
||||
'„' | '“' | '”' => '"',
|
||||
'‚' | '‘' | '’' => '\'',
|
||||
'…' => '.',
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// which glyphs the two sheets hold
|
||||
pub struct FontMap {
|
||||
has: Vec<bool>, // Dinowyde slots with a glyph
|
||||
dbyte: [bool; 256], // DINOBYTE codes with a glyph
|
||||
}
|
||||
|
||||
fn glyph_drawn(data: &[[u8; 4]], w: usize, gx: usize, gy: usize) -> bool {
|
||||
(0..GH as usize).any(|y| (0..GW as usize).any(|x| data.get((gy + y) * w + gx + x).map_or(false, |p| p[3] > 0)))
|
||||
}
|
||||
|
||||
// the font texture: Dinowyde, and DINOBYTE right of it; white glyphs on a transparent
|
||||
// background (for tinting)
|
||||
pub fn load_font() -> (Image, FontMap) {
|
||||
let wyde = Image::from_file_with_format(include_bytes!("../assets/dinowyde_1x.png"), Some(ImageFormat::Png)).expect("font");
|
||||
let byte = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("font");
|
||||
let (w, h) = (DBYTE_X + byte.width as usize, (wyde.height as usize).max(byte.height as usize));
|
||||
let mut img = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||
{
|
||||
let out = img.get_image_data_mut();
|
||||
let ww = wyde.width as usize;
|
||||
for (i, p) in wyde.get_image_data().iter().enumerate() {
|
||||
if p[3] > 128 {
|
||||
out[(i / ww) * w + i % ww] = [255, 255, 255, 255];
|
||||
}
|
||||
}
|
||||
let bw = byte.width as usize;
|
||||
for (i, p) in byte.get_image_data().iter().enumerate() {
|
||||
if p[3] > 128 && p[0] > 128 {
|
||||
out[(i / bw) * w + DBYTE_X + i % bw] = [255, 255, 255, 255];
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(i) = CP437_HIGH.chars().position(|c| c == ch) {
|
||||
return 128 + i as u8;
|
||||
}
|
||||
match ch {
|
||||
'ẞ' => 0xE1,
|
||||
'–' | '—' => b'-',
|
||||
'„' | '“' | '”' => b'"',
|
||||
'‚' | '‘' | '’' => b'\'',
|
||||
'…' => b'.',
|
||||
'×' => b'x',
|
||||
_ => b'?',
|
||||
let data = img.get_image_data();
|
||||
let has = (0..FONT_COLS * FONT_ROWS).map(|g| glyph_drawn(data, w, (g % FONT_COLS) * GW as usize, (g / FONT_COLS) * GH as usize)).collect();
|
||||
let mut dbyte = [false; 256];
|
||||
for (c, d) in dbyte.iter_mut().enumerate() {
|
||||
*d = glyph_drawn(data, w, DBYTE_X + (c % 16) * GW as usize, (c / 16) * GH as usize);
|
||||
}
|
||||
(img, FontMap { has, dbyte })
|
||||
}
|
||||
|
||||
// where in the font texture a character's glyph is (top-left pixel): Dinowyde, else
|
||||
// DINOBYTE, else a stand-in, else '?'
|
||||
pub fn font_src(map: &FontMap, ch: char) -> (f32, f32) {
|
||||
let find = |c: char| -> Option<(f32, f32)> {
|
||||
if let Some(g) = font_slot(c).filter(|&g| map.has.get(g).copied().unwrap_or(false)) {
|
||||
return Some(((g % FONT_COLS) as f32 * GW, (g / FONT_COLS) as f32 * GH));
|
||||
}
|
||||
cp437(c).filter(|&k| map.dbyte[k as usize]).map(|k| ((DBYTE_X + (k as usize % 16) * GW as usize) as f32, (k / 16) as f32 * GH))
|
||||
};
|
||||
find(ch).or_else(|| font_substitute(ch).and_then(find)).or_else(|| find('?')).unwrap_or((0.0, 0.0))
|
||||
}
|
||||
|
||||
// frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical]
|
||||
pub const BOX_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3];
|
||||
pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA];
|
||||
pub const BOX_SINGLE: [char; 6] = ['┌', '┐', '└', '┘', '─', '│'];
|
||||
pub const BOX_DOUBLE: [char; 6] = ['╔', '╗', '╚', '╝', '═', '║'];
|
||||
|
||||
fn hash(x: i32, y: i32, s: u32) -> u32 {
|
||||
let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ s.wrapping_mul(2246822519);
|
||||
@@ -1772,17 +1855,13 @@ impl Gfx {
|
||||
atlas.set_filter(FilterMode::Nearest);
|
||||
let sprites = Texture2D::from_image(&gen_sprites());
|
||||
sprites.set_filter(FilterMode::Nearest);
|
||||
// load the font; transparent background, white glyphs (for tinting)
|
||||
let mut img = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("Schrift");
|
||||
for px in img.get_image_data_mut().iter_mut() {
|
||||
let on = px[3] > 128 && px[0] > 128;
|
||||
*px = if on { [255, 255, 255, 255] } else { [0, 0, 0, 0] };
|
||||
}
|
||||
// the fonts (Dinowyde, DINOBYTE for what it lacks)
|
||||
let (img, font_map) = load_font();
|
||||
let font = Texture2D::from_image(&img);
|
||||
font.set_filter(FilterMode::Nearest);
|
||||
let cursors = Texture2D::from_image(&gen_cursors());
|
||||
cursors.set_filter(FilterMode::Nearest);
|
||||
Gfx { atlas, sprites, cursors, font }
|
||||
Gfx { atlas, sprites, cursors, font, font_map }
|
||||
}
|
||||
|
||||
pub fn cell(id: u32) -> Rect {
|
||||
@@ -1881,9 +1960,10 @@ impl Gfx {
|
||||
);
|
||||
}
|
||||
|
||||
// draw a single CP437 glyph (x, y = top-left corner)
|
||||
pub fn glyph(&self, code: u8, x: f32, y: f32, scale: f32, color: Color) {
|
||||
let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, GW, GH);
|
||||
// draw a single character (x, y = top-left corner)
|
||||
pub fn glyph(&self, ch: char, x: f32, y: f32, scale: f32, color: Color) {
|
||||
let (sx, sy) = font_src(&self.font_map, ch);
|
||||
let src = Rect::new(sx, sy, GW, GH);
|
||||
draw_texture_ex(
|
||||
&self.font,
|
||||
x.floor(),
|
||||
@@ -1898,7 +1978,7 @@ impl Gfx {
|
||||
let mut cx = x;
|
||||
for ch in s.chars() {
|
||||
if ch != ' ' {
|
||||
self.glyph(cp437(ch), cx, y, scale, color);
|
||||
self.glyph(ch, cx, y, scale, color);
|
||||
}
|
||||
cx += GW * scale;
|
||||
}
|
||||
@@ -1971,7 +2051,7 @@ impl Gfx {
|
||||
self.text(s, x, y, scale, color);
|
||||
}
|
||||
|
||||
// window with a CP437 frame; size in characters (cols x rows incl. frame)
|
||||
// window with a box-drawing frame; size in characters (cols x rows incl. frame)
|
||||
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
|
||||
let cw = GW * scale;
|
||||
let ch = GH * scale;
|
||||
@@ -2003,9 +2083,21 @@ impl Gfx {
|
||||
if !title.is_empty() {
|
||||
let cw = GW * scale;
|
||||
let t = format!(" {} ", title);
|
||||
let tx = x + 2.0 * cw;
|
||||
draw_rectangle(tx, y, self.measure(&t, scale), GH * scale, bg);
|
||||
self.text(&t, tx, y, scale, tcol);
|
||||
let n = t.chars().count() as i32;
|
||||
// the frame line ends cleanly on both sides of the title (rope ends, or the double
|
||||
// strand's caps), if there is room for them
|
||||
let caps = n + 5 <= cols;
|
||||
let tx = x + if caps { 1.0 } else { 2.0 } * cw;
|
||||
let w = cw * (n + if caps { 2 } else { 0 }) as f32;
|
||||
draw_rectangle(tx, y, w, GH * scale, bg);
|
||||
if caps {
|
||||
let (l, r) = if double { ('╡', '╞') } else { ('╴', '╶') };
|
||||
self.glyph(l, tx, y, scale, frame);
|
||||
self.text(&t, tx + cw, y, scale, tcol);
|
||||
self.glyph(r, tx + cw * (n + 1) as f32, y, scale, frame);
|
||||
} else {
|
||||
self.text(&t, tx, y, scale, tcol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2057,3 +2149,63 @@ mod doc_images {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod font_tests {
|
||||
use super::*;
|
||||
|
||||
// the glyph of a character as rows of '#' and '.'
|
||||
fn bitmap(img: &Image, ch: char) -> Vec<String> {
|
||||
let g = font_slot(ch).expect("slot");
|
||||
let (gx, gy) = ((g % FONT_COLS) * 6, (g / FONT_COLS) * 8);
|
||||
let w = img.width as usize;
|
||||
let d = img.get_image_data();
|
||||
(0..8).map(|y| (0..6).map(|x| if d[(gy + y) * w + gx + x][3] > 0 { '#' } else { '.' }).collect()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dinowyde_slots_match_the_sheet() {
|
||||
let (img, _) = load_font();
|
||||
assert_eq!(bitmap(&img, 'A'), ["......", ".###..", "#...#.", "#...#.", "#####.", "#...#.", "#...#.", "......"]);
|
||||
assert_eq!(bitmap(&img, 'Ж'), ["......", "#.#.#.", "#.#.#.", ".###..", "#.#.#.", "#.#.#.", "#.#.#.", "......"]);
|
||||
assert_eq!(bitmap(&img, 'Ο')[2..4], [".###..", "#...#."], "Greek capital omicron");
|
||||
assert!(bitmap(&img, '▀')[0..4].iter().all(|r| r == "######"), "upper half block");
|
||||
assert_eq!(bitmap(&img, '–')[4], ".###..", "en dash");
|
||||
assert_eq!(bitmap(&img, '—')[4], "#####.", "em dash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_glyphs_come_from_dinobyte() {
|
||||
let (_, map) = load_font();
|
||||
let from_dbyte = |ch: char| font_src(&map, ch).0 >= DBYTE_X as f32;
|
||||
assert!(!from_dbyte('A') && !from_dbyte('ö') && !from_dbyte('Ж') && !from_dbyte('─'), "Dinowyde first");
|
||||
for ch in ['↑', '↓', '→', '←', '☺', '♥', '♪', '☼'] {
|
||||
assert!(from_dbyte(ch), "{} from DINOBYTE", ch);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_text_of_the_game_has_its_glyphs() {
|
||||
let (_, map) = load_font();
|
||||
let question = font_src(&map, '?');
|
||||
let mut missing = std::collections::BTreeSet::new();
|
||||
for file in [include_str!("../lang/en.lang"), include_str!("../lang/de.lang")] {
|
||||
for ch in file.chars().filter(|c| !c.is_control() && *c != ' ') {
|
||||
if ch != '?' && font_src(&map, ch) == question {
|
||||
missing.insert(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
// interface symbols used in the code
|
||||
for ch in ['█', '░', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '▼', '▲', '☻', '≡', '☼', '●', '○', '✓', '—', '·', '↑', '↓', '→', '←', '♥', '♪', '┌', '┐', '└', '┘', '─', '│', '╴', '╶', '╔', '╗', '╚', '╝', '═', '║', '╡', '╞'] {
|
||||
if font_src(&map, ch) == question {
|
||||
missing.insert(ch);
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "no glyph for {:?}", missing);
|
||||
// Cyrillic and Greek are there for later languages
|
||||
for ch in "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёΑΒΓΔαβγδ".chars() {
|
||||
assert_ne!(font_src(&map, ch), question, "{}", ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user