mirror of
https://github.com/Astatin3/IntroToWebAuthoring.git
synced 2026-06-08 16:18:01 -06:00
Fonts and text
This commit is contained in:
@@ -7,6 +7,8 @@ edition = "2024"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
fontdue = "0.9.3"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
wasm-bindgen = {version = "0.2.104"}
|
wasm-bindgen = {version = "0.2.104"}
|
||||||
web-sys = {version = "0.3.81", features = ['CanvasRenderingContext2d', 'Document', 'Element', 'HtmlCanvasElement', 'Window', 'ImageData']}
|
web-sys = {version = "0.3.81", features = ['CanvasRenderingContext2d', 'Document', 'Element', 'HtmlCanvasElement', 'Window', 'ImageData']}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+1
-1
@@ -54,7 +54,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw(&mut self) {
|
||||||
if let Some(view) = &self.root_view {
|
if let Some(view) = &mut self.root_view {
|
||||||
let (width, height) = (
|
let (width, height) = (
|
||||||
self.renderer.actual_width.clone() as f32,
|
self.renderer.actual_width.clone() as f32,
|
||||||
self.renderer.actual_height.clone() as f32,
|
self.renderer.actual_height.clone() as f32,
|
||||||
|
|||||||
@@ -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
@@ -1,5 +1,6 @@
|
|||||||
// mod activities;
|
// mod activities;
|
||||||
mod app;
|
mod app;
|
||||||
|
mod fonts;
|
||||||
mod render;
|
mod render;
|
||||||
mod views;
|
mod views;
|
||||||
|
|
||||||
@@ -28,32 +29,9 @@ macro_rules! log {
|
|||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn init(canvas: &web_sys::HtmlCanvasElement, width: u32, height: u32) -> App {
|
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!");
|
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);
|
renderer.resize(width, height);
|
||||||
|
|
||||||
App::new(renderer)
|
App::new(renderer)
|
||||||
|
|||||||
@@ -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) {
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
self.data = vec![0; (width * height * 4) as usize];
|
self.data = vec![0; (width * height * 4) as usize];
|
||||||
self.width = width;
|
self.width = width;
|
||||||
@@ -371,3 +384,63 @@ impl ImgBuffer {
|
|||||||
// // self.data[y*self.width + x] = color;
|
// // 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
@@ -4,7 +4,7 @@ mod renderer;
|
|||||||
|
|
||||||
pub use renderer::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) {
|
pub fn calc_resolution(width: u32, height: u32) -> (u32, u32, f32, f32) {
|
||||||
let aspect = width as f32 / height as f32;
|
let aspect = width as f32 / height as f32;
|
||||||
|
|||||||
+37
-5
@@ -1,7 +1,13 @@
|
|||||||
|
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
|
||||||
use wasm_bindgen::{Clamped, prelude::*};
|
use wasm_bindgen::{Clamped, prelude::*};
|
||||||
use web_sys::ImageData;
|
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]
|
#[wasm_bindgen]
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -16,6 +22,8 @@ pub struct Renderer {
|
|||||||
|
|
||||||
pub distortion_x: f32,
|
pub distortion_x: f32,
|
||||||
pub distortion_y: f32,
|
pub distortion_y: f32,
|
||||||
|
pub ratio_x: f32,
|
||||||
|
pub ratio_y: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
@@ -42,6 +50,8 @@ impl Renderer {
|
|||||||
actual_height: height,
|
actual_height: height,
|
||||||
distortion_x: dist_x,
|
distortion_x: dist_x,
|
||||||
distortion_y: dist_y,
|
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_x = dist_x;
|
||||||
self.distortion_y = dist_y;
|
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.img.resize(cwidth, cheight);
|
||||||
self.ctx.canvas().unwrap().set_width(cwidth);
|
self.ctx.canvas().unwrap().set_width(cwidth);
|
||||||
self.ctx.canvas().unwrap().set_height(cheight);
|
self.ctx.canvas().unwrap().set_height(cheight);
|
||||||
@@ -77,10 +90,7 @@ impl Renderer {
|
|||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
pub fn undistort(&self, x: f32, y: f32) -> (f32, f32) {
|
pub fn undistort(&self, x: f32, y: f32) -> (f32, f32) {
|
||||||
(
|
(x * self.ratio_x, y * self.ratio_y)
|
||||||
x * (self.canvas_width as f32 / self.actual_width as f32),
|
|
||||||
y * (self.canvas_height as f32 / self.actual_height as f32),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a rectangle centered at (cx, cy) with the given width and height.
|
/// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
log,
|
|
||||||
render::Renderer,
|
render::Renderer,
|
||||||
views::{Bounds, View},
|
views::{Bounds, View},
|
||||||
};
|
};
|
||||||
@@ -15,11 +14,11 @@ impl ColorRectView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl View for 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);
|
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) {
|
fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) {
|
||||||
(Bounds::MatchParent, Bounds::Pixels(2200.))
|
(Bounds::MatchParent, Bounds::Pixels(200.))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,13 +1,15 @@
|
|||||||
use crate::render::Renderer;
|
use crate::render::Renderer;
|
||||||
|
|
||||||
mod color_rect_view;
|
mod color_rect_view;
|
||||||
|
mod text_view;
|
||||||
mod vertical_layout;
|
mod vertical_layout;
|
||||||
|
|
||||||
use color_rect_view::ColorRectView;
|
use color_rect_view::ColorRectView;
|
||||||
|
use text_view::TextView;
|
||||||
use vertical_layout::VerticalLayout;
|
use vertical_layout::VerticalLayout;
|
||||||
|
|
||||||
pub trait View {
|
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);
|
fn bounds(&self, pw: f32, ph: f32) -> (Bounds, Bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ pub enum Bounds {
|
|||||||
pub fn default_view() -> Box<dyn View> {
|
pub fn default_view() -> Box<dyn View> {
|
||||||
Box::new(VerticalLayout::new(vec![
|
Box::new(VerticalLayout::new(vec![
|
||||||
Box::new(ColorRectView::new(12, 34, 56)),
|
Box::new(ColorRectView::new(12, 34, 56)),
|
||||||
|
Box::new(TextView::new("Testing!\n12345".to_string())),
|
||||||
Box::new(ColorRectView::new(20, 60, 80)),
|
Box::new(ColorRectView::new(20, 60, 80)),
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ impl VerticalLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl View for 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;
|
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, vy) = view.bounds(w, h);
|
||||||
|
|
||||||
let vx = match vx {
|
let vx = match vx {
|
||||||
|
|||||||
Reference in New Issue
Block a user