1use super::BrushStroke;
4use motion_core::{Color, Path2D, Point, ValidationError};
5
6#[derive(Clone, Copy, Debug)]
8pub struct StrokeFont {
9 height: f64,
10}
11
12impl StrokeFont {
13 pub fn new(height: f64) -> Result<Self, ValidationError> {
15 if !height.is_finite() || !(f64::MIN_POSITIVE..=512.0).contains(&height) {
16 return Err(invalid("height", "Require positive finite height <=512"));
17 }
18 Ok(Self { height })
19 }
20
21 pub fn strokes(
24 &self,
25 text: &str,
26 width: f64,
27 color: Color,
28 ) -> Result<Vec<BrushStroke>, ValidationError> {
29 if text.chars().count() > 256
30 || !width.is_finite()
31 || !(f64::MIN_POSITIVE..=512.0).contains(&width)
32 {
33 return Err(invalid(
34 "text",
35 "Require <=256 characters and positive finite width <=512",
36 ));
37 }
38 let segments = [
39 [[0.0, 0.0], [1.0, 0.0]],
40 [[1.0, 0.0], [1.0, 0.5]],
41 [[1.0, 0.5], [1.0, 1.0]],
42 [[0.0, 1.0], [1.0, 1.0]],
43 [[0.0, 0.5], [0.0, 1.0]],
44 [[0.0, 0.0], [0.0, 0.5]],
45 [[0.0, 0.5], [0.5, 0.5]],
46 [[0.5, 0.5], [1.0, 0.5]],
47 [[0.0, 0.0], [0.5, 0.5]],
48 [[1.0, 0.0], [0.5, 0.5]],
49 [[0.5, 0.5], [0.0, 1.0]],
50 [[0.5, 0.5], [1.0, 1.0]],
51 [[0.5, 0.0], [0.5, 0.5]],
52 [[0.5, 0.5], [0.5, 1.0]],
53 ];
54 let mut result = Vec::new();
55 let mut column = 0.0;
56 let mut row = 0.0;
57 for character in text.chars() {
58 if character == '\n' {
59 column = 0.0;
60 row += 1.4;
61 continue;
62 }
63 if character == ' ' {
64 column += 1.0;
65 continue;
66 }
67 let glyph = glyph(character.to_ascii_uppercase())
68 .ok_or_else(|| invalid("glyph", "Unsupported single-line character"))?;
69 let mut path = Path2D::new();
70 for index in glyph {
71 let [first, second] = segments[*index];
72 let point = |value: [f64; 2]| {
73 Point::new(
74 (column + value[0] * 0.65) * self.height,
75 (row + value[1]) * self.height,
76 )
77 };
78 path.try_move_to(point(first))?;
79 path.try_line_to(point(second))?;
80 }
81 result.push(BrushStroke::new(path, width, color));
82 column += 1.0;
83 }
84 Ok(result)
85 }
86}
87
88fn invalid(field: &str, message: &str) -> ValidationError {
89 ValidationError::invalid("stroke_font", field, message)
90}
91
92fn glyph(character: char) -> Option<&'static [usize]> {
93 Some(match character {
94 'A' => &[0, 1, 2, 4, 5, 6, 7],
95 'B' | '8' => &[0, 1, 2, 3, 4, 5, 6, 7],
96 'C' => &[0, 3, 4, 5],
97 'D' | 'O' | '0' => &[0, 1, 2, 3, 4, 5],
98 'E' => &[0, 3, 4, 5, 6, 7],
99 'F' => &[0, 4, 5, 6, 7],
100 'G' => &[0, 2, 3, 4, 5, 7],
101 'H' => &[1, 2, 4, 5, 6, 7],
102 'I' => &[0, 3, 12, 13],
103 'J' => &[1, 2, 3, 4],
104 'K' => &[4, 5, 9, 11],
105 'L' => &[3, 4, 5],
106 'M' => &[1, 2, 4, 5, 8, 9],
107 'N' => &[1, 2, 4, 5, 8, 11],
108 'P' => &[0, 1, 4, 5, 6, 7],
109 'Q' => &[0, 1, 2, 3, 4, 5, 11],
110 'R' => &[0, 1, 4, 5, 6, 7, 11],
111 'S' | '5' => &[0, 2, 3, 5, 6, 7],
112 'T' => &[0, 12, 13],
113 'U' => &[1, 2, 3, 4, 5],
114 'V' => &[1, 5, 10, 11],
115 'W' => &[1, 2, 4, 5, 10, 11],
116 'X' => &[8, 9, 10, 11],
117 'Y' => &[8, 9, 13],
118 'Z' => &[0, 3, 9, 10],
119 '1' => &[1, 2],
120 '2' => &[0, 1, 3, 4, 6, 7],
121 '3' => &[0, 1, 2, 3, 6, 7],
122 '4' => &[1, 2, 5, 6, 7],
123 '6' => &[0, 2, 3, 4, 5, 6, 7],
124 '7' => &[0, 1, 2],
125 '9' => &[0, 1, 2, 3, 5, 6, 7],
126 '-' => &[6, 7],
127 '+' => &[6, 7, 12, 13],
128 _ => return None,
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn lettering_is_bounded_deterministic_and_asset_free() -> motion_core::Result {
138 let font = StrokeFont::new(20.0)?;
139 let text = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-";
140 let first = font.strokes(text, 1.0, Color::WHITE)?;
141 let second = font.strokes(text, 1.0, Color::WHITE)?;
142 assert_eq!(first.len(), 38);
143 let nodes = first
144 .into_iter()
145 .map(motion_core::Node::from)
146 .collect::<Vec<_>>();
147 let again = second
148 .into_iter()
149 .map(motion_core::Node::from)
150 .collect::<Vec<_>>();
151 let compile = |nodes| {
152 motion_core::Node::group(nodes).compile(
153 motion_core::Size::new(800.0, 40.0),
154 motion_core::PixelSize::new(800, 40),
155 motion_core::ValidationLimits::default(),
156 )
157 };
158 assert_eq!(compile(nodes)?, compile(again)?);
159 assert!(font.strokes("\u{4e2d}", 1.0, Color::WHITE).is_err());
160 assert!(font.strokes(&"A".repeat(257), 1.0, Color::WHITE).is_err());
161 assert!(StrokeFont::new(f64::NAN).is_err());
162 Ok(())
163 }
164}