diff --git a/CHANGELOG.md b/CHANGELOG.md index b23533a9..417d09c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial unicode support for font rendering. +- Kerning support for font rendering. ### Fixed diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index c63e09eb..73a99a50 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -3,6 +3,11 @@ use quote::quote; use proc_macro2::TokenStream; +struct KerningData { + previous_character: char, + amount: f32, +} + struct LetterData { character: char, width: usize, @@ -11,6 +16,7 @@ struct LetterData { ymin: i32, advance_width: f32, rendered: Vec, + kerning_data: Vec, } pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { @@ -31,8 +37,8 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let mut letters: Vec<_> = font .chars() .iter() - .map(|(&c, _)| (c, font.rasterize(c, pixels_per_em))) - .map(|(c, (metrics, bitmap))| { + .map(|(&c, &index)| (c, index, font.rasterize(c, pixels_per_em))) + .map(|(c, index, (metrics, bitmap))| { let width = metrics.width; let height = metrics.height; @@ -50,6 +56,25 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { }) .collect(); + let mut kerning_data: Vec<_> = font + .chars() + .iter() + .filter_map(|(&left_char, &left_index)| { + let kerning = font.horizontal_kern_indexed( + left_index.into(), + index.into(), + pixels_per_em, + )?; + + Some(KerningData { + previous_character: left_char, + amount: kerning, + }) + }) + .collect(); + + kerning_data.sort_unstable_by_key(|kd| kd.previous_character); + LetterData { character: c, width, @@ -58,6 +83,7 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { xmin: metrics.xmin, ymin: metrics.ymin, advance_width: metrics.advance_width, + kerning_data, } }) .collect(); @@ -82,6 +108,13 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let xmin = letter_data.xmin as i8; let ymin = letter_data.ymin as i8; let advance_width = letter_data.advance_width.ceil() as u8; + let kerning_amounts = letter_data.kerning_data.iter().map(|kerning_data| { + let amount = kerning_data.amount as i8; + let c = kerning_data.previous_character; + quote! { + (#c, #amount) + } + }); quote!( display::FontLetter::new( @@ -92,6 +125,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { #xmin, #ymin, #advance_width, + &[ + #(#kerning_amounts),* + ] ) ) }); diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index d725e4b1..ae3bde95 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -17,10 +17,12 @@ pub struct FontLetter { pub(crate) xmin: i8, pub(crate) ymin: i8, pub(crate) advance_width: u8, + kerning_amounts: &'static [(char, i8)], } impl FontLetter { #[must_use] + #[allow(clippy::too_many_arguments)] // only used in macro pub const fn new( character: char, width: u8, @@ -29,6 +31,7 @@ impl FontLetter { xmin: i8, ymin: i8, advance_width: u8, + kerning_amounts: &'static [(char, i8)], ) -> Self { Self { character, @@ -38,6 +41,7 @@ impl FontLetter { xmin, ymin, advance_width, + kerning_amounts, } } @@ -47,6 +51,17 @@ impl FontLetter { let bit = position % 8; ((byte >> bit) & 1) != 0 } + + pub(crate) fn kerning_amount(&self, previous_char: char) -> i32 { + if let Ok(index) = self + .kerning_amounts + .binary_search_by_key(&previous_char, |kerning_data| kerning_data.0) + { + self.kerning_amounts[index].1 as i32 + } else { + 0 + } + } } pub struct Font { @@ -92,6 +107,7 @@ impl Font { TextRenderer { current_x_pos: 0, current_y_pos: 0, + previous_character: None, font: self, tile_pos: tile_pos.into(), tiles: Default::default(), @@ -103,6 +119,7 @@ impl Font { pub struct TextRenderer<'a> { current_x_pos: i32, current_y_pos: i32, + previous_character: Option, font: &'a Font, tile_pos: Vector2D, tiles: HashMap<(i32, i32), DynamicTile<'a>>, @@ -258,6 +275,12 @@ impl<'a, 'b> TextRenderer<'b> { self.current_x_pos = 0; } else { let letter = self.font.letter(c); + + if let Some(previous_character) = self.previous_character { + self.current_x_pos += letter.kerning_amount(previous_character); + } + self.previous_character = Some(c); + self.render_letter(letter, vram_manager, foreground_colour, background_colour); self.current_x_pos += i32::from(letter.advance_width); } diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index f899071f..e07356a2 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -44,6 +44,7 @@ pub(crate) struct Preprocessed { #[derive(Debug, Default)] struct Preprocessor { + previous_character: Option, width_in_sprite: i32, } @@ -72,6 +73,10 @@ impl Preprocessor { } letter => { let letter = font.letter(letter); + if let Some(previous_character) = self.previous_character { + self.width_in_sprite += letter.kerning_amount(previous_character); + } + if self.width_in_sprite + letter.width as i32 > sprite_width { widths.push_back( PreprocessedElement::LetterGroup { @@ -87,6 +92,8 @@ impl Preprocessor { self.width_in_sprite += letter.advance_width as i32; } } + + self.previous_character = Some(character); } } diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index b59b6549..bbd0628b 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -43,6 +43,8 @@ pub(crate) struct WordRender { working: WorkingLetter, config: Configuration, colour: usize, + + previous_character: Option, } impl WordRender { @@ -56,6 +58,7 @@ impl WordRender { working: WorkingLetter::new(config.sprite_size), config, colour: 1, + previous_character: None, } } @@ -82,6 +85,11 @@ impl WordRender { let font_letter: &crate::display::FontLetter = font.letter(c); + if let Some(previous_character) = self.previous_character { + self.working.x_offset += font_letter.kerning_amount(previous_character); + } + self.previous_character = Some(c); + // uses more than the sprite can hold let group = if self.working.x_offset + font_letter.width as i32 > self.config.sprite_size.to_width_height().0 as i32