diff --git a/CREDITS.md b/CREDITS.md index 5274c3e..e92952c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,9 +1,15 @@ # Credits -## Font: DINOBYTE bitmap font +## Font: Dinowyde bitmap font +- Author: **mby** – https://mby.itch.io/ +- License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/ +- File: `assets/dinowyde_1x.png` (unmodified, work-in-progress version of the font). 6x8 px glyphs ordered by Unicode: Latin with all European accents, Greek, Cyrillic and many symbols. In the game it is scaled by integer factors and tinted; characters the sheet does not draw yet (arrows, card suits, smileys, notes, ...) come from DINOBYTE (below). +- `assets/dinowyde_wip.png` is the font's overview sheet (which Unicode blocks are where). + +## Font: DINOBYTE bitmap font (for the characters Dinowyde does not have yet) - Author: **mby** – https://mby.itch.io/dinobyte - License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/ -- File: `assets/dbyte_1x.png` (unmodified). In the game it is scaled by integer factors and tinted. The swapped glyphs ö/ò are corrected in the character mapping, not in the file. +- File: `assets/dbyte_1x.png` (unmodified, CP437). The swapped glyphs ö/ò are corrected in the character mapping, not in the file. ## Made with Claude Code The game was developed with the help of [Claude Code](https://claude.com/claude-code), Anthropic's AI coding assistant. The small pixel-art mark next to the note on the title and login screens is a rendition of the Claude Code logo. Claude, Claude Code and Anthropic are trademarks of Anthropic; the mark only credits the tool and is not covered by the MIT licence of this project. diff --git a/DESIGN.md b/DESIGN.md index d95f301..2816615 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -5,8 +5,8 @@ An original dungeon management game (genre: dungeon building & defence) that run ## Technology - Rust + **macroquad** (WebGL), target `wasm32-unknown-unknown` - User interface drawn entirely **in the canvas** -- Font: **DINOBYTE** bitmap font (CP437, 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled -- All UI windows use **CP437 frames** (single lines for panels, double lines for dialogs); **all bars are CP437 block characters** (█ ▌ ░) +- Font: **Dinowyde** bitmap font (Unicode: Latin, Greek, Cyrillic, symbols; 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled; missing glyphs (arrows, suits, ...) from DINOBYTE; frames: rope (single) and double strand, bars in eighth blocks +- All UI windows use **box-drawing frames** of the font (single lines for panels, double lines for dialogs); **all bars are block characters** (█ ▌ ░) - Text only in **fixed integer sizes**: 1×, 2× and 4× the 6×8 glyph size, times an integer **interface factor** (automatic from the device resolution, or 1×–4× in the settings) - The interface is drawn in **device pixels** (high DPI) and laid out on the **character grid**, so every font pixel has exactly the same size - **3D resolution** setting: the world can be rendered at 1/1 down to 1/16 resolution and scaled up by exactly that factor (pixel look); the setting refers to a window 1080 device pixels high and scales with the window size, so the 3D pixels keep their size relative to the window; the interface stays sharp diff --git a/INSTALL.md b/INSTALL.md index 88d4b24..5674394 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -131,7 +131,8 @@ number by one (a new month starts again at `00001`) and prints the old and the n 1. Copy `lang/en.lang` to `lang/.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are). 2. Add the file to `SOURCES` in `src/i18n.rs`. -3. Note: the DINOBYTE font only covers the CP437 character set. +3. The Dinowyde font covers Latin (all European accents), Greek and Cyrillic; the test + `every_text_of_the_game_has_its_glyphs` in `src/gfx.rs` lists characters it cannot draw (add the new file there). ## Refreshing the generated files diff --git a/README.md b/README.md index 0b9ef24..1bf7cb2 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ marks on the title screen only credit the tools. DungeonCraft is released under the **[MIT License](LICENSE)** - © 2026 Richard Zink. It covers the code, the generated music in `assets/music/` and the procedural graphics. -The **DINOBYTE** font (`assets/dbyte_1x.png`) by [mby](https://mby.itch.io/dinobyte) is licensed separately under +The **Dinowyde** and **DINOBYTE** fonts (`assets/dinowyde_1x.png`, `assets/dbyte_1x.png`) by [mby](https://mby.itch.io/) are licensed separately under **CC BY 4.0**. Details in [CREDITS.md](CREDITS.md). --- diff --git a/assets/dinowyde_wip_characters.png b/assets/dinowyde_1x.png similarity index 100% rename from assets/dinowyde_wip_characters.png rename to assets/dinowyde_1x.png diff --git a/lang/de.lang b/lang/de.lang index b6b1142..3a4b5a7 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -153,7 +153,7 @@ help.s2 = 2. Baue Quartier, Pilzgarten und Schatzkammer. help.s3 = 3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich. help.s4 = 4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an. help.s5 = 5. Setze die Angriffsflagge nahe ans Tor und greife an! -help.font = Schrift: DINOBYTE von mby (CC BY 4.0) +help.font = Schriften: Dinowyde und DINOBYTE von mby (CC BY 4.0) help.start = Klick zum Starten # ---- Spielende diff --git a/lang/en.lang b/lang/en.lang index 4f738c9..097d41d 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -154,7 +154,7 @@ help.s2 = 2. Build a lair, a mushroom garden and a treasury. help.s3 = 3. Train your creatures and pay their wages on time. help.s4 = 4. Fend off the hero waves. They adapt to your army. help.s5 = 5. Place the attack flag near the gate and attack! -help.font = Font: DINOBYTE by mby (CC BY 4.0) +help.font = Fonts: Dinowyde and DINOBYTE by mby (CC BY 4.0) help.start = Click to start # ---- end screen diff --git a/src/gfx.rs b/src/gfx.rs index ec5612c..1b23e2a 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -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 { + 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 { // 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 { + Some(match ch { + '✓' | '✔' => '√', + 'ẞ' => 'ß', + '–' | '—' => '-', + '„' | '“' | '”' => '"', + '‚' | '‘' | '’' => '\'', + '…' => '.', + _ => return None, + }) +} + +// which glyphs the two sheets hold +pub struct FontMap { + has: Vec, // 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 { + 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); + } + } +} diff --git a/src/menu.rs b/src/menu.rs index 59a9049..1bbf983 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,6 +1,6 @@ // Menus: title screen, pause menu, savegame list (server) and the settings dialog // (tabs, values, key bindings). -// Drawn as CP437 windows on top of the (paused) game world. +// Drawn as text-mode windows on top of the (paused) game world. use crate::campaign::{missions, Progress}; use crate::campaign_doc::CampaignDoc; diff --git a/src/render3d.rs b/src/render3d.rs index afa6785..4cb0340 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1536,7 +1536,7 @@ impl R3d { ascii_bar(gfx, x, head.y - GH * small, cols, f, c, small); } if e.guard { - gfx.glyph(0x0F, head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255)); + gfx.glyph('☼', head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255)); } // rank badge left of the health bar if e.level > 1 && px_per_tile >= 20.0 * F() as f32 { @@ -1547,11 +1547,11 @@ impl R3d { if matches!(e.job, Job::Sleep(_)) && !e.moving { let k = (t * 1.5 + e.anim) % 1.0; let rise = (k * 14.0 * F() as f32).floor(); - gfx.glyph(b'z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k)); + gfx.glyph('z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k)); } if e.carry > 0 { let p = proj(vec3(e.disp_x, 0.35, e.disp_y)); - gfx.glyph(0x0F, p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255)); + gfx.glyph('☼', p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255)); } } for tw in &g.towers { diff --git a/src/ui.rs b/src/ui.rs index 43e05f0..1292306 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,4 @@ -// In-canvas user interface: CP437 windows with the DINOBYTE bitmap font. +// In-canvas user interface: text-mode windows with the Dinowyde bitmap font. // All texts come from the language files (see i18n.rs). use crate::game::*; @@ -114,21 +114,22 @@ pub fn begin_ui() { set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() }); } -// bar made of CP437 block characters: full blocks, a half block, shaded rest +// bar made of block characters: full blocks, an eighth block for the rest, shaded background +const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; + pub fn ascii_bar(gfx: &Gfx, x: f32, y: f32, cols: usize, frac: f32, col: Color, scale: f32) { - let f = frac.clamp(0.0, 1.0) * cols as f32; - let full = f.floor() as usize; - let half = f - full as f32 >= 0.5; + let eighths = (frac.clamp(0.0, 1.0) * cols as f32 * 8.0).round() as usize; + let (full, part) = (eighths / 8, eighths % 8); let dim = Color::new(col.r * 0.35, col.g * 0.35, col.b * 0.35, col.a); for k in 0..cols { let px = x + k as f32 * GW * scale; if k < full { - gfx.glyph(0xDB, px, y, scale, col); - } else if k == full && half { - gfx.glyph(0xB0, px, y, scale, dim); - gfx.glyph(0xDD, px, y, scale, col); + gfx.glyph('█', px, y, scale, col); } else { - gfx.glyph(0xB0, px, y, scale, dim); + gfx.glyph('░', px, y, scale, dim); + if k == full && part > 0 { + gfx.glyph(EIGHTHS[part - 1], px, y, scale, col); + } } } } @@ -729,11 +730,11 @@ impl Ui { let locked = !tools.iter().any(|&t| g.tool_allowed(t)); let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered, locked); if has { - gfx.glyph(0x1F, bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI }); + gfx.glyph('▼', bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI }); } if tools.len() > 1 { // small arrow: this button opens a row - gfx.glyph(0x1E, r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM }); + gfx.glyph('▲', r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM }); } self.touch_btns.push((r, TBtn::Cat(c))); } @@ -743,7 +744,7 @@ impl Ui { let hovered = !touch_ui() && r.contains(vec2(mx, my)); draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_overview { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if self.show_overview { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); - gfx.glyph(0x02, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); + gfx.glyph('☻', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); let ol = tr("cat.overview"); let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize; let ol: String = ol.chars().take(maxc).collect(); @@ -755,7 +756,7 @@ impl Ui { let hovered = !touch_ui() && r.contains(vec2(mx, my)); draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0)); - gfx.glyph(0xF0, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); + gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); let ml = tr("cat.menu"); let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize; let ml: String = ml.chars().take(maxc).collect();