Skip to main content

denise/
angle.rs

1//! Binary-turn angles and the fixed point the geometry runs in.
2//!
3//! A full revolution is [`TURN`] = 65536, angle 0 points at twelve o'clock, and
4//! positive sweeps go clockwise. This is the unit a panel actually computes in:
5//! a progress ring is `done * TURN / total` with no floating point and no π, and
6//! wrap-around is `& (TURN - 1)` rather than a comparison nobody remembers to
7//! write. Radians would put `f32` and a libm dependency in the one crate that
8//! has neither; degrees would make the common quarters 90/180/270 but leave the
9//! progress ring with a rounding remainder. Negative sweeps go anticlockwise.
10//!
11//!
12//! No floating point, so a `no_std` target needs no `libm` and the same angle
13//! gives the same pixel on x86 and on ARM.
14
15/// One full revolution, in the angle unit every arc call takes.
16///
17/// Angle 0 is twelve o'clock; positive angles go clockwise. `TURN / 4` is three
18/// o'clock, `TURN / 2` six, and a progress ring at 30% is a sweep of
19/// `30 * TURN / 100`.
20pub const TURN: i32 = 1 << 16;
21
22/// sin(i / 256 · τ/4) · 2^16, for the first quarter turn inclusive.
23///
24/// Generated from the real function and pinned by exhaustive tests rather than
25/// trusted; see the module documentation.
26#[rustfmt::skip]
27const SIN_QUARTER: [i32; 257] = [
28    0, 402, 804, 1206, 1608, 2010, 2412, 2814,
29    3216, 3617, 4019, 4420, 4821, 5222, 5623, 6023,
30    6424, 6824, 7224, 7623, 8022, 8421, 8820, 9218,
31    9616, 10014, 10411, 10808, 11204, 11600, 11996, 12391,
32    12785, 13180, 13573, 13966, 14359, 14751, 15143, 15534,
33    15924, 16314, 16703, 17091, 17479, 17867, 18253, 18639,
34    19024, 19409, 19792, 20175, 20557, 20939, 21320, 21699,
35    22078, 22457, 22834, 23210, 23586, 23961, 24335, 24708,
36    25080, 25451, 25821, 26190, 26558, 26925, 27291, 27656,
37    28020, 28383, 28745, 29106, 29466, 29824, 30182, 30538,
38    30893, 31248, 31600, 31952, 32303, 32652, 33000, 33347,
39    33692, 34037, 34380, 34721, 35062, 35401, 35738, 36075,
40    36410, 36744, 37076, 37407, 37736, 38064, 38391, 38716,
41    39040, 39362, 39683, 40002, 40320, 40636, 40951, 41264,
42    41576, 41886, 42194, 42501, 42806, 43110, 43412, 43713,
43    44011, 44308, 44604, 44898, 45190, 45480, 45769, 46056,
44    46341, 46624, 46906, 47186, 47464, 47741, 48015, 48288,
45    48559, 48828, 49095, 49361, 49624, 49886, 50146, 50404,
46    50660, 50914, 51166, 51417, 51665, 51911, 52156, 52398,
47    52639, 52878, 53114, 53349, 53581, 53812, 54040, 54267,
48    54491, 54714, 54934, 55152, 55368, 55582, 55794, 56004,
49    56212, 56418, 56621, 56823, 57022, 57219, 57414, 57607,
50    57798, 57986, 58172, 58356, 58538, 58718, 58896, 59071,
51    59244, 59415, 59583, 59750, 59914, 60075, 60235, 60392,
52    60547, 60700, 60851, 60999, 61145, 61288, 61429, 61568,
53    61705, 61839, 61971, 62101, 62228, 62353, 62476, 62596,
54    62714, 62830, 62943, 63054, 63162, 63268, 63372, 63473,
55    63572, 63668, 63763, 63854, 63944, 64031, 64115, 64197,
56    64277, 64354, 64429, 64501, 64571, 64639, 64704, 64766,
57    64827, 64884, 64940, 64993, 65043, 65091, 65137, 65180,
58    65220, 65259, 65294, 65328, 65358, 65387, 65413, 65436,
59    65457, 65476, 65492, 65505, 65516, 65525, 65531, 65535,
60    65536,
61];
62
63/// sin of a binary-turn angle, in Q16.
64fn sin_bam(angle: i32) -> i32 {
65    let a = angle.rem_euclid(TURN);
66    let quarter = TURN / 4;
67    let (quadrant, q) = (a / quarter, a % quarter);
68    // Fold into the first quarter. The fold reaches q = quarter inclusive, which
69    // is the last table entry with nothing to interpolate towards.
70    let lookup = |q: i32| -> i32 {
71        let idx = (q >> 6) as usize;
72        let frac = q & 63;
73        if frac == 0 {
74            SIN_QUARTER[idx]
75        } else {
76            SIN_QUARTER[idx] + (SIN_QUARTER[idx + 1] - SIN_QUARTER[idx]) * frac / 64
77        }
78    };
79    match quadrant {
80        0 => lookup(q),
81        1 => lookup(quarter - q),
82        2 => -lookup(q),
83        _ => -lookup(quarter - q),
84    }
85}
86
87/// The unit vector of a clock angle, in Q16 screen coordinates (y down).
88///
89/// Twelve o'clock is (0, -1), three o'clock (1, 0).
90pub fn direction(angle: i32) -> (i32, i32) {
91    (sin_bam(angle), -sin_bam(angle + TURN / 4))
92}
93
94/// Fractional bits in the fixed-point coordinates the shape code uses.
95pub const FRAC_BITS: u32 = 8;
96
97/// One whole pixel in fixed point.
98pub const ONE: i32 = 1 << FRAC_BITS;
99
100/// Coordinates are clamped to this before entering fixed point, so a rectangle
101/// placed absurdly far off-screen cannot overflow the shift. Any real surface is
102/// orders of magnitude inside it.
103pub const COORD_LIMIT: i32 = 1 << 22;
104
105/// A whole-pixel coordinate in fixed point, clamped to [`COORD_LIMIT`].
106#[inline]
107pub fn to_fx(v: i32) -> i32 {
108    v.clamp(-COORD_LIMIT, COORD_LIMIT) << FRAC_BITS
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    // ------------------------------------------------------------- the table
116
117    /// The table is data and data can rot, so it is held to the mathematics it
118    /// claims to encode: every one of the 65536 angles must satisfy the
119    /// Pythagorean identity to about a part in a thousand.
120    #[test]
121    fn every_angle_satisfies_the_pythagorean_identity() {
122        for a in 0..TURN {
123            let s = sin_bam(a) as i64;
124            let c = sin_bam(a + TURN / 4) as i64;
125            let one = (s * s + c * c) >> 16;
126            assert!(
127                (one - 65536).abs() < 64,
128                "angle {a}: sin²+cos² is {one}, not 65536"
129            );
130        }
131    }
132
133    /// The quarters are exact, not approximately right: a progress ring at
134    /// exactly 25% must point at exactly three o'clock.
135    #[test]
136    fn the_cardinal_directions_are_exact() {
137        assert_eq!(direction(0), (0, -65536), "twelve o'clock");
138        assert_eq!(direction(TURN / 4), (65536, 0), "three o'clock");
139        assert_eq!(direction(TURN / 2), (0, 65536), "six o'clock");
140        assert_eq!(direction(3 * TURN / 4), (-65536, 0), "nine o'clock");
141        assert_eq!(direction(TURN), (0, -65536), "and round again");
142        assert_eq!(direction(-TURN / 4), (-65536, 0), "negative wraps too");
143    }
144
145    /// Monotone over the first quarter — a table with a transposed pair of
146    /// entries would still pass the identity test within tolerance.
147    #[test]
148    fn sine_rises_monotonically_over_the_first_quarter() {
149        let mut previous = -1;
150        for a in 0..=TURN / 4 {
151            let s = sin_bam(a);
152            assert!(s >= previous, "sin fell at angle {a}");
153            previous = s;
154        }
155    }
156}