I'm learning Rust, and as-is tradition, I'm starting out with a Mandelbrot Set explorer as my first project (although, it just produces images so far). When run, it just creates and writes an image to the CWD. The coordinates and coloring function that are hardcoded produce my avatar. Eventually, there will be a GUI using Druid that allows changing these at runtime, but one step at a time.
text.rs
is technically dead code, but I figured I'd include it because it works when hooked up. It produces ASCII images of the Set.
This is the first large chunk of code I've written in Rust, and would like feedback on anything. Not really anything in particular yet. I just want to make sure I'm not doing any bad habits early on. Am I doing it correctly? It feels kind of awkward to set up. Speed will eventually be a concern, so if I'm doing anything wonky that will bite me in terms of performance later on, I'd like to know about that as well.
Actually, I'd like comments on char_for_iterations
if possible. It looks off to me. It requires tons of casting, and then also requires as_bytes
which seems to be O(1), but I can't find anything concrete saying that. That function gave me a really hard time for some reason.
I'll post the code inline below, but it's also here in case that's easier to read.
main.rs
mod ui;
mod logic;
fn main() {
let width = 7000;
let height = width * 2/3;
let start = std::time::Instant::now();
ui::image::draw_area("mandelbrot.png", 0.3539265474695936, 0.3607889277625950, 0.3531315391015801, 0.3599939193945812, width, height, 200, 2);
println!("Elapsed: {:?}", start.elapsed());
}
logic.rs
pub mod mandelbrot_iteration;
logic/mandelbrot_iteration.rs
use num::complex::Complex;
pub type ComplexPoint = Complex<f64>;
fn mandelbrot_iteration(initial: ComplexPoint, current: ComplexPoint) -> ComplexPoint {
let current_sq = current * current;
return Complex {
re: current_sq.re + initial.re,
im: current_sq.im + initial.im,
};
}
fn is_under_limit(current: ComplexPoint, infinity_limit: u32) -> bool {
let real_sq = current.re * current.re;
let imag_sq = current.im * current.im;
return (real_sq + imag_sq) <= (infinity_limit * infinity_limit) as f64;
}
pub fn test_point(initial: ComplexPoint, max_iterations: u32, infinity_limit: u32) -> u32 {
let mut current = initial;
let mut i = 0;
while i < max_iterations {
if is_under_limit(current, infinity_limit) {
current = mandelbrot_iteration(initial, current);
} else {
break;
}
i += 1;
}
return i;
}
/// Callback arguments: real, imag, display_x, display_y, iters
pub fn test_area
<F: FnMut(f64, f64, u32, u32, u32) -> ()>
(
real_min: f64,
real_max: f64,
imag_min: f64,
imag_max: f64,
display_width: u32,
display_height: u32,
max_iterations: u32,
infinity_limit: u32,
mut point_callback: F,
) -> () {
let real_length = real_max - real_min;
let imag_length = imag_max - imag_min;
let real_step = real_length / display_width as f64;
let imag_step = imag_length / display_height as f64;
for display_y in 0..display_height {
let imag = imag_min + (display_y as f64) * imag_step;
for display_x in 0..display_width {
let real = real_min + (display_x as f64) * real_step;
let c = Complex::new(real, imag);
let iters = test_point(c, max_iterations, infinity_limit);
point_callback(real, imag, display_x, display_y, iters);
}
}
}
ui.rs
pub mod text;
pub mod image;
ui/image.rs
use image::{ImageBuffer, Rgb};
use num::{clamp, Num};
use crate::logic::mandelbrot_iteration;
fn wrap<N: Num + Copy>(n: N, min_n: N, max_n: N) -> N {
let limit = max_n - min_n;
return (n % limit) + min_n;
}
fn color_f(real: f64, imag: f64, iters: u32) -> Rgb<u8> {
let f_iters = iters as f64;
let red = clamp(real * f_iters * 6.0, 0.0, 255.0);
let green = clamp(imag * f_iters * 3.0, 0.0, 255.0);
let blue = clamp(f_iters * f_iters * 0.01, 0.0, 255.0);
return Rgb([red as u8, green as u8, blue as u8]);
}
pub fn draw_area(file_path: &str, real_min: f64, real_max: f64, imag_min: f64, imag_max: f64, display_width: u32, display_height: u32, max_iterations: u32, infinity_limit: u32) -> () {
let mut buffer = ImageBuffer::new(display_width, display_height);
let total_pixels = display_width * display_height;
let report_every = total_pixels as u32 / 10;
let mut current_pixel = 0;
mandelbrot_iteration::test_area(real_min, real_max, imag_min, imag_max, display_width, display_height, max_iterations, infinity_limit, |real, imag, x, y, iters | {
let pixel = buffer.get_pixel_mut(x, y);
*pixel = color_f(real, imag, iters);
current_pixel += 1;
if current_pixel % report_every == 0 {
let perc = current_pixel as f32 / total_pixels as f32;
println!("Progress: {}/{} ({}%)", current_pixel, total_pixels, perc * 100.0);
}
});
match buffer.save(file_path) {
Ok(_) => println!("Saved successfully"),
Err(err) => println!("Error: {}", err),
};
}
ui/text.rs
use crate::logic::mandelbrot_iteration;
// Looks better with less chars for some reason
const DENSITY_SORTED_CHARS: &str = " .-:;~*\\/i({vx?@";
fn char_for_iterations(n_iterations: u32, max_iterations: u32) -> char {
let density_perc = n_iterations as f32 / (max_iterations + 1) as f32;
let index = ((DENSITY_SORTED_CHARS.len()) as f32 * density_perc) as usize;
return DENSITY_SORTED_CHARS.as_bytes()[index] as char;
}
pub fn draw_area(real_min: f64, real_max: f64, imag_min: f64, imag_max: f64, display_width: u32, display_height: u32, max_iterations: u32, infinity_limit: u32) -> String {
let mut result = String::new();
mandelbrot_iteration::test_area(real_min, real_max, imag_min, imag_max, display_width, display_height, max_iterations, infinity_limit, | _real, _imag, x, _y, iters | {
result.push(char_for_iterations(iters, max_iterations));
if x >= display_width - 1 {
result.push('\n');
}
});
return result;
}