Skip to main content

pfx_live/flat/
sdf.rs

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}