potlood/
lib.rs

1#![feature(array_windows)]
2use std::{fs::File, io::Write};
3
4pub enum Color {
5    Black,
6    Grey,
7    White,
8    Red,
9    Green,
10    Blue,
11    Yellow,
12    Cyan,
13    Magenta,
14    Rgb(u8, u8, u8),
15    Hex(u32),
16    RgbFloat(f64, f64, f64),
17}
18
19#[derive(Clone, Copy)]
20pub struct Pixel {
21    r: u8,
22    g: u8,
23    b: u8,
24}
25
26impl Pixel {
27    pub fn new(r: u8, g: u8, b: u8) -> Self {
28        Self { r, g, b }
29    }
30
31    pub fn color(c: Color) -> Self {
32        match c {
33            Color::Black => Pixel::new(0, 0, 0),
34            Color::Grey => Pixel::new(0xff / 2, 0xff / 2, 0xff / 2),
35            Color::White => Pixel::new(0xff, 0xff, 0xff),
36            Color::Red => Pixel::new(0xff, 0, 0),
37            Color::Green => Pixel::new(0, 0xff, 0),
38            Color::Blue => Pixel::new(0, 0, 0xff),
39            Color::Yellow => Pixel::new(0xff, 0xff, 0),
40            Color::Cyan => Pixel::new(0, 0xff, 0xff),
41            Color::Magenta => Pixel::new(0xff, 0, 0xff),
42            Color::Rgb(r, g, b) => Pixel::new(r, g, b),
43            Color::RgbFloat(r, g, b) => {
44                let convert = |v| (v * 255.0) as u8;
45                Pixel::new(convert(r), convert(g), convert(b))
46            }
47            Color::Hex(h) => {
48                let bytes = h.to_le_bytes();
49                Pixel::new(bytes[2], bytes[1], bytes[0])
50            }
51        }
52    }
53
54    pub fn empty() -> Self {
55        Self { r: 0, g: 0, b: 0 }
56    }
57
58    pub fn ppm(&self) -> String {
59        format!("{} {} {}", self.r, self.g, self.b)
60    }
61}
62
63#[derive(Debug)]
64pub enum PaperError {
65    OutOfBounds,
66}
67
68pub struct Paper {
69    width: usize,
70    height: usize,
71    pixels: Vec<Pixel>,
72}
73
74impl Paper {
75    pub fn new(width: usize, height: usize) -> Self {
76        Self {
77            width,
78            height,
79            pixels: vec![Pixel::empty(); width * height],
80        }
81    }
82
83    fn ppm(&self) -> String {
84        let header = format!("P3\n{} {}\n255", self.width, self.height);
85        let pixels = self
86            .pixels
87            .iter()
88            .map(|pixel| pixel.ppm())
89            .collect::<Vec<String>>()
90            .join("\n");
91        return [header, pixels, String::new()].join("\n");
92    }
93
94    pub fn generate(&self, path: String) -> std::io::Result<()> {
95        let mut file = File::create(path)?;
96        file.write_all(self.ppm().as_bytes())?;
97        Ok(())
98    }
99
100    pub fn set(&mut self, x: usize, y: usize, pixel: Pixel) {
101        self.pixels[y * self.width + x] = pixel
102    }
103
104    pub fn set_checked(&mut self, x: usize, y: usize, pixel: Pixel) -> Result<(), PaperError> {
105        if !(x < self.width && y < self.height) {
106            return Err(PaperError::OutOfBounds);
107        }
108
109        self.set(x, y, pixel);
110        Ok(())
111    }
112
113    pub fn set_horizontal(&mut self, y: usize, pixel: Pixel) {
114        let index = y * self.width;
115        let mut row = vec![pixel; self.width];
116        self.pixels[index..index + self.width].swap_with_slice(&mut row)
117    }
118
119    pub fn set_vertical(&mut self, x: usize, pixel: Pixel) {
120        self.pixels
121            .chunks_mut(self.width)
122            .for_each(|row| row[x] = pixel)
123    }
124
125    pub fn fill(&mut self, pixel: Pixel) {
126        self.pixels.iter_mut().for_each(|p| *p = pixel)
127    }
128
129    pub fn rect(
130        &mut self,
131        x: usize,
132        y: usize,
133        width: usize,
134        height: usize,
135        pixel: Pixel,
136    ) -> Result<(), PaperError> {
137        if !(x + width < self.width && y + height < self.height) {
138            return Err(PaperError::OutOfBounds);
139        }
140
141        for row in y..y + height {
142            for col in x..x + width {
143                self.set(col, row, pixel)
144            }
145        }
146
147        Ok(())
148    }
149
150    pub fn circle(
151        &mut self,
152        x: usize,
153        y: usize,
154        radius: usize,
155        pixel: Pixel,
156    ) -> Result<(), PaperError> {
157        if !(radius < x && radius < y && x + radius < self.width && y + radius < self.height) {
158            return Err(PaperError::OutOfBounds);
159        }
160
161        let odd = 1 - radius % 2;
162
163        for row in y - radius..y + radius {
164            let v = y as isize - row as isize;
165            for col in x - radius..x + radius {
166                // From this square of pixels, we only set those which we calculate to be _inside_
167                // the circle.
168                //
169                //        .(row, col)
170                //      d/|
171                //      /_|v
172                //     M h
173                //
174                // With
175                //     v = y - row
176                // and
177                //     h = x - col
178                // we can see
179                //     d^2 = h^2 + v^2.
180                // In case the point lies in the circle,
181                //     d < radius,
182                // which is identical to
183                //     d^2 < radius^2.
184                let h = x as isize - col as isize;
185                let d_squared = h * h + v * v;
186                // TODO: Check out the effect of <= vs < and change code and above comment
187                // accordingly.
188                if (d_squared as usize) <= radius * radius + (1 * odd) {
189                    self.set(col, row, pixel)
190                }
191            }
192        }
193
194        Ok(())
195    }
196
197    pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, pixel: Pixel) {
198        let dx = x1.abs_diff(x0) as isize;
199        let sx = if x0 < x1 { 1 } else { -1 };
200        let dy = -(y1.abs_diff(y0) as isize);
201        let sy = if y0 < y1 { 1 } else { -1 };
202        let mut error = dx + dy;
203        let mut x0 = x0 as isize;
204        let mut y0 = y0 as isize;
205        loop {
206            if self.set_checked(x0 as usize, y0 as usize, pixel).is_err() {
207                break;
208            };
209            if x0 == x1 as isize && y0 == y1 as isize {
210                break;
211            }
212            if 2 * error >= dy {
213                if x0 == x1 as isize {
214                    break;
215                }
216                error = error + dy;
217                x0 = x0 + sx;
218            }
219            if 2 * error <= dx {
220                if y0 == y1 as isize {
221                    break;
222                }
223                error = error + dx;
224                y0 = y0 + sy;
225            }
226        }
227    }
228
229    pub fn stroke(&mut self, curve: Vec<(usize, usize)>, pixel: Pixel) {
230        for &[(x0, y0), (x1, y1)] in curve.array_windows() {
231            self.line(x0, y0, x1, y1, pixel)
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn pixel_ppm() {
242        let pixel = Pixel::new(20, 30, 40);
243        let ppm = pixel.ppm();
244        assert_eq!(ppm, "20 30 40");
245    }
246
247    #[test]
248    fn ppm() {
249        let mut paper = Paper::new(2, 3);
250        paper.line(0, 0, 1, 2, Pixel::color(Color::Hex(0xffeeaa)));
251        let ppm = "P3
2522 3
253255
254255 238 170
2550 0 0
2560 0 0
257255 238 170
2580 0 0
259255 238 170
260";
261        assert_eq!(paper.ppm(), ppm);
262    }
263}