1use std::f32::consts::TAU;
2
3pub type Field = [f32; 3];
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Along {
7 pub s: f32,
8 pub tangent: [f32; 2],
9}
10
11fn sign(value: f32) -> f32 {
12 if value >= 0.0 { 1.0 } else { -1.0 }
13}
14
15fn length(p: [f32; 2]) -> f32 {
16 (p[0] * p[0] + p[1] * p[1]).sqrt()
17}
18
19fn direction(p: [f32; 2]) -> [f32; 2] {
20 let l = length(p);
21 if l > 0.0 {
22 [p[0] / l, p[1] / l]
23 } else {
24 [1.0, 0.0]
25 }
26}
27
28pub fn turn(p: [f32; 2], angle: f32) -> [f32; 2] {
29 let (s, c) = angle.sin_cos();
30 [c * p[0] - s * p[1], s * p[0] + c * p[1]]
31}
32
33pub fn corner(p: [f32; 2], radii: [f32; 4]) -> f32 {
34 let right = p[0] >= 0.0;
35 if p[1] >= 0.0 {
36 if right { radii[2] } else { radii[3] }
37 } else if right {
38 radii[1]
39 } else {
40 radii[0]
41 }
42}
43
44pub fn rect(p: [f32; 2], half: [f32; 2], radii: [f32; 4]) -> Field {
45 let radius = corner(p, radii);
46 let s = [sign(p[0]), sign(p[1])];
47 let q = [p[0].abs() - half[0] + radius, p[1].abs() - half[1] + radius];
48 if q[0] > 0.0 && q[1] > 0.0 {
49 let l = length(q);
50 return [l - radius, s[0] * q[0] / l, s[1] * q[1] / l];
51 }
52 if q[0] > q[1] {
53 [q[0] - radius, s[0], 0.0]
54 } else {
55 [q[1] - radius, 0.0, s[1]]
56 }
57}
58
59pub fn circle(p: [f32; 2], radius: f32) -> Field {
60 let d = direction(p);
61 [length(p) - radius, d[0], d[1]]
62}
63
64pub fn ring(p: [f32; 2], radius: f32, width: f32) -> Field {
65 let offset = length(p) - radius;
66 let d = direction(p);
67 let s = sign(offset);
68 [offset.abs() - width * 0.5, s * d[0], s * d[1]]
69}
70
71pub fn arc(p: [f32; 2], radius: f32, width: f32, start: f32, sweep: f32) -> Field {
72 if sweep >= TAU {
73 return ring(p, radius, width);
74 }
75 let half = sweep * 0.5;
76 let middle = start + half;
77 let q = turn(p, -middle);
78 let angle = q[1].atan2(q[0]);
79 if angle.abs() <= half {
80 return ring(p, radius, width);
81 }
82 let end = [radius * half.cos(), sign(q[1]) * radius * half.sin()];
83 let away = [q[0] - end[0], q[1] - end[1]];
84 let g = turn(direction(away), middle);
85 [length(away) - width * 0.5, g[0], g[1]]
86}
87
88pub fn rect_along(p: [f32; 2], half: [f32; 2], radii: [f32; 4]) -> Along {
89 let [tl, tr, br, bl] = radii;
90 let arc = radii.map(|radius| radius * TAU * 0.25);
91 let top = (half[0] - tl) + (half[0] - tr);
92 let right = (half[1] - tr) + (half[1] - br);
93 let bottom = (half[0] - bl) + (half[0] - br);
94 let left = (half[1] - tl) + (half[1] - bl);
95 let at_right = top + arc[1];
96 let at_bottom_arc = at_right + right;
97 let at_bottom = at_bottom_arc + arc[2];
98 let at_left_arc = at_bottom + bottom;
99 let at_left = at_left_arc + arc[3];
100 let at_top_arc = at_left + left;
101 let radius = corner(p, radii);
102 let inner = [half[0] - radius, half[1] - radius];
103 let q = [p[0].abs() - inner[0], p[1].abs() - inner[1]];
104 if q[0] > 0.0 && q[1] > 0.0 {
105 let s = [sign(p[0]), sign(p[1])];
106 let offset = [p[0] - s[0] * inner[0], p[1] - s[1] * inner[1]];
107 let rho = length(offset).max(1e-6);
108 let tangent = [
109 -offset[1] / rho * (radius / rho),
110 offset[0] / rho * (radius / rho),
111 ];
112 let (start, angle) = if s[0] > 0.0 && s[1] > 0.0 {
113 (at_bottom_arc, offset[1].atan2(offset[0]))
114 } else if s[0] < 0.0 && s[1] > 0.0 {
115 (at_left_arc, offset[1].atan2(offset[0]) - TAU * 0.25)
116 } else if s[0] < 0.0 && s[1] < 0.0 {
117 (at_top_arc, (-offset[1]).atan2(-offset[0]))
118 } else {
119 (top, offset[1].atan2(offset[0]) + TAU * 0.25)
120 };
121 return Along {
122 s: start + radius * angle.clamp(0.0, TAU * 0.25),
123 tangent,
124 };
125 }
126 if q[0] > q[1] {
127 if p[0] > 0.0 {
128 return Along {
129 s: at_right + p[1] + (half[1] - tr),
130 tangent: [0.0, 1.0],
131 };
132 }
133 return Along {
134 s: at_left + (half[1] - bl) - p[1],
135 tangent: [0.0, -1.0],
136 };
137 }
138 if p[1] < 0.0 {
139 return Along {
140 s: p[0] + (half[0] - tl),
141 tangent: [1.0, 0.0],
142 };
143 }
144 Along {
145 s: at_bottom + (half[0] - br) - p[0],
146 tangent: [-1.0, 0.0],
147 }
148}
149
150pub fn circle_along(p: [f32; 2], radius: f32) -> Along {
151 let rho = length(p).max(1e-6);
152 let mut angle = p[1].atan2(p[0]);
153 if angle < 0.0 {
154 angle += TAU;
155 }
156 Along {
157 s: radius * angle,
158 tangent: [-p[1] / rho * (radius / rho), p[0] / rho * (radius / rho)],
159 }
160}
161
162pub fn dash(along: Along, on: f32, off: f32, phase: f32) -> Field {
163 let period = on + off;
164 let u = along.s + phase - on * 0.5;
165 let centred = u - period * (u / period + 0.5).floor();
166 let s = sign(centred);
167 [
168 centred.abs() - on * 0.5,
169 s * along.tangent[0],
170 s * along.tangent[1],
171 ]
172}
173
174fn stripe(u: f32, width: f32) -> [f32; 2] {
175 let c = u - (u + 0.5).floor();
176 [c.abs() - width * 0.5, sign(c)]
177}
178
179pub fn pattern(kind: u32, params: [f32; 4], p: [f32; 2]) -> Field {
180 let scale = params[0];
181 let width = params[2];
182 let turned = turn(p, -params[1]);
183 let q = [turned[0] / scale, turned[1] / scale];
184 let (d, g) = match kind {
185 1 => {
186 let s = stripe(q[0], width);
187 (s[0], [s[1], 0.0])
188 }
189 2 => {
190 let f = [q[0] - (q[0] + 0.5).floor(), q[1] - (q[1] + 0.5).floor()];
191 (length(f) - width * 0.5, direction(f))
192 }
193 3 => {
194 let a = stripe(q[0], width);
195 let b = stripe(q[1], width);
196 if a[0] <= b[0] {
197 (a[0], [a[1], 0.0])
198 } else {
199 (b[0], [0.0, b[1]])
200 }
201 }
202 _ => {
203 let t = q[1] - (q[1] + 0.5).floor();
204 let s = stripe(q[0] + t.abs(), width);
205 let r = std::f32::consts::FRAC_1_SQRT_2;
206 (s[0] * r, [s[1] * r, s[1] * sign(t) * r])
207 }
208 };
209 let g = turn(g, params[1]);
210 [d * scale, g[0], g[1]]
211}
212
213pub fn coverage(field: Field, inverse: [[f32; 2]; 2]) -> f32 {
214 let screen = [
215 inverse[0][0] * field[1] + inverse[0][1] * field[2],
216 inverse[1][0] * field[1] + inverse[1][1] * field[2],
217 ];
218 (0.5 - field[0] / length(screen).max(1e-6)).clamp(0.0, 1.0)
219}
220
221pub fn coverage_even(distance: f32, inverse: [[f32; 2]; 2]) -> f32 {
222 let determinant = inverse[0][0] * inverse[1][1] - inverse[1][0] * inverse[0][1];
223 let unit = determinant.abs().sqrt();
224 (0.5 - distance / unit.max(1e-6)).clamp(0.0, 1.0)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 fn brute(f: impl Fn([f32; 2]) -> f32, p: [f32; 2]) -> [f32; 2] {
232 let e = 1e-3;
233 [
234 (f([p[0] + e, p[1]]) - f([p[0] - e, p[1]])) / (2.0 * e),
235 (f([p[0], p[1] + e]) - f([p[0], p[1] - e])) / (2.0 * e),
236 ]
237 }
238
239 fn check_gradient(f: impl Fn([f32; 2]) -> Field, points: &[[f32; 2]]) {
240 for &p in points {
241 let field = f(p);
242 if !field[1].is_finite() {
243 continue;
244 }
245 let numeric = brute(|q| f(q)[0], p);
246 assert!(
247 (field[1] - numeric[0]).abs() < 2e-2 && (field[2] - numeric[1]).abs() < 2e-2,
248 "{p:?}: {field:?} vs {numeric:?}"
249 );
250 }
251 }
252
253 fn grid() -> Vec<[f32; 2]> {
254 (0..23)
255 .flat_map(|i| (0..19).map(move |j| [-57.3 + i as f32 * 5.1, -48.7 + j as f32 * 5.3]))
256 .collect()
257 }
258
259 #[test]
260 fn rounded_rect_is_an_exact_distance() {
261 let d = rect([0.0, 0.0], [40.0, 20.0], [8.0; 4]);
262 assert_eq!(d[0], -20.0);
263 assert_eq!(rect([50.0, 0.0], [40.0, 20.0], [8.0; 4])[0], 10.0);
264 let corner = rect([32.0 + 3.0, -12.0 - 4.0], [40.0, 20.0], [8.0; 4])[0];
265 assert!((corner - (5.0 - 8.0)).abs() < 1e-5);
266 let off_axis = grid()
267 .into_iter()
268 .filter(|p| ((p[0].abs() - 32.0) - (p[1].abs() - 12.0)).abs() > 0.05)
269 .collect::<Vec<_>>();
270 check_gradient(|p| rect(p, [40.0, 20.0], [8.0; 4]), &off_axis);
271 }
272
273 #[test]
274 fn circle_ring_and_arc_distances() {
275 assert_eq!(circle([3.0, 4.0], 2.0)[0], 3.0);
276 assert_eq!(ring([0.0, 10.0], 10.0, 4.0)[0], -2.0);
277 assert_eq!(ring([0.0, 14.0], 10.0, 4.0)[0], 2.0);
278 let start = -TAU / 4.0;
279 let sweep = TAU * 0.25;
280 assert!((arc([0.0, -10.0], 10.0, 4.0, start, sweep)[0] + 2.0).abs() < 1e-5);
281 assert!((arc([10.0, 0.0], 10.0, 4.0, start, sweep)[0] + 2.0).abs() < 1e-5);
282 let past = arc([0.0, 10.0], 10.0, 4.0, start, sweep)[0];
283 assert!((past - (200.0f32.sqrt() - 2.0)).abs() < 1e-4);
284 let cap = arc([-3.0, -10.0], 10.0, 4.0, start, sweep)[0];
285 assert!((cap - 1.0).abs() < 1e-5);
286 check_gradient(|p| circle(p, 20.0), &grid());
287 check_gradient(|p| ring(p, 20.0, 6.0), &grid());
288 check_gradient(|p| arc(p, 30.0, 8.0, 0.4, 4.0), &grid());
289 }
290
291 #[test]
292 fn perimeter_runs_clockwise_from_the_top_left_like_svg() {
293 let half = [50.0, 30.0];
294 let r = [10.0; 4];
295 let arc = r[0] * TAU * 0.25;
296 assert!((rect_along([-40.0, -30.0], half, r).s).abs() < 1e-5);
297 assert!((rect_along([40.0, -30.0], half, r).s - 80.0).abs() < 1e-4);
298 assert!((rect_along([50.0, -20.0], half, r).s - (80.0 + arc)).abs() < 1e-4);
299 assert!((rect_along([50.0, 20.0], half, r).s - (120.0 + arc)).abs() < 1e-4);
300 assert!((rect_along([40.0, 30.0], half, r).s - (120.0 + 2.0 * arc)).abs() < 1e-4);
301 assert!((rect_along([-40.0, 30.0], half, r).s - (200.0 + 2.0 * arc)).abs() < 1e-4);
302 assert!((rect_along([-50.0, 20.0], half, r).s - (200.0 + 3.0 * arc)).abs() < 1e-4);
303 assert!((rect_along([-50.0, -20.0], half, r).s - (240.0 + 3.0 * arc)).abs() < 1e-4);
304 let total = 240.0 + 4.0 * arc;
305 let near_end = rect_along([-40.0 - 1e-3, -30.0], half, r).s;
306 assert!((near_end - total).abs() < 1e-2);
307 for p in grid() {
308 let along = rect_along(p, half, r);
309 let numeric = brute(|q| rect_along(q, half, r).s, p);
310 if numeric[0].abs() + numeric[1].abs() > 10.0 {
311 continue;
312 }
313 assert!(
314 (along.tangent[0] - numeric[0]).abs() < 2e-2
315 && (along.tangent[1] - numeric[1]).abs() < 2e-2,
316 "{p:?}"
317 );
318 }
319 }
320
321 #[test]
322 fn dashes_start_on_and_repeat() {
323 let at = |s: f32| {
324 dash(
325 Along {
326 s,
327 tangent: [1.0, 0.0],
328 },
329 14.0,
330 12.0,
331 0.0,
332 )[0]
333 };
334 assert!(at(7.0) < -6.9);
335 assert!((at(0.0)).abs() < 1e-5);
336 assert!((at(14.0)).abs() < 1e-5);
337 assert!(at(20.0) > 5.9);
338 assert!((at(33.0) - at(7.0)).abs() < 1e-4);
339 let shifted = dash(
340 Along {
341 s: 0.0,
342 tangent: [1.0, 0.0],
343 },
344 14.0,
345 12.0,
346 7.0,
347 )[0];
348 assert!((shifted + 7.0).abs() < 1e-5);
349 }
350
351 #[test]
352 fn coverage_is_a_one_pixel_ramp_in_screen_space() {
353 let identity = [[1.0, 0.0], [0.0, 1.0]];
354 assert_eq!(coverage([0.0, 1.0, 0.0], identity), 0.5);
355 assert_eq!(coverage([-0.5, 1.0, 0.0], identity), 1.0);
356 assert_eq!(coverage([0.25, 1.0, 0.0], identity), 0.25);
357 let half_scale = [[0.5, 0.0], [0.0, 0.5]];
358 assert_eq!(coverage([0.25, 1.0, 0.0], half_scale), 0.0);
359 let tipped = [[1.0, 0.0], [0.0, 2.0]];
360 assert_eq!(coverage([0.5, 0.0, 1.0], tipped), 0.25);
361 assert_eq!(coverage_even(0.25, identity), 0.25);
362 assert_eq!(coverage_even(0.25, half_scale), 0.0);
363 }
364}