I'm in the process of familiarizing myself with Rust. In order to get some practice, I decided to make a program that generates images of the Mandelbrot set.
The function I use to generate the image is included below. I'm specifically interested in feedback regarding my use (or lack thereof) of Rust idioms, as well as my interaction with the borrowing system. Notice that I had to clone the PathBuf
as I have two uses of it, and the call to save
borrows the value. Of course, however, any and all feedback is appreciated as well.
use image::{ImageBuffer, Rgb};
use num::Complex;
use std::path::PathBuf;
pub fn generate_image(width: u32, height: u32, iterations: u32, zoom: f32, out: PathBuf) {
let to_imaginary_domain = |x: u32, y: u32| -> (f32, f32) {
let re: f32 = x as f32 - width as f32 / 2.0;
let im: f32 = y as f32 - height as f32 / 2.0;
(re / zoom, im / zoom)
};
println!("Generating {} x {} image of the Mandelbrot set...", width, height);
let img = ImageBuffer::from_fn(width, height, |px, py| {
let (x, y) = to_imaginary_domain(px, py);
let c = Complex::<f32> { re: x, im: y };
let mut z = Complex::<f32> { re: 0.0, im: 0.0 };
for _i in 0..iterations {
z = z * z + c;
if z.norm() >= 2.0 {
return Rgb::<u8>([0x00, 0x00, 0x00]);
}
}
Rgb::<u8>([0xFF, 0xFF, 0xFF])
});
match img.save(out.clone()) {
Ok(_) => {
println!("Successfully saved image to {:#?}.", out.as_os_str());
},
Err(error) => {
panic!("Failed to save the image: {:#?}", error);
}
};
}
1 Answer 1
Disclaimer: I'm not an experienced Rust developer.
Note for other reviewers/people who want to test the code
Here are the dependencies you can use:
[dependencies]
image = "0.24.4"
num = "0.4.0"
and here are parameters that worked fine for me:
- width: 1000
- height: 1000
- iterations: 100
- zoom: 300
Clippy
clippy is a really nice tool to catch mistakes and improve your code.
In your case, there is not much to say:
- a few things about integer types (but you are a bit stuck with the
ImageBuffer::from_fn
types) - a few things about Path and PathBuffer which may answer your initial question.
I ended up with:
/// # Panics
///
/// Will panic if image is not saved
pub fn generate_image(width: u32, height: u32, iterations: u32, zoom: f32, out: &Path) {
...
match img.save(out) {
Ok(_) => {
println!("Successfully saved image to {:#?}.", out.as_os_str());
}
Err(error) => {
panic!("Failed to save the image: {:#?}", error);
}
};
Splitting the logic in functions
Having a to_imaginary_domain
function is nice but it is a bit surprising to me that it does not return a complex number.
let to_imaginary_domain = |x: u32, y: u32| -> Complex<f32> {
let re: f32 = x as f32 - width as f32 / 2.0;
let im: f32 = y as f32 - height as f32 / 2.0;
Complex::<f32> {
re: re / zoom,
im: im / zoom,
}
};
The mathematical operation probably deserves to be in a dedicated function as well.
#[must_use]
pub fn mandelbrot_func_diverges(c: Complex<f32>, iterations: u32) -> bool {
let mut z = Complex::<f32> { re: 0.0, im: 0.0 };
for _i in 0..iterations {
z = z * z + c;
if z.norm() >= 2.0 {
return true;
}
}
false
}
Then, in the from_fn
call, we just have:
let img = ImageBuffer::from_fn(width, height, |px, py| {
if mandelbrot_func_diverges(to_imaginary_domain(px, py), iterations) {
Rgb::<u8>([0x00, 0x00, 0x00])
} else {
Rgb::<u8>([0xFF, 0xFF, 0xFF])
}
});
If it was for me, I'd also rewrite the generate_image
so that it returns the ImageBuffer instead of dealing with saving it, but it makes things slightly more verbose.
#[must_use]
pub fn generate_image(
width: u32,
height: u32,
iterations: u32,
zoom: f32,
) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
More ideas
In order to make the output somehow better:
you could center the image around a different point
you could get the number of iterations before divergence and use this number to get a gradient of color