glance_core/drawing/
shapes.rs

1use super::traits::Drawable;
2use crate::{
3    Result,
4    img::{Image, pixel::Pixel},
5};
6
7/// A circle shape that can be drawn onto an image.
8/// Can be either filled or drawn as an outline with a specified thickness.
9/// The color is specified in RGBA8 format.
10pub struct Circle<P: Pixel> {
11    /// Center of the circle (x, y)
12    pub position: (usize, usize),
13    /// Color as a struct that implements Pixel (like [`Rgba`], [`Luma`])
14    pub color: P,
15    /// Radius in pixels
16    pub radius: u32,
17    /// Fill the shape (true) or draw outline (false)
18    pub filled: bool,
19    /// Outline thickness (only used when `filled = false`)
20    pub thickness: u32,
21}
22
23impl<P> Drawable<P> for Circle<P>
24where
25    P: Pixel,
26{
27    fn draw_on(&self, image: &mut Image<P>) -> Result<()> {
28        let (cx, cy) = (self.position.0 as i32, self.position.1 as i32);
29        let radius = self.radius as i32;
30        let thickness = self.thickness as i32;
31        let dims = image.dimensions();
32
33        let outer_radius = radius + thickness;
34        let outer_radius_sq = outer_radius.pow(2);
35        let inner_radius_sq = radius.pow(2);
36
37        for dy in -outer_radius..outer_radius {
38            for dx in -outer_radius..outer_radius {
39                let nx = cx + dx;
40                let ny = cy + dy;
41
42                // Early skip for out of bounds pixels
43                if nx < 0 || ny < 0 {
44                    continue;
45                }
46
47                let (nx, ny) = (nx as usize, ny as usize);
48                if nx >= dims.0 || ny >= dims.1 {
49                    continue;
50                }
51
52                let distance_sq = dx * dx + dy * dy;
53                // Check if (nx, ny) is within bounds
54                let draw_pixel = match self.filled {
55                    true => distance_sq <= inner_radius_sq,
56                    false => distance_sq <= outer_radius_sq && distance_sq >= inner_radius_sq,
57                };
58
59                if draw_pixel {
60                    image.set_pixel((nx, ny), self.color)?;
61                }
62            }
63        }
64        Ok(())
65    }
66}
67
68/// An axis aligned bounding box that can be drawn onto an image.
69/// Can be either filled or drawn as an outline with a specified thickness.
70/// The color is specified in RGBA8 format.
71pub struct AABB<P: Pixel> {
72    /// Top-left corner (x, y)
73    pub position: (usize, usize),
74    /// Size as [width, height]
75    pub size: (usize, usize),
76    /// Color in RGBA8 format
77    pub color: P,
78    /// Fill the shape (true) or draw outline (false)
79    pub filled: bool,
80    /// Outline thickness (only used when `filled = false`)
81    pub thickness: u32,
82}
83
84impl<P> Drawable<P> for AABB<P>
85where
86    P: Pixel,
87{
88    fn draw_on(&self, image: &mut Image<P>) -> Result<()> {
89        let (cx, cy) = (self.position.0 as i32, self.position.1 as i32);
90        let dims = image.dimensions();
91        let width = self.size.0 as i32;
92        let height = self.size.1 as i32;
93        let thickness = self.thickness as i32;
94
95        let left_x = cx - thickness;
96        let right_x = cx + width + thickness;
97        let top_y = cy - thickness;
98        let bottom_y = cy + height + thickness;
99
100        // Go through all pixels inside the rectangle
101        for x in left_x..right_x {
102            for y in top_y..bottom_y {
103                // Early return for out of bounds pixels
104                if x < 0 || y < 0 {
105                    continue;
106                }
107
108                let nx = x as usize;
109                let ny = y as usize;
110
111                if nx >= dims.0 || ny >= dims.1 {
112                    continue;
113                }
114
115                // If not filled, only draw outline
116                let on_left_edge = x - left_x <= thickness;
117                let on_right_edge = right_x - x <= thickness;
118                let on_top_edge = y - top_y < thickness;
119                let on_bottom_edge = bottom_y - y <= thickness;
120                let draw_pixel =
121                    self.filled || on_left_edge || on_right_edge || on_top_edge || on_bottom_edge;
122
123                if draw_pixel {
124                    image.set_pixel((nx, ny), self.color)?;
125                }
126            }
127        }
128
129        Ok(())
130    }
131}
132
133/// A line that can be drawn onto an image. Uses a square brush (Bresenham's algorithm).
134/// The color is specified in RGBA8 format.
135pub struct Line<P: Pixel> {
136    /// Start point (x, y)
137    pub start: (usize, usize),
138    /// End point (x, y)
139    pub end: (usize, usize),
140    /// Color in RGBA8 format
141    pub color: P,
142    /// Line segment thickness (only used when `filled = false`)
143    pub thickness: u32,
144}
145
146impl<P> Drawable<P> for Line<P>
147where
148    P: Pixel,
149{
150    fn draw_on(&self, image: &mut Image<P>) -> Result<()> {
151        let (x0, y0) = self.start;
152        let (x1, y1) = self.end;
153
154        let dims = image.dimensions();
155
156        let x0 = x0 as i32;
157        let y0 = y0 as i32;
158        let x1 = x1 as i32;
159        let y1 = y1 as i32;
160        let thickness = self.thickness as i32;
161        let half_thickness = thickness / 2;
162
163        // Difference in each direction
164        let dx = (x1 - x0).abs();
165        let dy = -(y1 - y0).abs();
166        // Step in each direcion
167        let sx = if x0 < x1 { 1 } else { -1 };
168        let sy = if y0 < y1 { 1 } else { -1 };
169        let mut err = dx + dy;
170        let mut x = x0;
171        let mut y = y0;
172
173        loop {
174            for tx in -half_thickness..=half_thickness {
175                for ty in -half_thickness..=half_thickness {
176                    let nx = x + tx;
177                    let ny = y + ty;
178
179                    // Early return for out of bounds pixels
180                    if nx < 0 || ny < 0 {
181                        continue;
182                    }
183
184                    let (nx, ny) = (nx as usize, ny as usize);
185                    if nx >= dims.0 || ny >= dims.1 {
186                        continue;
187                    }
188
189                    image.set_pixel((nx, ny), self.color)?;
190                }
191            }
192
193            // Done if reached end point
194            if x == x1 && y == y1 {
195                break;
196            }
197
198            let err_twice = 2 * err;
199            if err_twice >= dy {
200                err += dy;
201                x += sx;
202            }
203            if err_twice <= dx {
204                err += dx;
205                y += sy;
206            }
207        }
208
209        Ok(())
210    }
211}