From b46f2683ff3f04001a30b0cef5acbbc7a2ee4b84 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:43:40 -0600 Subject: [PATCH] Fonts and text --- Cargo.toml | 2 + .../AtkinsonHyperlegible-Regular.ttf | Bin 0 -> 53504 bytes src/app/mod.rs | 2 +- src/fonts.rs | 23 +++++ src/lib.rs | 26 +----- src/render/buffer.rs | 73 +++++++++++++++ src/render/mod.rs | 2 +- src/render/renderer.rs | 42 +++++++-- src/views/color_rect_view.rs | 7 +- src/views/mod.rs | 5 +- src/views/text_view.rs | 85 ++++++++++++++++++ src/views/vertical_layout.rs | 4 +- 12 files changed, 233 insertions(+), 38 deletions(-) create mode 100644 fonts/Atkinson_Hyperlegible/AtkinsonHyperlegible-Regular.ttf create mode 100644 src/fonts.rs create mode 100644 src/views/text_view.rs diff --git a/Cargo.toml b/Cargo.toml index 0c5b949..dea5b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] +fontdue = "0.9.3" +lazy_static = "1.5.0" wasm-bindgen = {version = "0.2.104"} web-sys = {version = "0.3.81", features = ['CanvasRenderingContext2d', 'Document', 'Element', 'HtmlCanvasElement', 'Window', 'ImageData']} diff --git a/fonts/Atkinson_Hyperlegible/AtkinsonHyperlegible-Regular.ttf b/fonts/Atkinson_Hyperlegible/AtkinsonHyperlegible-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f0edd19047f7272b1abac2169850cb28bec503cd GIT binary patch literal 53504 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 = vec![ + Font::from_bytes( + ATKINSON_HYPERLEGIBLE_REGULAR, + fontdue::FontSettings::default() + ) + .unwrap(), + ]; +} + +#[non_exhaustive] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum FontHandle { + AtiksonHyperlegibleRegular = 0, +} diff --git a/src/lib.rs b/src/lib.rs index afba90c..53e1c82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ // mod activities; mod app; +mod fonts; mod render; mod views; @@ -28,32 +29,9 @@ macro_rules! log { #[wasm_bindgen] pub fn init(canvas: &web_sys::HtmlCanvasElement, width: u32, height: u32) -> App { - let (cwidth, cheight, dist_x, dist_y) = render::calc_resolution(width, height); - canvas.set_width(cwidth); - canvas.set_height(cheight); - log!("WASM Successfully initialized!"); - // set_cursor(Cursor::Auto); - - let mut renderer = Renderer { - ctx: canvas - .get_context("2d") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap(), - img: ImgBuffer::new(width, height), - rand: Rnd::new(9825782), - - canvas_width: cwidth, - canvas_height: cheight, - actual_width: width, - actual_height: height, - - distortion_x: dist_x, - distortion_y: dist_y, - }; + let mut renderer = Renderer::new(canvas, width, height); renderer.resize(width, height); App::new(renderer) diff --git a/src/render/buffer.rs b/src/render/buffer.rs index 436a362..49155ed 100644 --- a/src/render/buffer.rs +++ b/src/render/buffer.rs @@ -29,6 +29,19 @@ impl ImgBuffer { } } + pub fn overlay_bitmap(&mut self, other: &Bitmap, xoffset: usize, yoffset: usize) { + for y in 0..other.height { + for x in 0..other.width { + let offset = (y + yoffset) * self.width as usize + (x + xoffset); + let color = other.data[y * other.width + x]; + self.data[offset * 4] = color; + self.data[offset * 4 + 1] = color; + self.data[offset * 4 + 2] = color; + self.data[offset * 4 + 3] = 255; + } + } + } + pub fn resize(&mut self, width: u32, height: u32) { self.data = vec![0; (width * height * 4) as usize]; self.width = width; @@ -371,3 +384,63 @@ impl ImgBuffer { // // self.data[y*self.width + x] = color; // // } // } + +pub struct Bitmap { + pub data: Vec, + pub width: usize, + pub height: usize, +} + +impl Bitmap { + pub fn new(width: usize, height: usize) -> Self { + Self { + data: vec![0; width * height], + width, + height, + } + } + + pub fn from_data(data: Vec, width: usize, height: usize) -> Self { + // assert!(data.len() == width * height, "Invalid data length!");z + Self { + data, + width, + height, + } + } + + pub fn overlay(&mut self, other: &Bitmap, xoffset: usize, yoffset: usize) { + for y in 0..other.height { + for x in 0..other.width { + self.data[(y + yoffset as usize) * self.width + (x + xoffset as usize)] = + other.data[y * other.width + x]; + } + } + } + + /// Scale using nearest-neighbor (faster but lower quality) + pub fn scale(&self, scale_x: f32, scale_y: f32) -> Bitmap { + let new_width = (self.width as f32 * scale_x).round() as usize; + let new_height = (self.height as f32 * scale_y).round() as usize; + + let mut new_data = vec![0u8; new_width * new_height]; + + for y in 0..new_height { + let src_y = ((y as f32) / scale_y) as usize; + let src_y = src_y.min(self.height - 1); + + for x in 0..new_width { + let src_x = ((x as f32) / scale_x) as usize; + let src_x = src_x.min(self.width - 1); + + new_data[y * new_width + x] = self.data[src_y * self.width + src_x]; + } + } + + Bitmap { + data: new_data, + width: new_width, + height: new_height, + } + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs index 05930dd..79b966a 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -4,7 +4,7 @@ mod renderer; pub use renderer::Renderer; -pub const RESOLUTION: u32 = 1200; +pub const RESOLUTION: u32 = 2000; pub fn calc_resolution(width: u32, height: u32) -> (u32, u32, f32, f32) { let aspect = width as f32 / height as f32; diff --git a/src/render/renderer.rs b/src/render/renderer.rs index 5f8bb0a..4de4013 100644 --- a/src/render/renderer.rs +++ b/src/render/renderer.rs @@ -1,7 +1,13 @@ +use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}; use wasm_bindgen::{Clamped, prelude::*}; use web_sys::ImageData; -use crate::render::{RESOLUTION, buffer::ImgBuffer, calc_resolution, rand::Rnd}; +use crate::render::{ + RESOLUTION, + buffer::{Bitmap, ImgBuffer}, + calc_resolution, + rand::Rnd, +}; #[wasm_bindgen] pub struct Renderer { @@ -16,6 +22,8 @@ pub struct Renderer { pub distortion_x: f32, pub distortion_y: f32, + pub ratio_x: f32, + pub ratio_y: f32, } impl Renderer { @@ -42,6 +50,8 @@ impl Renderer { actual_height: height, distortion_x: dist_x, distortion_y: dist_y, + ratio_x: cwidth as f32 / width as f32, + ratio_y: cheight as f32 / height as f32, } } @@ -55,6 +65,9 @@ impl Renderer { self.distortion_x = dist_x; self.distortion_y = dist_y; + self.ratio_x = cwidth as f32 / width as f32; + self.ratio_y = cheight as f32 / height as f32; + self.img.resize(cwidth, cheight); self.ctx.canvas().unwrap().set_width(cwidth); self.ctx.canvas().unwrap().set_height(cheight); @@ -77,10 +90,7 @@ impl Renderer { impl Renderer { pub fn undistort(&self, x: f32, y: f32) -> (f32, f32) { - ( - x * (self.canvas_width as f32 / self.actual_width as f32), - y * (self.canvas_height as f32 / self.actual_height as f32), - ) + (x * self.ratio_x, y * self.ratio_y) } /// Draw a rectangle centered at (cx, cy) with the given width and height. @@ -159,3 +169,25 @@ impl Renderer { } } } + +// // Fonts +// impl Renderer { +// pub fn rasterize_font( +// &mut self, +// x: i32, +// y: i32, +// text: &str, +// font_scale: f32, +// font: FontHandle, +// ) { +// } + +// pub fn measure_text_bounds( +// &mut self, +// text: &str, +// font_scale: i32, +// font: FontHandle, +// ) -> (i32, i32) { +// (100, 100) +// } +// } diff --git a/src/views/color_rect_view.rs b/src/views/color_rect_view.rs index a04e39e..2160b22 100644 --- a/src/views/color_rect_view.rs +++ b/src/views/color_rect_view.rs @@ -1,5 +1,4 @@ use crate::{ - log, render::Renderer, views::{Bounds, View}, }; @@ -15,11 +14,11 @@ impl ColorRectView { } impl View for ColorRectView { - fn draw(&self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32) { + fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32) { renderer.rect_xywh(x as i32, y as i32, w as i32, h as i32, self.color); - log!("Draw"); + // log!("Draw"); } fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) { - (Bounds::MatchParent, Bounds::Pixels(2200.)) + (Bounds::MatchParent, Bounds::Pixels(200.)) } } diff --git a/src/views/mod.rs b/src/views/mod.rs index a591b08..fdd06c2 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -1,13 +1,15 @@ use crate::render::Renderer; mod color_rect_view; +mod text_view; mod vertical_layout; use color_rect_view::ColorRectView; +use text_view::TextView; use vertical_layout::VerticalLayout; pub trait View { - fn draw(&self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32); + fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32); fn bounds(&self, pw: f32, ph: f32) -> (Bounds, Bounds); } @@ -23,6 +25,7 @@ pub enum Bounds { pub fn default_view() -> Box { Box::new(VerticalLayout::new(vec![ Box::new(ColorRectView::new(12, 34, 56)), + Box::new(TextView::new("Testing!\n12345".to_string())), Box::new(ColorRectView::new(20, 60, 80)), ])) } diff --git a/src/views/text_view.rs b/src/views/text_view.rs new file mode 100644 index 0000000..6660512 --- /dev/null +++ b/src/views/text_view.rs @@ -0,0 +1,85 @@ +use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}; + +use crate::{ + fonts::{FONTS, FontHandle}, + log, + render::{Renderer, buffer::Bitmap}, + views::{Bounds, View}, +}; + +pub struct TextView { + layout: Layout, + text: String, + scale: f32, + font: FontHandle, +} + +impl TextView { + pub fn new(text: String) -> Self { + let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + + layout.reset(&LayoutSettings { + ..LayoutSettings::default() + }); + + Self { + layout, + text, + scale: 20., + font: FontHandle::AtiksonHyperlegibleRegular, + } + } +} + +impl View for TextView { + fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32) { + // renderer.rasterize_font( + // x as i32, + // y as i32, + // &self.text, + // 20., + // FontHandle::AtiksonHyperlegibleRegular, + // ); + // renderer. + + // renderer.rect_xywh(x as i32, y as i32, w as i32, h as i32, self.color); + + let (x, y) = renderer.undistort(x as f32, y as f32); + + let font = (self.font as usize).clone(); + + self.layout.clear(); + + self.layout + .append(&FONTS, &TextStyle::new(&self.text, self.scale, font)); + + let (mut width, mut height): (usize, usize) = (0, 0); + for glyph in self.layout.glyphs() { + width = width.max(glyph.x as usize + glyph.width); + height = height.max(glyph.y as usize + glyph.height); + } + + let x_padding = 0.; + let mut new_bitmap = Bitmap::new(width + 2 * (self.scale * x_padding) as usize, height); + + for glyph in self.layout.glyphs() { + let font = &FONTS[glyph.font_index]; + let (_, char_bitmap) = font.rasterize_config(glyph.key); + + new_bitmap.overlay( + &Bitmap::from_data(char_bitmap, glyph.width, glyph.height), + glyph.x as usize, + glyph.y as usize, + ); + } + + let scaled = new_bitmap.scale(renderer.ratio_x, renderer.ratio_y); + + renderer.img.overlay_bitmap(&scaled, x as usize, y as usize); + + // new_bitmap + } + fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) { + (Bounds::MatchParent, Bounds::Pixels(200.)) + } +} diff --git a/src/views/vertical_layout.rs b/src/views/vertical_layout.rs index da42031..8b667db 100644 --- a/src/views/vertical_layout.rs +++ b/src/views/vertical_layout.rs @@ -11,10 +11,10 @@ impl VerticalLayout { } impl View for VerticalLayout { - fn draw(&self, renderer: &mut crate::render::Renderer, x: f32, y: f32, w: f32, h: f32) { + fn draw(&mut self, renderer: &mut crate::render::Renderer, x: f32, y: f32, w: f32, h: f32) { let mut cur_y = y; - for view in &self.views { + for view in &mut self.views { let (vx, vy) = view.bounds(w, h); let vx = match vx {