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
- Initial unicode support for font rendering.
- Kerning support for font rendering.
### Fixed

View file

@ -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<u8>,
kerning_data: Vec<KerningData>,
}
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),*
]
)
)
});

View file

@ -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<char>,
font: &'a Font,
tile_pos: Vector2D<u16>,
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);
}

View file

@ -44,6 +44,7 @@ pub(crate) struct Preprocessed {
#[derive(Debug, Default)]
struct Preprocessor {
previous_character: Option<char>,
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);
}
}

View file

@ -43,6 +43,8 @@ pub(crate) struct WordRender {
working: WorkingLetter,
config: Configuration,
colour: usize,
previous_character: Option<char>,
}
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