math2/
raster.rs

1use super::{rect::Rectangle, vector2::Vector2};
2
3/// Returns the fractional part of a number.
4///
5/// # Arguments
6/// * `x` - The input value.
7///
8/// # Example
9/// ```rust
10/// # use math2::fract;
11/// let frac = fract(3.14);
12/// assert!((frac - 0.14).abs() < 1e-2);
13/// ```
14pub fn fract(x: f32) -> f32 {
15    x - x.floor()
16}
17
18/// Computes a pseudo-random noise value for the given 2D coordinate.
19///
20/// This follows a GLSL-style hash using "magic" constants to produce a
21/// nicely distributed value. While fast for grain generation, it's not
22/// intended for high quality noise.
23///
24/// The calculation performed is:
25/// `noise(x, y) = fract(sin(x * 12.9898 + y * 78.233) * 43758.5453)`.
26///
27/// # Parameters
28/// - `x`: X coordinate.
29/// - `y`: Y coordinate.
30///
31/// # Returns
32/// A pseudo-random value in `[0, 1]`.
33///
34/// # Example
35/// ```rust
36/// # use math2::noise;
37/// let v = noise(12.34, 56.78);
38/// assert!(v >= 0.0 && v <= 1.0);
39/// ```
40pub fn noise(x: f32, y: f32) -> f32 {
41    fract(((x * 12.9898 + y * 78.233).sin()) * 43758.5453)
42}
43
44/// Returns all integer pixel coordinates along a straight line between
45/// `a` and `b` using Bresenham's algorithm.
46///
47/// # Parameters
48/// - `a`: Start point in pixel coordinates.
49/// - `b`: End point in pixel coordinates.
50///
51/// # Example
52/// ```rust
53/// # use math2::raster_bresenham;
54/// let pts = raster_bresenham([10.0, 10.0], [15.0, 20.0]);
55/// assert_eq!(pts.first(), Some(&[10.0, 10.0]));
56/// ```
57pub fn bresenham(a: Vector2, b: Vector2) -> Vec<Vector2> {
58    let (mut x0, mut y0) = (a[0] as i32, a[1] as i32);
59    let (x1, y1) = (b[0] as i32, b[1] as i32);
60    let mut pixels = Vec::new();
61
62    let dx = (x1 - x0).abs();
63    let sx = if x0 < x1 { 1 } else { -1 };
64    let dy = -(y1 - y0).abs();
65    let sy = if y0 < y1 { 1 } else { -1 };
66    let mut err = dx + dy;
67
68    loop {
69        pixels.push([x0 as f32, y0 as f32]);
70        if x0 == x1 && y0 == y1 {
71            break;
72        }
73        let e2 = 2 * err;
74        if e2 >= dy {
75            err += dy;
76            x0 += sx;
77        }
78        if e2 <= dx {
79            err += dx;
80            y0 += sy;
81        }
82    }
83
84    pixels
85}
86
87/// Generates all integer pixel coordinates contained within a rectangle.
88///
89/// The rectangle is defined by its top-left corner and dimensions, and
90/// the output includes all pixels from `x` to `x + width` and `y` to
91/// `y + height` inclusively.
92///
93/// # Example
94/// ```rust
95/// # use math2::{Rectangle, raster_rectangle};
96/// let rect = Rectangle { x: 40.0, y: 35.0, width: 20.0, height: 30.0 };
97/// let points = raster_rectangle(&rect);
98/// assert!(points.contains(&[40.0, 35.0]));
99/// ```
100pub fn rectangle(rect: &Rectangle) -> Vec<Vector2> {
101    let start_x = rect.x.ceil() as i32;
102    let end_x = (rect.x + rect.width).floor() as i32;
103    let start_y = rect.y.ceil() as i32;
104    let end_y = (rect.y + rect.height).floor() as i32;
105    let mut points = Vec::new();
106    for y in start_y..=end_y {
107        for x in start_x..=end_x {
108            points.push([x as f32, y as f32]);
109        }
110    }
111    points
112}
113
114/// A raw bitmap represented by width, height and RGBA data.
115#[derive(Clone, Debug, PartialEq)]
116pub struct Bitmap {
117    pub width: usize,
118    pub height: usize,
119    pub data: Vec<u8>,
120}
121
122/// Tiles a source bitmap to cover the given `width` and `height`.
123pub fn tile(source: &Bitmap, width: usize, height: usize) -> Bitmap {
124    let mut out = vec![0u8; width * height * 4];
125    for y in 0..height {
126        for x in 0..width {
127            let sx = x % source.width;
128            let sy = y % source.height;
129            let src = (sy * source.width + sx) * 4;
130            let dst = (y * width + x) * 4;
131            out[dst..dst + 4].copy_from_slice(&source.data[src..src + 4]);
132        }
133    }
134    Bitmap {
135        width,
136        height,
137        data: out,
138    }
139}
140
141/// Scales a bitmap by `[factor_x, factor_y]` using nearest neighbour sampling.
142pub fn scale(bitmap: &Bitmap, factor: Vector2) -> Bitmap {
143    let (factor_x, factor_y) = (factor[0], factor[1]);
144    assert!(factor_x > 0.0 && factor_y > 0.0, "factors must be positive");
145    let width = ((bitmap.width as f32 * factor_x).floor().max(1.0)) as usize;
146    let height = ((bitmap.height as f32 * factor_y).floor().max(1.0)) as usize;
147    let mut out = vec![0u8; width * height * 4];
148    for y in 0..height {
149        for x in 0..width {
150            let sx = ((x as f32) / factor_x).floor() as usize;
151            let sy = ((y as f32) / factor_y).floor() as usize;
152            let src = (sy * bitmap.width + sx) * 4;
153            let dst = (y * width + x) * 4;
154            out[dst..dst + 4].copy_from_slice(&bitmap.data[src..src + 4]);
155        }
156    }
157    Bitmap {
158        width,
159        height,
160        data: out,
161    }
162}
163
164/// Resizes a bitmap to the specified `[width, height]`.
165pub fn resize(bitmap: &Bitmap, dst: Vector2) -> Bitmap {
166    let (w2, h2) = (dst[0] as f32, dst[1] as f32);
167    let fx = w2 / bitmap.width as f32;
168    let fy = h2 / bitmap.height as f32;
169    scale(bitmap, [fx, fy])
170}
171
172/// Pads a bitmap to the given `[width, height]` filling empty space with `bg`.
173pub fn pad(bitmap: &Bitmap, dst: Vector2, bg: super::vector4::Vector4) -> Bitmap {
174    let width = dst[0] as usize;
175    let height = dst[1] as usize;
176    let mut out = vec![0u8; width * height * 4];
177    for i in 0..width * height {
178        let idx = i * 4;
179        out[idx] = bg[0] as u8;
180        out[idx + 1] = bg[1] as u8;
181        out[idx + 2] = bg[2] as u8;
182        out[idx + 3] = bg[3] as u8;
183    }
184    let offset_x = ((width as i32 - bitmap.width as i32) / 2) as isize;
185    let offset_y = ((height as i32 - bitmap.height as i32) / 2) as isize;
186    for y in 0..bitmap.height {
187        for x in 0..bitmap.width {
188            let dst_x = x as isize + offset_x;
189            let dst_y = y as isize + offset_y;
190            if dst_x < 0 || dst_y < 0 || dst_x >= width as isize || dst_y >= height as isize {
191                continue;
192            }
193            let src = (y * bitmap.width + x) * 4;
194            let dst = (dst_y as usize * width + dst_x as usize) * 4;
195            out[dst..dst + 4].copy_from_slice(&bitmap.data[src..src + 4]);
196        }
197    }
198    Bitmap {
199        width,
200        height,
201        data: out,
202    }
203}
204
205/// Flood fills starting at `pos` with `fill` color.
206pub fn floodfill(bitmap: &mut Bitmap, pos: Vector2, fill: super::vector4::Vector4) {
207    let x = pos[0] as i32;
208    let y = pos[1] as i32;
209    if x < 0 || y < 0 || x >= bitmap.width as i32 || y >= bitmap.height as i32 {
210        return;
211    }
212    let idx = (y as usize * bitmap.width + x as usize) * 4;
213    let target = [
214        bitmap.data[idx],
215        bitmap.data[idx + 1],
216        bitmap.data[idx + 2],
217        bitmap.data[idx + 3],
218    ];
219    if target == [fill[0] as u8, fill[1] as u8, fill[2] as u8, fill[3] as u8] {
220        return;
221    }
222    let mut stack = vec![(x, y)];
223    while let Some((cx, cy)) = stack.pop() {
224        if cx < 0 || cy < 0 || cx >= bitmap.width as i32 || cy >= bitmap.height as i32 {
225            continue;
226        }
227        let i = (cy as usize * bitmap.width + cx as usize) * 4;
228        let cur = [
229            bitmap.data[i],
230            bitmap.data[i + 1],
231            bitmap.data[i + 2],
232            bitmap.data[i + 3],
233        ];
234        if cur != target {
235            continue;
236        }
237        bitmap.data[i..i + 4].copy_from_slice(&[
238            fill[0] as u8,
239            fill[1] as u8,
240            fill[2] as u8,
241            fill[3] as u8,
242        ]);
243        stack.push((cx - 1, cy));
244        stack.push((cx + 1, cy));
245        stack.push((cx, cy - 1));
246        stack.push((cx, cy + 1));
247    }
248}
249
250/// Generates integer pixel coordinates within a circle with optional clipping.
251pub fn circle(center: Vector2, radius: f32, clip: Option<Rectangle>) -> Vec<Vector2> {
252    let (cx, cy) = (center[0], center[1]);
253    let r_sq = radius * radius;
254    let mut results = Vec::new();
255    let (min_x, min_y, max_x, max_y) = if let Some(c) = clip {
256        (c.x, c.y, c.x + c.width - 1.0, c.y + c.height - 1.0)
257    } else {
258        (
259            f32::NEG_INFINITY,
260            f32::NEG_INFINITY,
261            f32::INFINITY,
262            f32::INFINITY,
263        )
264    };
265    let y_start = (cy - radius).floor() as i32;
266    let y_end = (cy + radius).floor() as i32;
267    for y in y_start..=y_end {
268        let dy = y as f32 - cy;
269        let span = (r_sq - dy * dy).sqrt();
270        if span.is_nan() {
271            continue;
272        }
273        let left = (cx - span).floor() as i32;
274        let right = (cx + span).floor() as i32;
275        for x in left..=right {
276            let xf = x as f32;
277            let yf = y as f32;
278            if xf < min_x || xf > max_x || yf < min_y || yf > max_y {
279                continue;
280            }
281            results.push([xf, yf]);
282        }
283    }
284    results
285}
286
287/// Returns pixel coordinates for an ellipse.
288pub fn ellipse(center: Vector2, radius: Vector2) -> Vec<Vector2> {
289    let (cx, cy) = (center[0], center[1]);
290    let (rx, ry) = (radius[0], radius[1]);
291    let mut pts = Vec::new();
292    let start_x = (cx - rx).ceil() as i32;
293    let end_x = (cx + rx).floor() as i32;
294    let start_y = (cy - ry).ceil() as i32;
295    let end_y = (cy + ry).floor() as i32;
296    for y in start_y..=end_y {
297        for x in start_x..=end_x {
298            let dx = x as f32 - cx;
299            let dy = y as f32 - cy;
300            if (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1.0 {
301                pts.push([x as f32, y as f32]);
302            }
303        }
304    }
305    pts
306}
307
308/// Gaussian weight with `hardness` controlling the falloff steepness.
309pub fn gaussian(norm_dist: f32, hardness: f32) -> f32 {
310    let k_hard = 2.0_f32;
311    let k_soft = 10.0_f32;
312    let k = hardness * k_hard + (1.0 - hardness) * k_soft;
313    (-k * norm_dist * norm_dist).exp()
314}
315
316/// Generalized smoothstep function of order `n`.
317pub fn smoothstep(n: i32, mut x: f32) -> f32 {
318    use super::utils::clamp;
319    x = clamp(x, 0.0, 1.0);
320    let mut result = 0.0;
321    for i in 0..=n {
322        result += pascaltriangle(-(n as f32) - 1.0, i)
323            * pascaltriangle(2.0 * n as f32 + 1.0, n - i)
324            * x.powf(n as f32 + i as f32 + 1.0);
325    }
326    result
327}
328
329/// Binomial coefficient using generalized Pascal's triangle.
330pub fn pascaltriangle(a: f32, b: i32) -> f32 {
331    let mut result = 1.0;
332    for i in 0..b {
333        result *= (a - i as f32) / (i as f32 + 1.0);
334    }
335    result
336}