Kerning support (#588)

Some fonts look a bit weird if you don't do kerning.

@corwinkuiper can you check if I've done the correct thing for object
font rendering? I'm not entirely sure... Although it does render
correctly in my tests :D

- [x] Changelog updated / no changelog update needed
This commit is contained in:
Gwilym Inzani 2024-03-29 15:17:18 +00:00 committed by GitHub
commit 6fdd961b61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 2 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Initial unicode support for font rendering. - Initial unicode support for font rendering.
- Kerning support for font rendering.
### Fixed ### Fixed

View file

@ -3,6 +3,11 @@ use quote::quote;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
struct KerningData {
previous_character: char,
amount: f32,
}
struct LetterData { struct LetterData {
character: char, character: char,
width: usize, width: usize,
@ -11,6 +16,7 @@ struct LetterData {
ymin: i32, ymin: i32,
advance_width: f32, advance_width: f32,
rendered: Vec<u8>, rendered: Vec<u8>,
kerning_data: Vec<KerningData>,
} }
pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { 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 let mut letters: Vec<_> = font
.chars() .chars()
.iter() .iter()
.map(|(&c, _)| (c, font.rasterize(c, pixels_per_em))) .map(|(&c, &index)| (c, index, font.rasterize(c, pixels_per_em)))
.map(|(c, (metrics, bitmap))| { .map(|(c, index, (metrics, bitmap))| {
let width = metrics.width; let width = metrics.width;
let height = metrics.height; let height = metrics.height;
@ -50,6 +56,25 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
}) })
.collect(); .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 { LetterData {
character: c, character: c,
width, width,
@ -58,6 +83,7 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
xmin: metrics.xmin, xmin: metrics.xmin,
ymin: metrics.ymin, ymin: metrics.ymin,
advance_width: metrics.advance_width, advance_width: metrics.advance_width,
kerning_data,
} }
}) })
.collect(); .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 xmin = letter_data.xmin as i8;
let ymin = letter_data.ymin as i8; let ymin = letter_data.ymin as i8;
let advance_width = letter_data.advance_width.ceil() as u8; 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!( quote!(
display::FontLetter::new( display::FontLetter::new(
@ -92,6 +125,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
#xmin, #xmin,
#ymin, #ymin,
#advance_width, #advance_width,
&[
#(#kerning_amounts),*
]
) )
) )
}); });

View file

@ -17,10 +17,12 @@ pub struct FontLetter {
pub(crate) xmin: i8, pub(crate) xmin: i8,
pub(crate) ymin: i8, pub(crate) ymin: i8,
pub(crate) advance_width: u8, pub(crate) advance_width: u8,
kerning_amounts: &'static [(char, i8)],
} }
impl FontLetter { impl FontLetter {
#[must_use] #[must_use]
#[allow(clippy::too_many_arguments)] // only used in macro
pub const fn new( pub const fn new(
character: char, character: char,
width: u8, width: u8,
@ -29,6 +31,7 @@ impl FontLetter {
xmin: i8, xmin: i8,
ymin: i8, ymin: i8,
advance_width: u8, advance_width: u8,
kerning_amounts: &'static [(char, i8)],
) -> Self { ) -> Self {
Self { Self {
character, character,
@ -38,6 +41,7 @@ impl FontLetter {
xmin, xmin,
ymin, ymin,
advance_width, advance_width,
kerning_amounts,
} }
} }
@ -47,6 +51,17 @@ impl FontLetter {
let bit = position % 8; let bit = position % 8;
((byte >> bit) & 1) != 0 ((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 { pub struct Font {
@ -92,6 +107,7 @@ impl Font {
TextRenderer { TextRenderer {
current_x_pos: 0, current_x_pos: 0,
current_y_pos: 0, current_y_pos: 0,
previous_character: None,
font: self, font: self,
tile_pos: tile_pos.into(), tile_pos: tile_pos.into(),
tiles: Default::default(), tiles: Default::default(),
@ -103,6 +119,7 @@ impl Font {
pub struct TextRenderer<'a> { pub struct TextRenderer<'a> {
current_x_pos: i32, current_x_pos: i32,
current_y_pos: i32, current_y_pos: i32,
previous_character: Option<char>,
font: &'a Font, font: &'a Font,
tile_pos: Vector2D<u16>, tile_pos: Vector2D<u16>,
tiles: HashMap<(i32, i32), DynamicTile<'a>>, tiles: HashMap<(i32, i32), DynamicTile<'a>>,
@ -258,6 +275,12 @@ impl<'a, 'b> TextRenderer<'b> {
self.current_x_pos = 0; self.current_x_pos = 0;
} else { } else {
let letter = self.font.letter(c); 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.render_letter(letter, vram_manager, foreground_colour, background_colour);
self.current_x_pos += i32::from(letter.advance_width); self.current_x_pos += i32::from(letter.advance_width);
} }

View file

@ -44,6 +44,7 @@ pub(crate) struct Preprocessed {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Preprocessor { struct Preprocessor {
previous_character: Option<char>,
width_in_sprite: i32, width_in_sprite: i32,
} }
@ -72,6 +73,10 @@ impl Preprocessor {
} }
letter => { letter => {
let letter = font.letter(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 { if self.width_in_sprite + letter.width as i32 > sprite_width {
widths.push_back( widths.push_back(
PreprocessedElement::LetterGroup { PreprocessedElement::LetterGroup {
@ -87,6 +92,8 @@ impl Preprocessor {
self.width_in_sprite += letter.advance_width as i32; self.width_in_sprite += letter.advance_width as i32;
} }
} }
self.previous_character = Some(character);
} }
} }

View file

@ -43,6 +43,8 @@ pub(crate) struct WordRender {
working: WorkingLetter, working: WorkingLetter,
config: Configuration, config: Configuration,
colour: usize, colour: usize,
previous_character: Option<char>,
} }
impl WordRender { impl WordRender {
@ -56,6 +58,7 @@ impl WordRender {
working: WorkingLetter::new(config.sprite_size), working: WorkingLetter::new(config.sprite_size),
config, config,
colour: 1, colour: 1,
previous_character: None,
} }
} }
@ -82,6 +85,11 @@ impl WordRender {
let font_letter: &crate::display::FontLetter = font.letter(c); 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 // uses more than the sprite can hold
let group = if self.working.x_offset + font_letter.width as i32 let group = if self.working.x_offset + font_letter.width as i32
> self.config.sprite_size.to_width_height().0 as i32 > self.config.sprite_size.to_width_height().0 as i32