1pub const MAX_FONT_SCALE_KNOTS: usize = 12;
41
42const COLLINEAR_EPSILON_DP: f32 = 1.0e-3;
43
44#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct FontScaleCurve {
53 scale: f32,
54 knots: [(f32, f32); MAX_FONT_SCALE_KNOTS],
55 len: usize,
56 fingerprint: u32,
57}
58
59impl Default for FontScaleCurve {
60 fn default() -> Self {
61 Self::linear(1.0)
62 }
63}
64
65impl FontScaleCurve {
66 pub const fn linear(scale: f32) -> Self {
69 Self {
70 scale,
71 knots: [(0.0, 0.0); MAX_FONT_SCALE_KNOTS],
72 len: 0,
73 fingerprint: 0,
74 }
75 }
76
77 pub fn from_samples(scale: f32, samples: &[(f32, f32)]) -> Self {
87 let Some(kept) = compress(samples) else {
88 return Self::linear(scale);
89 };
90 let mut curve = Self::linear(scale);
91 for (index, knot) in kept.iter().enumerate() {
92 curve.knots[index] = *knot;
93 }
94 curve.len = kept.len();
95 curve.fingerprint = fingerprint(scale, &kept);
96 curve
97 }
98
99 pub fn scale(self) -> f32 {
103 self.scale
104 }
105
106 pub fn is_linear(self) -> bool {
108 self.len == 0
109 }
110
111 pub fn is_identity(self) -> bool {
117 if self.len == 0 {
118 return (self.scale - 1.0).abs() <= f32::EPSILON;
119 }
120 self.knots[..self.len]
121 .iter()
122 .all(|(sp, dp)| (dp - sp).abs() <= COLLINEAR_EPSILON_DP)
123 }
124
125 pub fn knots(self) -> [(f32, f32); MAX_FONT_SCALE_KNOTS] {
127 self.knots
128 }
129
130 pub fn knot_count(self) -> usize {
132 self.len
133 }
134
135 pub fn fingerprint(self) -> u32 {
138 self.fingerprint ^ self.scale.to_bits()
139 }
140
141 pub fn sp_to_dp(self, sp: f32) -> f32 {
148 if !sp.is_finite() {
149 return sp;
150 }
151 if self.len == 0 {
152 return sp * self.scale;
153 }
154 let magnitude = sp.abs();
155 let sign = if sp.is_sign_negative() { -1.0 } else { 1.0 };
156 let knots = &self.knots[..self.len];
157 let (first_sp, first_dp) = knots[0];
158 if magnitude <= first_sp {
159 return sign * magnitude * (first_dp / first_sp);
160 }
161 let (last_sp, last_dp) = knots[self.len - 1];
162 if magnitude >= last_sp {
163 return sign * magnitude * (last_dp / last_sp);
164 }
165 for window in knots.windows(2) {
166 let (low_sp, low_dp) = window[0];
167 let (high_sp, high_dp) = window[1];
168 if magnitude <= high_sp {
169 let t = (magnitude - low_sp) / (high_sp - low_sp);
170 return sign * (low_dp + (high_dp - low_dp) * t);
171 }
172 }
173 sign * magnitude * (last_dp / last_sp)
174 }
175}
176
177fn compress(samples: &[(f32, f32)]) -> Option<Vec<(f32, f32)>> {
178 if samples.len() < 2 {
179 return None;
180 }
181 let mut previous_sp = 0.0f32;
182 for (sp, dp) in samples {
183 if !sp.is_finite() || !dp.is_finite() || *sp <= previous_sp || *dp <= 0.0 {
184 return None;
185 }
186 previous_sp = *sp;
187 }
188 let mut kept: Vec<(f32, f32)> = Vec::with_capacity(samples.len());
189 kept.push(samples[0]);
190 for index in 1..samples.len() - 1 {
191 let (low_sp, low_dp) = *kept.last().expect("the first sample was pushed");
192 let (sp, dp) = samples[index];
193 let (high_sp, high_dp) = samples[index + 1];
194 let t = (sp - low_sp) / (high_sp - low_sp);
195 let straight = low_dp + (high_dp - low_dp) * t;
196 if (dp - straight).abs() > COLLINEAR_EPSILON_DP {
197 kept.push(samples[index]);
198 }
199 }
200 kept.push(samples[samples.len() - 1]);
201 if kept.len() > MAX_FONT_SCALE_KNOTS {
202 return None;
203 }
204 Some(kept)
205}
206
207fn fingerprint(scale: f32, knots: &[(f32, f32)]) -> u32 {
208 let mut hash = 2166136261u32;
209 let mut mix = |bits: u32| {
210 hash ^= bits;
211 hash = hash.wrapping_mul(16777619);
212 };
213 mix(scale.to_bits());
214 for (sp, dp) in knots {
215 mix(sp.to_bits());
216 mix(dp.to_bits());
217 }
218 hash
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 const PLATFORM_124: [(f32, f32); 10] = [
226 (8.0, 9.92),
227 (10.0, 12.4),
228 (12.0, 14.88),
229 (14.0, 17.84),
230 (16.0, 19.36),
231 (18.0, 20.88),
232 (20.0, 22.88),
233 (24.0, 25.92),
234 (30.0, 30.0),
235 (100.0, 100.0),
236 ];
237
238 fn platform_124() -> FontScaleCurve {
239 FontScaleCurve::from_samples(1.24, &PLATFORM_124)
240 }
241
242 #[test]
243 fn a_curve_with_no_knots_multiplies() {
244 let curve = FontScaleCurve::linear(1.24);
245 assert!(curve.is_linear());
246 assert_eq!(curve.sp_to_dp(13.0), 13.0 * 1.24);
247 assert_eq!(curve.sp_to_dp(0.4), 0.4 * 1.24);
248 assert_eq!(curve.scale(), 1.24);
249 }
250
251 #[test]
252 fn the_identity_curve_leaves_a_size_alone() {
253 assert!(FontScaleCurve::linear(1.0).is_identity());
254 assert!(!FontScaleCurve::linear(1.24).is_identity());
255 assert!(!platform_124().is_identity());
256 let flat: Vec<(f32, f32)> = (1..=40).map(|sp| (sp as f32, sp as f32)).collect();
257 assert!(FontScaleCurve::from_samples(1.0, &flat).is_identity());
258 }
259
260 #[test]
261 fn the_platform_curve_is_reproduced_at_every_size_the_platform_was_asked() {
262 let curve = platform_124();
263 for (sp, expected_px) in [
264 (0.4f32, 0.992f32),
265 (12.0, 29.76),
266 (13.0, 32.72),
267 (14.0, 35.68),
268 (15.0, 37.2),
269 (16.0, 38.72),
270 (18.0, 41.76),
271 (19.0, 43.76),
272 (20.0, 45.76),
273 (24.0, 51.84),
274 (30.0, 60.0),
275 (40.0, 80.0),
276 (100.0, 200.0),
277 ] {
278 let px = curve.sp_to_dp(sp) * 2.0;
279 assert!(
280 (px - expected_px).abs() < 1.0e-3,
281 "{sp}sp resolved to {px}px where the platform answered {expected_px}px",
282 );
283 }
284 }
285
286 #[test]
287 fn the_thirteen_sp_secondary_label_is_where_multiplying_goes_wrong() {
288 let curve = platform_124();
289 assert!((curve.sp_to_dp(13.0) * 2.0 - 32.72).abs() < 1.0e-3);
290 assert!((FontScaleCurve::linear(1.24).sp_to_dp(13.0) * 2.0 - 32.24).abs() < 1.0e-3);
291 }
292
293 #[test]
294 fn collinear_samples_are_dropped_and_the_curve_still_answers_the_same() {
295 let curve = platform_124();
296 assert_eq!(curve.knot_count(), 8);
297 let dense: Vec<(f32, f32)> = (1..=120)
298 .map(|sp| {
299 let sp = sp as f32;
300 (sp, curve.sp_to_dp(sp))
301 })
302 .collect();
303 let resampled = FontScaleCurve::from_samples(1.24, &dense);
304 for step in 1..=1200 {
305 let sp = step as f32 * 0.1;
306 assert!(
307 (resampled.sp_to_dp(sp) - curve.sp_to_dp(sp)).abs() < 1.0e-3,
308 "{sp}sp: {} against {}",
309 resampled.sp_to_dp(sp),
310 curve.sp_to_dp(sp),
311 );
312 }
313 }
314
315 #[test]
316 fn samples_the_platform_could_not_have_produced_fall_back_to_multiplying() {
317 for samples in [
318 &[][..],
319 &[(12.0, 14.88)][..],
320 &[(12.0, 14.88), (12.0, 15.0)][..],
321 &[(14.0, 17.84), (12.0, 14.88)][..],
322 &[(12.0, f32::NAN), (14.0, 17.84)][..],
323 &[(12.0, 0.0), (14.0, 17.84)][..],
324 ] {
325 let curve = FontScaleCurve::from_samples(1.24, samples);
326 assert!(curve.is_linear(), "{samples:?} was accepted");
327 assert_eq!(curve.sp_to_dp(13.0), 13.0 * 1.24);
328 }
329 }
330
331 #[test]
332 fn more_bends_than_there_are_knots_falls_back_rather_than_truncating() {
333 let zigzag: Vec<(f32, f32)> = (1..=40)
334 .map(|sp| {
335 let sp = sp as f32;
336 (sp, sp * if sp as i32 % 2 == 0 { 1.5 } else { 1.2 })
337 })
338 .collect();
339 assert!(FontScaleCurve::from_samples(1.24, &zigzag).is_linear());
340 }
341
342 #[test]
343 fn a_negative_size_keeps_its_sign() {
344 let curve = platform_124();
345 assert!((curve.sp_to_dp(-13.0) + curve.sp_to_dp(13.0)).abs() < 1.0e-6);
346 assert!(FontScaleCurve::linear(1.24).sp_to_dp(-13.0) < 0.0);
347 }
348
349 #[test]
350 fn a_size_that_is_not_a_number_comes_back_unchanged() {
351 let curve = platform_124();
352 assert!(curve.sp_to_dp(f32::NAN).is_nan());
353 assert_eq!(curve.sp_to_dp(f32::INFINITY), f32::INFINITY);
354 }
355
356 #[test]
357 fn curves_that_convert_differently_do_not_share_a_fingerprint() {
358 let platform = platform_124();
359 assert_ne!(
360 platform.fingerprint(),
361 FontScaleCurve::linear(1.24).fingerprint()
362 );
363 assert_ne!(
364 FontScaleCurve::linear(1.0).fingerprint(),
365 FontScaleCurve::linear(1.24).fingerprint()
366 );
367 assert_eq!(platform.fingerprint(), platform_124().fingerprint());
368 }
369}