pub const MAX_FONT_SCALE_KNOTS: usize = 12;
const COLLINEAR_EPSILON_DP: f32 = 1.0e-3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FontScaleCurve {
scale: f32,
fingerprint: u32,
knots: &'static [(f32, f32)],
}
impl Default for FontScaleCurve {
fn default() -> Self {
Self::linear(1.0)
}
}
impl FontScaleCurve {
pub const fn linear(scale: f32) -> Self {
Self {
scale,
fingerprint: 0,
knots: &[],
}
}
pub fn from_samples(scale: f32, samples: &[(f32, f32)]) -> Self {
let Some(kept) = compress(samples) else {
return Self::linear(scale);
};
if kept
.iter()
.all(|(sp, dp)| (dp - sp * scale).abs() <= COLLINEAR_EPSILON_DP)
{
return Self::linear(scale);
}
Self {
scale,
fingerprint: fingerprint(scale, &kept),
knots: shared_knots(kept),
}
}
pub fn scale(self) -> f32 {
self.scale
}
pub fn is_linear(self) -> bool {
self.knots.is_empty()
}
pub fn is_identity(self) -> bool {
if self.knots.is_empty() {
return (self.scale - 1.0).abs() <= f32::EPSILON;
}
self.knots
.iter()
.all(|(sp, dp)| (dp - sp).abs() <= COLLINEAR_EPSILON_DP)
}
pub fn knots(self) -> &'static [(f32, f32)] {
self.knots
}
pub fn knot_count(self) -> usize {
self.knots.len()
}
pub fn fingerprint(self) -> u32 {
self.fingerprint ^ self.scale.to_bits()
}
pub fn sp_to_dp(self, sp: f32) -> f32 {
if !sp.is_finite() {
return sp;
}
let knots = self.knots;
let (Some(&(first_sp, first_dp)), Some(&(last_sp, last_dp))) =
(knots.first(), knots.last())
else {
return sp * self.scale;
};
let magnitude = sp.abs();
let sign = if sp.is_sign_negative() { -1.0 } else { 1.0 };
if magnitude <= first_sp {
return sign * magnitude * (first_dp / first_sp);
}
if magnitude >= last_sp {
return sign * magnitude * (last_dp / last_sp);
}
for window in knots.windows(2) {
let (low_sp, low_dp) = window[0];
let (high_sp, high_dp) = window[1];
if magnitude <= high_sp {
let t = (magnitude - low_sp) / (high_sp - low_sp);
return sign * (low_dp + (high_dp - low_dp) * t);
}
}
sign * magnitude * (last_dp / last_sp)
}
}
fn compress(samples: &[(f32, f32)]) -> Option<Vec<(f32, f32)>> {
if samples.len() < 2 {
return None;
}
let mut previous_sp = 0.0f32;
for (sp, dp) in samples {
if !sp.is_finite() || !dp.is_finite() || *sp <= previous_sp || *dp <= 0.0 {
return None;
}
previous_sp = *sp;
}
let mut kept: Vec<(f32, f32)> = Vec::with_capacity(samples.len());
kept.push(samples[0]);
for index in 1..samples.len() - 1 {
let (low_sp, low_dp) = *kept.last().expect("the first sample was pushed");
let (sp, dp) = samples[index];
let (high_sp, high_dp) = samples[index + 1];
let t = (sp - low_sp) / (high_sp - low_sp);
let straight = low_dp + (high_dp - low_dp) * t;
if (dp - straight).abs() > COLLINEAR_EPSILON_DP {
kept.push(samples[index]);
}
}
kept.push(samples[samples.len() - 1]);
if kept.len() > MAX_FONT_SCALE_KNOTS {
return None;
}
Some(kept)
}
fn shared_knots(knots: Vec<(f32, f32)>) -> &'static [(f32, f32)] {
static TABLES: std::sync::Mutex<Vec<&'static [(f32, f32)]>> = std::sync::Mutex::new(Vec::new());
let mut tables = TABLES
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(table) = tables.iter().find(|table| ***table == knots[..]) {
return table;
}
let table: &'static [(f32, f32)] = Box::leak(knots.into_boxed_slice());
tables.push(table);
table
}
fn fingerprint(scale: f32, knots: &[(f32, f32)]) -> u32 {
let mut hash = 2166136261u32;
let mut mix = |bits: u32| {
hash ^= bits;
hash = hash.wrapping_mul(16777619);
};
mix(scale.to_bits());
for (sp, dp) in knots {
mix(sp.to_bits());
mix(dp.to_bits());
}
hash
}
#[cfg(test)]
#[path = "tests/font_scale_tests.rs"]
mod tests;