dcc_lsystem/
image_renderer.rs

1use crate::dcc_lsystem::LSystem;
2use crate::image::{draw_line_mut, fill_mut};
3use crate::renderer::{Renderer, TurtleRenderer};
4use crate::turtle::TurtleContainer;
5use gifski::progress::{NoProgress, ProgressReporter};
6use gifski::{CatResult, Collector, Repeat};
7use image::{ImageBuffer, Rgb};
8use pbr::ProgressBar;
9use std::fs::File;
10use std::io::Stdout;
11use std::path::PathBuf;
12use std::thread;
13use std::time::Duration;
14
15pub struct ImageRendererOptionsBuilder {
16    padding: Option<u32>,
17    thickness: Option<f32>,
18    fill_color: Option<Rgb<u8>>,
19    line_color: Option<Rgb<u8>>,
20}
21
22impl ImageRendererOptionsBuilder {
23    pub fn new() -> Self {
24        // TODO: think up some sensible options for these variables
25        // so we don't end up panicking by default
26        Self {
27            padding: None,
28            thickness: None,
29            fill_color: None,
30            line_color: None,
31        }
32    }
33
34    pub fn padding(&mut self, padding: u32) -> &mut Self {
35        self.padding = Some(padding);
36        self
37    }
38
39    pub fn thickness(&mut self, thickness: f32) -> &mut Self {
40        self.thickness = Some(thickness);
41        self
42    }
43
44    pub fn fill_color(&mut self, fill_color: Rgb<u8>) -> &mut Self {
45        self.fill_color = Some(fill_color);
46        self
47    }
48
49    pub fn line_color(&mut self, line_color: Rgb<u8>) -> &mut Self {
50        self.line_color = Some(line_color);
51        self
52    }
53
54    pub fn build(&mut self) -> ImageRendererOptions {
55        ImageRendererOptions {
56            padding: self.padding.unwrap(),
57            thickness: self.thickness.unwrap(),
58            fill_color: self.fill_color.unwrap(),
59            line_color: self.line_color.unwrap(),
60        }
61    }
62}
63
64impl Default for ImageRendererOptionsBuilder {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70pub struct ImageRendererOptions {
71    padding: u32,
72    thickness: f32,
73    fill_color: Rgb<u8>,
74    line_color: Rgb<u8>,
75}
76
77impl ImageRendererOptions {
78    pub fn padding(&self) -> u32 {
79        self.padding
80    }
81
82    pub fn thickness(&self) -> f32 {
83        self.thickness
84    }
85
86    pub fn fill_color(&self) -> Rgb<u8> {
87        self.fill_color
88    }
89
90    pub fn line_color(&self) -> Rgb<u8> {
91        self.line_color
92    }
93}
94
95pub struct VideoRendererOptionsBuilder {
96    filename: Option<String>,
97    fps: Option<usize>,
98    skip_by: Option<usize>,
99    padding: Option<u32>,
100    thickness: Option<f32>,
101    fill_color: Option<Rgb<u8>>,
102    line_color: Option<Rgb<u8>>,
103    progress_bar: Option<bool>,
104}
105
106impl VideoRendererOptionsBuilder {
107    pub fn new() -> Self {
108        // TODO: think up some sensible options for these variables
109        // so we don't end up panicking by default
110        Self {
111            filename: Some(String::from("render.gif")),
112            fps: Some(20),
113            skip_by: Some(0),
114            padding: None,
115            thickness: None,
116            fill_color: None,
117            line_color: None,
118            progress_bar: Some(false),
119        }
120    }
121
122    pub fn filename<T: Into<String>>(&mut self, filename: T) -> &mut Self {
123        self.filename = Some(filename.into());
124        self
125    }
126
127    pub fn fps(&mut self, fps: usize) -> &mut Self {
128        self.fps = Some(fps);
129        self
130    }
131
132    pub fn skip_by(&mut self, skip_by: usize) -> &mut Self {
133        self.skip_by = Some(skip_by);
134        self
135    }
136
137    pub fn padding(&mut self, padding: u32) -> &mut Self {
138        self.padding = Some(padding);
139        self
140    }
141
142    pub fn thickness(&mut self, thickness: f32) -> &mut Self {
143        self.thickness = Some(thickness);
144        self
145    }
146
147    pub fn fill_color(&mut self, fill_color: Rgb<u8>) -> &mut Self {
148        self.fill_color = Some(fill_color);
149        self
150    }
151
152    pub fn line_color(&mut self, line_color: Rgb<u8>) -> &mut Self {
153        self.line_color = Some(line_color);
154        self
155    }
156
157    pub fn progress_bar(&mut self, progress_bar: bool) -> &mut Self {
158        self.progress_bar = Some(progress_bar);
159        self
160    }
161
162    pub fn build(&mut self) -> VideoRendererOptions {
163        VideoRendererOptions {
164            filename: self.filename.as_ref().unwrap().clone(),
165            fps: self.fps.unwrap(),
166            skip_by: self.skip_by.unwrap(),
167            padding: self.padding.unwrap(),
168            thickness: self.thickness.unwrap(),
169            fill_color: self.fill_color.unwrap(),
170            line_color: self.line_color.unwrap(),
171            progress_bar: self.progress_bar.unwrap(),
172        }
173    }
174}
175
176impl Default for VideoRendererOptionsBuilder {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182pub struct VideoRendererOptions {
183    filename: String,
184    fps: usize,
185    skip_by: usize,
186    padding: u32,
187    thickness: f32,
188    fill_color: Rgb<u8>,
189    line_color: Rgb<u8>,
190    progress_bar: bool,
191}
192
193impl VideoRendererOptions {
194    pub fn filename(&self) -> &String {
195        &self.filename
196    }
197
198    pub fn fps(&self) -> usize {
199        self.fps
200    }
201
202    pub fn skip_by(&self) -> usize {
203        self.skip_by
204    }
205
206    pub fn padding(&self) -> u32 {
207        self.padding
208    }
209
210    pub fn thickness(&self) -> f32 {
211        self.thickness
212    }
213
214    pub fn fill_color(&self) -> Rgb<u8> {
215        self.fill_color
216    }
217
218    pub fn line_color(&self) -> Rgb<u8> {
219        self.line_color
220    }
221
222    pub fn progress_bar(&self) -> bool {
223        self.progress_bar
224    }
225}
226
227struct Lodecoder {
228    frames: Vec<PathBuf>,
229    fps: usize,
230}
231
232impl Lodecoder {
233    pub fn new(frames: Vec<PathBuf>, fps: usize) -> Self {
234        Self { frames, fps }
235    }
236
237    fn total_frames(&self) -> u64 {
238        self.frames.len() as u64
239    }
240
241    fn collect(&mut self, mut dest: Collector) -> CatResult<()> {
242        for (i, frame) in self.frames.drain(..).enumerate() {
243            let delay = ((i + 1) * 100 / self.fps) - (i * 100 / self.fps); // See telecine/pulldown.
244            dest.add_frame_png_file(i, frame, delay as f64)?;
245        }
246        Ok(())
247    }
248}
249
250impl<Q: TurtleContainer> Renderer<VideoRendererOptions> for TurtleRenderer<Q> {
251    type Output = ();
252
253    // ffmpeg -r 24 -f image2 -i frame-%8d.png -vcodec libx264 -crf 20 -pix_fmt yuv420p output.mp4
254    fn render(mut self, system: &LSystem, options: &VideoRendererOptions) -> Self::Output {
255        // Setup our state machine based on the system state
256        self.compute(system.get_state());
257
258        let (turtle_width, turtle_height, min_x, min_y) = self.state.inner().inner().bounds();
259
260        // We add some padding to the width reported by our turtle to make
261        // our final image look a little nicer.
262        let width = 2 * options.padding + turtle_width;
263        let height = 2 * options.padding + turtle_height;
264
265        let mut buffer = ImageBuffer::new(width, height);
266        fill_mut(&mut buffer, options.fill_color);
267
268        let mut files = Vec::new();
269
270        // Helper functions for converting between the coordinate system used
271        // by the image crate and our coordinate system.  These functions also
272        // take care of the padding for us.
273        let xp = |x: i32| -> u32 { (x - min_x + options.padding as i32) as u32 };
274
275        let yp = |y: i32| -> u32 {
276            (i64::from(height) - i64::from(y - min_y + options.padding as i32)) as u32
277        };
278
279        let mut absolute_frame_counter = 0;
280        let total_frame_counter = self.state.inner().inner().lines().len();
281
282        let mut pb = if options.progress_bar {
283            Some(ProgressBar::new(total_frame_counter as u64))
284        } else {
285            None
286        };
287
288        let dir = tempfile::tempdir().unwrap();
289        let mut workers = Vec::new();
290
291        for (frame_counter, (x1, y1, x2, y2)) in
292            self.state.inner().inner().lines().iter().enumerate()
293        {
294            draw_line_mut(
295                &mut buffer,
296                xp(*x1),
297                yp(*y1),
298                xp(*x2),
299                yp(*y2),
300                options.thickness,
301                options.line_color,
302            );
303
304            if options.progress_bar {
305                pb.as_mut().unwrap().inc();
306            }
307
308            if options.skip_by == 0 || frame_counter % options.skip_by == 0 {
309                // TODO: estimate number of digits we need (for correct padding of filenames)
310                // for the moment we just use 8.
311                let filename = dir
312                    .path()
313                    .join(format!("frame-{:08}.png", absolute_frame_counter));
314                absolute_frame_counter += 1;
315                files.push(filename.clone());
316
317                let local_buffer = buffer.clone();
318
319                // spawn a thread to do this work
320                workers.push(std::thread::spawn(move || {
321                    local_buffer.save(filename).unwrap();
322                }));
323            }
324        }
325
326        for child in workers {
327            child.join().expect("Failure");
328            if options.progress_bar {
329                pb.as_mut().unwrap().inc();
330            }
331        }
332
333        if options.progress_bar {
334            pb.unwrap().finish();
335        }
336
337        let settings = gifski::Settings {
338            width: None,
339            height: None,
340            quality: 100,
341            fast: false,
342            repeat: Repeat::Infinite,
343        };
344
345        let mut decoder = Box::new(Lodecoder::new(files, options.fps));
346
347        let mut progress: Box<dyn ProgressReporter> = if !options.progress_bar {
348            Box::new(NoProgress {})
349        } else {
350            let mut pb: ProgressBar<Stdout> = ProgressBar::new(decoder.total_frames());
351            pb.set_max_refresh_rate(Some(Duration::from_millis(250)));
352            Box::new(pb)
353        };
354
355        let (collector, writer) = gifski::new(settings).expect("Failed to initialise gifski");
356        let decode_thread = thread::spawn(move || decoder.collect(collector));
357
358        let file = File::create(&options.filename).expect("Couldn't open output file");
359        writer.write(file, &mut *progress).expect("Failed to write");
360        let _ = decode_thread.join().expect("Failed to decode");
361        progress.done(&format!("Output written to {}", options.filename));
362
363        // Now delete the temporary files
364        drop(dir);
365    }
366}
367
368impl<Q: TurtleContainer> Renderer<ImageRendererOptions> for TurtleRenderer<Q> {
369    type Output = ImageBuffer<Rgb<u8>, Vec<u8>>;
370
371    fn render(mut self, system: &LSystem, options: &ImageRendererOptions) -> Self::Output {
372        // Setup our state machine based on the LSystem state
373        self.compute(system.get_state());
374
375        let (turtle_width, turtle_height, min_x, min_y) = self.state.inner().inner().bounds();
376        let width = 2 * options.padding + turtle_width;
377        let height = 2 * options.padding + turtle_height;
378
379        let mut buffer = ImageBuffer::new(width, height);
380        fill_mut(&mut buffer, options.fill_color);
381
382        // Helper functions for converting between the coordinate system used
383        // by the image crate and our coordinate system.  These functions also
384        // take care of the padding for us.
385        let xp = |x: i32| -> u32 { (x - min_x + options.padding as i32) as u32 };
386
387        let yp = |y: i32| -> u32 {
388            (i64::from(height) - i64::from(y - min_y + options.padding as i32)) as u32
389        };
390
391        for (x1, y1, x2, y2) in self.state.inner().inner().lines() {
392            draw_line_mut(
393                &mut buffer,
394                xp(*x1),
395                yp(*y1),
396                xp(*x2),
397                yp(*y2),
398                options.thickness,
399                options.line_color,
400            );
401        }
402
403        buffer
404    }
405}