rotated_grid/
lib.rs

1//! # Rotated grids for CMYK halftone dithering and more.
2//!
3//! This crate provides the [`GridPositionIterator`] type that creates
4//! spaced grid positions along a rotated grid.
5//!
6//! ## Order of generated coordinates
7//!
8//! Do note that the generation order of the coordinates depends on the specific grid parameters
9//! and may not be in the most efficient layout when used directly, depending on your use case.
10//! For image processing you may want to prefer a top-down order, in which case you should collect
11//! the coordinates into a vector and sort by `y` coordinate first.
12//!
13//! ## Example
14//!
15//! ```
16//! use rotated_grid::{Angle, GridPoint, GridPositionIterator};
17//!
18//! const WIDTH: usize = 16;
19//! const HEIGHT: usize = 10;
20//!
21//! let halftone_grids = [
22//!     ("Cyan", 15.0),
23//!     ("Magenta", 75.0),
24//!     ("Yellow", 0.0),
25//!     ("Black", 45.0),
26//! ];
27//!
28//! for (name, angle) in halftone_grids {
29//!     println!("{name} at {angle}°");
30//!
31//!     let grid = GridPositionIterator::new(
32//!         WIDTH as _,
33//!         HEIGHT as _,
34//!         7.0,
35//!         7.0,
36//!         0.0,
37//!         0.0,
38//!         Angle::<f64>::from_degrees(angle),
39//!     );
40//!
41//!     let (_, expected_max) = grid.size_hint();
42//!     let mut count = 0;
43//!
44//!     for GridPoint { x, y } in grid {
45//!         println!("{x}, {y}");
46//!         count += 1;
47//!     }
48//!
49//!     assert!(count <= expected_max.unwrap())
50//! }
51//! ```
52
53mod angle;
54mod point;
55
56pub use angle::Angle;
57pub use point::GridPoint;
58
59/// An iterator for positions on a rotated grid.
60pub struct GridPositionIterator {
61    width: f64,
62    height: f64,
63    rotated_width: f64,
64    rotated_height: f64,
65    dx: f64,
66    dy: f64,
67    x0: f64,
68    y0: f64,
69    center_x: f64,
70    center_y: f64,
71    start_x: f64,
72    start_y: f64,
73    sin_alpha: f64,
74    cos_alpha: f64,
75    current_x: f64,
76    current_y: f64,
77    #[cfg(debug_assertions)]
78    hits: u64,
79    #[cfg(debug_assertions)]
80    misses: u64,
81    #[cfg(debug_assertions)]
82    max_repeats: u64,
83}
84
85impl GridPositionIterator {
86    /// Creates a new iterator.
87    ///
88    /// ## Arguments
89    /// * `width` - The width of the grid. Must be positive.
90    /// * `height` - The height of the grid. Must be positive.
91    /// * `dx` - The spacing of grid elements along the (rotated) X axis.
92    /// * `dy` - The spacing of grid elements along the (rotated) Y axis.
93    /// * `x0` - The X offset of the first grid element.
94    /// * `x1` - The Y offset of the first grid element.
95    /// * `alpha` - The orientation of the grid.
96    pub fn new(
97        width: f64,
98        height: f64,
99        dx: f64,
100        dy: f64,
101        x0: f64,
102        y0: f64,
103        alpha: Angle<f64>,
104    ) -> Self {
105        assert!(width > 0.0);
106        assert!(height > 0.0);
107
108        let alpha = Self::normalize_angle(alpha.into_radians());
109        let (sin_alpha, cos_alpha) = alpha.sin_cos();
110
111        // Calculate the dimensions of the rotated grid
112        let rotated_width = (width.abs() * cos_alpha) + (height.abs() * sin_alpha);
113        let rotated_height = (width.abs() * sin_alpha) + (height.abs() * cos_alpha);
114
115        // Calculate the center of the rotated grid.
116        let center_x = x0 + (width * 0.5);
117        let center_y = y0 + (height * 0.5);
118
119        // Calculate the starting point of the rotated grid.
120        let start_x = center_x - (rotated_width * 0.5);
121        let start_y = center_y - (rotated_height * 0.5);
122
123        let iterator = GridPositionIterator {
124            width,
125            height,
126            rotated_width,
127            rotated_height,
128            dx,
129            dy,
130            x0,
131            y0,
132            center_x,
133            center_y,
134            start_x,
135            start_y,
136            sin_alpha,
137            cos_alpha,
138            current_x: 0.0,
139            current_y: 0.0,
140            #[cfg(debug_assertions)]
141            hits: 0,
142            #[cfg(debug_assertions)]
143            misses: 0,
144            #[cfg(debug_assertions)]
145            max_repeats: 0,
146        };
147        iterator
148    }
149
150    /// Provides an estimated upper bound for the number of grid points.
151    /// This is only correct for unrotated grids; rotated grids produce smaller values.
152    fn estimate_max_grid_points(&self) -> usize {
153        let num_points_x = (self.width / self.dx).ceil() as usize;
154        let num_points_y = (self.height / self.dy).ceil() as usize;
155        num_points_x * num_points_y
156    }
157
158    /// Normalizes the specified angle such that it falls into range -PI/2..PI/2.
159    fn normalize_angle(mut alpha: f64) -> f64 {
160        use std::f64::consts::PI;
161        const HALF_PI: f64 = PI * 0.5;
162        while alpha >= PI {
163            alpha -= PI;
164        }
165        while alpha >= HALF_PI {
166            alpha -= HALF_PI;
167        }
168        while alpha <= -PI {
169            alpha += PI;
170        }
171        while alpha <= -HALF_PI {
172            alpha += HALF_PI;
173        }
174        alpha
175    }
176}
177
178impl Iterator for GridPositionIterator {
179    type Item = GridPoint;
180
181    fn next(&mut self) -> Option<Self::Item> {
182        let (sin, cos) = (self.sin_alpha, self.cos_alpha);
183
184        let mut repeats = 0;
185        loop {
186            let x = self.start_x + self.current_x;
187            let y = self.start_y + self.current_y;
188
189            // Rotate the grid position back to the unrotated frame.
190            let inv_sin = -sin;
191            let inv_cos = cos;
192            let unrotated_x =
193                (x - self.center_x) * inv_cos - (y - self.center_y) * inv_sin + self.center_x;
194            let unrotated_y =
195                (x - self.center_x) * inv_sin + (y - self.center_y) * inv_cos + self.center_y;
196
197            // Update the current position.
198            self.current_x += self.dx;
199            if self.current_x > self.rotated_width {
200                self.current_x = 0.0;
201                self.current_y += self.dy;
202            }
203
204            // Check if the grid position is within the original rectangle.
205            if unrotated_x >= self.x0
206                && unrotated_x <= self.x0 + self.width
207                && unrotated_y >= self.y0
208                && unrotated_y <= self.y0 + self.height
209            {
210                #[cfg(debug_assertions)]
211                {
212                    self.hits += 1;
213                }
214                return Some(GridPoint::new(unrotated_x, unrotated_y));
215            }
216
217            if x > self.start_x + self.rotated_width || y > self.start_y + self.rotated_height {
218                #[cfg(debug_assertions)]
219                {
220                    debug_assert!(self.hits as usize <= self.estimate_max_grid_points());
221                }
222                return None;
223            }
224
225            #[cfg(debug_assertions)]
226            {
227                self.misses += 1;
228                repeats += 1;
229                self.max_repeats = repeats.max(self.max_repeats);
230            }
231        }
232    }
233
234    fn size_hint(&self) -> (usize, Option<usize>) {
235        (0, Some(self.estimate_max_grid_points()))
236    }
237}