Fonts and text

This commit is contained in:
Michael Mikovsky
2025-10-27 10:43:40 -06:00
parent d83e81fabc
commit b46f2683ff
12 changed files with 233 additions and 38 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ impl App {
}
pub fn draw(&mut self) {
if let Some(view) = &self.root_view {
if let Some(view) = &mut self.root_view {
let (width, height) = (
self.renderer.actual_width.clone() as f32,
self.renderer.actual_height.clone() as f32,
+23
View File
@@ -0,0 +1,23 @@
use fontdue::Font;
use lazy_static::lazy_static;
// Include the fonts in the binary
pub static ATKINSON_HYPERLEGIBLE_REGULAR: &'static [u8] =
include_bytes!("../fonts/Atkinson_Hyperlegible/AtkinsonHyperlegible-Regular.ttf");
// Parse the fonts at runtime
lazy_static! {
pub static ref FONTS: Vec<Font> = 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,
}
+2 -24
View File
@@ -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::<web_sys::CanvasRenderingContext2d>()
.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)
+73
View File
@@ -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<u8>,
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<u8>, 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,
}
}
}
+1 -1
View File
@@ -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;
+37 -5
View File
@@ -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)
// }
// }
+3 -4
View File
@@ -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.))
}
}
+4 -1
View File
@@ -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<dyn View> {
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)),
]))
}
+85
View File
@@ -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.))
}
}
+2 -2
View File
@@ -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 {