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}