diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..7a79ec8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "swellaby.rust-pack" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 25b152b..233a38f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,8 @@ categories = ["mathematics", "rendering", "visualization"] [dependencies] angular-units = "0.2.4" chrono = "0.4.26" -minifb = "0.24.0" +lazy_static = "1.4.0" +minifb = "0.25.0" num = "0.4.0" num_cpus = "1.15.0" png = "0.17.9" diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 5816d0f..3921ed6 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -4,7 +4,7 @@ - [x] Create a PixelBuffer struct. This struct should contain its associated PixelPlane struct, and should store the color of each pixel in 8 bits per color, so 8 bits for red, green and blue. This is also called true color (24-bit). The PixelBuffer should be the input for rendering functions. - [ ] Create a TrueColor struct, containing a 24-bit true color, with 8-bits for the R, G and B channels. The struct should contain methods for conversion to HSV and from HSV. The TrueColor struct should be constructable from a u32 representing a true color. - [x] Create a PixelPlane struct. This struct should contain a width and height in pixels, and an aspect ratio. -- [x] Create a MandelbrotSet struct. This struct should contain max_iterations, orbit radius, and an iterate function. +- [x] Create a MandelbrotFunction struct. This struct should contain max_iterations, orbit radius, and an iterate function. - [ ] Create a logging function, with log levels DEBUG, INFO, ERROR, and a commandline argument specifying the level, default should be INFO - [ ] Create a struct to combine KEY, description, action, to make it easy to display what keys are doing in the future, self-documenting - [x] Save the current Mandelbrot view with the 'S' key to a .png file, the view is stored in the metadata of the image diff --git a/benches/bench.rs b/benches/bench.rs index 5a0a3ee..6d3bc3c 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -2,7 +2,10 @@ extern crate test; -use mandelbrot::{mandelbrot_set::MandelbrotSet, complex::Complex, pixel_buffer::{PixelBuffer, pixel_plane::PixelPlane}, complex_plane::ComplexPlane, coloring::TrueColor, rendering}; +use mandelbrot::{ + model::complex::Complex, model::complex_plane::ComplexPlane, model::mandelbrot_function::MandelbrotFunction, + model::pixel_buffer::PixelBuffer, model::pixel_plane::PixelPlane, model::rendering, view::coloring::TrueColor, +}; use test::Bencher; //Mandelbrot set parameters @@ -15,12 +18,11 @@ static POINT_INSIDE_MANDELBROT_SET: Complex = Complex::new(-0.3, 0.0); static WIDTH: usize = 1280; static HEIGHT: usize = 720; - #[bench] -///Run MandelbrotSet::iterate on a point inside the Mandelbrot set, with typical Mandelbrot set parameters, 10k max_iterations, orbit_radius of 2.0 +///Run MandelbrotFunction::iterate on a point inside the Mandelbrot set, with typical Mandelbrot set parameters, 10k max_iterations, orbit_radius of 2.0 fn bench_mandelbrot_set_iterate(b: &mut Bencher) { //Setup - let m: MandelbrotSet = MandelbrotSet::new(HIGH_MAX_ITERATIONS, ORBIT_RADIUS); + let m: MandelbrotFunction = MandelbrotFunction::new(HIGH_MAX_ITERATIONS, ORBIT_RADIUS); //Benchmark b.iter(|| { let _ = m.iterate(&POINT_INSIDE_MANDELBROT_SET); @@ -30,14 +32,15 @@ fn bench_mandelbrot_set_iterate(b: &mut Bencher) { #[bench] ///Renders a 1280x720 1x SSAA image of the Mandelbrot set default view using Bernstein polynomal coloring, 1k max_iterations fn bench_render_mandelbrot_set_default_view_720p_1x_ssaa(b: &mut Bencher) { + //TODO //Setup let mut p: PixelBuffer = PixelBuffer::new(PixelPlane::new(WIDTH, HEIGHT)); let c: ComplexPlane = ComplexPlane::new(WIDTH, HEIGHT); - let m: MandelbrotSet = MandelbrotSet::new(DEFAULT_MAX_ITERATIONS, ORBIT_RADIUS); + let m: MandelbrotFunction = MandelbrotFunction::new(DEFAULT_MAX_ITERATIONS, ORBIT_RADIUS); let supersampling_amount = 1; let coloring_function = TrueColor::new_from_bernstein_polynomials; //Benchmark b.iter(|| { rendering::render_complex_plane_into_buffer(&mut p, &c, &m, supersampling_amount, coloring_function); }) -} \ No newline at end of file +} diff --git a/docs/MVC.md b/docs/MVC.md new file mode 100644 index 0000000..393062e --- /dev/null +++ b/docs/MVC.md @@ -0,0 +1,33 @@ +# Model-View-Controller design pattern explained +The Model-View-Controller (MVC) design pattern is a widely used architectural pattern in software development that separates an application into three interconnected components: Model, View, and Controller. This separation promotes modularity, maintainability, and reusability of code, making it easier to develop and maintain complex software systems. MVC is commonly used in web and desktop application development, but its principles can be applied to various domains. + +Here's an overview of each component in the MVC pattern: + +1. **Model:** + + * The Model represents the application's data and business logic. It manages the data, enforces rules and constraints, and interacts with the database or any other data source. + * It is responsible for maintaining the application's state, processing data, and notifying the View and Controller when changes occur. + * In a typical implementation, the Model is unaware of the user interface and operates independently of it. +2. **View:** + + * The View is responsible for rendering the user interface and presenting data to the user. It displays information from the Model and sends user input (e.g., clicks, keystrokes) to the Controller. + * It is essentially the presentation layer, responsible for presenting data in a format that's understandable and visually appealing to users. + * In an ideal MVC architecture, the View is passive, meaning it doesn't contain application logic. It should only focus on displaying data and forwarding user input to the Controller. +3. **Controller**: + + * The Controller acts as an intermediary between the Model and the View. It receives user input from the View, processes it, and interacts with the Model to update the application's state. + * It handles user requests, performs necessary actions, and updates the View to reflect any changes in the Model. + * The Controller is responsible for implementing the application's logic and flow, making decisions based on user input and the state of the Model. + + +The key principles and advantages of the MVC design pattern include: + +* **Separation of Concerns:** MVC enforces a clear separation between data management (Model), user interface (View), and application logic (Controller). This separation makes it easier to develop and maintain each component independently. + +* **Modularity and Reusability:** Since each component has a well-defined role, it's easier to reuse and replace parts of the system without affecting the others. This leads to more modular and maintainable code. + +* **Testability:** The separation of concerns allows for easier testing of individual components. You can test the Model, View, and Controller independently, which helps in identifying and fixing issues more efficiently. + +* **Scalability:** MVC can be scaled by adding new Views or Controllers without significantly modifying the existing components. This makes it adaptable to changing requirements and evolving applications. + +MVC has served as a foundation for many other architectural patterns and variations, such as Model-View-Presenter (MVP) and Model-View-ViewModel (MVVM), which adapt the core concepts to different development environments and technologies. \ No newline at end of file diff --git a/icons/rust.ico b/icons/rust.ico new file mode 100644 index 0000000..8a8f98d Binary files /dev/null and b/icons/rust.ico differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..93470b6 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 140 \ No newline at end of file diff --git a/src/config.rs b/src/controller/config.rs similarity index 63% rename from src/config.rs rename to src/controller/config.rs index f24a9bd..ed64adf 100644 --- a/src/config.rs +++ b/src/controller/config.rs @@ -1,4 +1,7 @@ -use std::{str::FromStr, fmt::{Display, self}}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; //Argument default values static WIDTH: usize = 1200; @@ -8,24 +11,36 @@ static ORBIT_RADIUS: f64 = 2.0; static SUPERSAMPLING_AMOUNT: u8 = 1; static WINDOW_SCALE: f64 = 1.0; +#[derive(Clone, Copy)] pub struct Config { // Window dimensions in pixels pub window_width: usize, pub window_height: usize, // Mandelbrot set parameters pub max_iterations: u32, - pub orbit_radius: f64, //If z remains within the orbit_radius in max_iterations, we assume c does not tend to infinity + pub orbit_radius: f64, //If z remains within the orbit_radius in max_iterations, we assume c does not tend to infinity // Rendering parameters pub supersampling_amount: u8, //Window scaling factor pub window_scale: f64, //Scaled window dimensions in pixels (used in images) pub image_width: usize, - pub image_height: usize + pub image_height: usize, } - impl Config { + pub fn new() -> Config { + Config { + window_width: WIDTH, + window_height: HEIGHT, + max_iterations: MAX_ITERATIONS, + orbit_radius: ORBIT_RADIUS, + supersampling_amount: SUPERSAMPLING_AMOUNT, + window_scale: WINDOW_SCALE, + image_width: WIDTH, + image_height: HEIGHT, + } + } /// Parse the command line arguments from e.g. `env::args` in the following format /// ```ignore /// cargo run -- width height max_iterations @@ -36,7 +51,7 @@ impl Config { args.next(); //Skip the first argument as it is the name of the executable //First argument - let image_width = Config::parse_argument("width", args.next(), WIDTH)?; + let image_width = Config::parse_argument("width", args.next(), WIDTH)?; //Second argument let image_height = Config::parse_argument("height", args.next(), HEIGHT)?; @@ -53,15 +68,26 @@ impl Config { let window_width = (f64::from(image_width as u32) * window_scale) as usize; let window_height = (f64::from(image_height as u32) * window_scale) as usize; - Ok(Config {window_width, window_height, max_iterations, orbit_radius: ORBIT_RADIUS, supersampling_amount, window_scale, image_width, image_height}) + Ok(Config { + window_width, + window_height, + max_iterations, + orbit_radius: ORBIT_RADIUS, + supersampling_amount, + window_scale, + image_width, + image_height, + }) } ///Parses an argument to a T value if possible, returns an error if not. Returns default if argument is None
///If Some(arg) == "-", return default /// # Errors /// Return an Error if the given argument cannot be parsed to a T type - pub fn parse_argument(name: &str, argument: Option, default: T) -> Result - where ::Err: Display{ + pub fn parse_argument(name: &str, argument: Option, default: T) -> Result + where + ::Err: Display, + { match argument { Some(arg) => { if arg == "-" { @@ -72,8 +98,8 @@ impl Config { Ok(val) => Ok(val), Err(err) => Err(err.to_string() + &format!(" for {} argument", name)), } - }, - None => { + } + None => { Config::print_no_argument_given(name, &default); Ok(default) } @@ -86,7 +112,17 @@ impl Config { } impl fmt::Debug for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { //TODO: Improve debug printing format legibility - f.debug_struct("Config").field("window_width", &self.window_width).field("window_height", &self.window_height).field("max_iterations", &self.max_iterations).field("orbit_radius", &self.orbit_radius).field("supersampling_amount", &self.supersampling_amount).field("window_scale", &self.window_scale).field("image_width", &self.image_width).field("image_height", &self.image_height).finish() + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + //TODO: Improve debug printing format legibility + f.debug_struct("Config") + .field("window_width", &self.window_width) + .field("window_height", &self.window_height) + .field("max_iterations", &self.max_iterations) + .field("orbit_radius", &self.orbit_radius) + .field("supersampling_amount", &self.supersampling_amount) + .field("window_scale", &self.window_scale) + .field("image_width", &self.image_width) + .field("image_height", &self.image_height) + .finish() } } diff --git a/src/controller/interaction_variables.rs b/src/controller/interaction_variables.rs new file mode 100644 index 0000000..2a28eaf --- /dev/null +++ b/src/controller/interaction_variables.rs @@ -0,0 +1,57 @@ +pub struct InteractionVariables { + ///Variable determining the amount of rows and columns are translated by pressing the 4 arrow keys + pub translation_amount: u8, + ///Variable denoting the user scaling speed; the lower this value, the more aggressive the zooming will become + pub scale_denominator: f64, + pub scale_numerator: f64, +} + +impl InteractionVariables { + pub fn new(translation_amount: u8, scale_numerator: f64, scale_denominator: f64) -> InteractionVariables { + InteractionVariables { + translation_amount, + scale_denominator, + scale_numerator, + } + } + + pub fn scaling_factor(&self) -> f64 { + self.scale_numerator / self.scale_denominator + } + + pub fn inverse_scaling_factor(&self) -> f64 { + self.scale_denominator / self.scale_numerator + } + + pub fn increment_translation_amount(&mut self) { + self.translation_amount = self.translation_amount.saturating_add(1); + } + + pub fn decrement_translation_amount(&mut self) { + if self.translation_amount > 1 { + self.translation_amount -= 1; + } + } + + pub fn increment_scale_numerator(&mut self) { + if self.scale_numerator < 9.0 { + self.scale_numerator += 1.0; + } + } + + pub fn decrement_scale_numerator(&mut self) { + if self.scale_numerator > 1.0 { + self.scale_numerator -= 1.0; + } + } +} + +impl Default for InteractionVariables { + fn default() -> Self { + InteractionVariables { + translation_amount: 10, + scale_numerator: 9.0, + scale_denominator: 10.0, + } + } +} diff --git a/src/controller/minifb_controller.rs b/src/controller/minifb_controller.rs new file mode 100644 index 0000000..da3c8d9 --- /dev/null +++ b/src/controller/minifb_controller.rs @@ -0,0 +1,264 @@ +use std::error::Error; + +use minifb::{Key, MouseButton, MouseMode, Window}; + +use crate::{ + controller::{minifb_mouse_click_recorder::MouseClickRecorder, user_input::pick_option}, + model::{ + coloring::TrueColor, + complex_plane::View, + rendering::{self, render, set_view}, + }, + view::{image, minifb_mandelbrot_view::MandelbrotView}, + MandelbrotModel, VIEW_0, VIEW_1, VIEW_2, VIEW_3, VIEW_4, VIEW_5, VIEW_6, VIEW_7, VIEW_8, VIEW_9, +}; + +use super::{minifb_key_bindings::KeyBindings, user_input::ask}; + +// Handle any key events +pub fn handle_key_events(window: &Window, k: &KeyBindings) { + if let Some(key) = window.get_keys_pressed(minifb::KeyRepeat::No).first() { + print!("\n{{Key pressed: "); + k.print_key(key); + println!("}}"); + k.run(key); + let mut mandelbrot_model = MandelbrotModel::get_instance(); + match key { + Key::K => k.print(), + Key::A => { + mandelbrot_model.coloring_function = pick_option(&[ + ("HSV", TrueColor::new_from_hsv_colors), + ("Bernstein polynomials", TrueColor::new_from_bernstein_polynomials), + ]); + render(&mut mandelbrot_model); + } + _ => (), + } + println!(); + } +} + +pub fn handle_left_mouse_clicked(mandelbrot_model: &MandelbrotModel, x: f32, y: f32) { + println!("\n{{MouseButton::Left -> Info at ({x}, {y})}}"); + //let iterations = MandelbrotModel::get_instance().p.iterations_at_point(x as usize, y as usize, MandelbrotModel::get_instance().m.max_iterations); //TODO: fix this + let complex = mandelbrot_model.c.complex_from_pixel_plane(x.into(), y.into()); + println!("Complex: {:?}", complex); + //println!("iterations: {}", iterations); + println!(); +} + +pub fn handle_right_mouse_clicked(mandelbrot_model: &mut MandelbrotModel, x: f32, y: f32) { + println!("\n{{MouseButton::Right -> Move to ({x}, {y})}}"); + let new_center = mandelbrot_model.c.complex_from_pixel_plane(x.into(), y.into()); + println!("mandelbrot_model.c.center: {:?}", mandelbrot_model.c.center()); + println!("new_center: {:?}", new_center); + + rendering::translate_to_center_and_render_efficiently(mandelbrot_model, &new_center); + mandelbrot_model.c.print(); + println!(); +} + +pub fn handle_mouse_events(window: &Window) { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + static LEFT_MOUSE_RECORDER: MouseClickRecorder = MouseClickRecorder::new(MouseButton::Left); //Static variable with interior mutability to toggle mouse clicks; without such a variable, clicking the screen once would result in multiple actions + static RIGHT_MOUSE_RECORDER: MouseClickRecorder = MouseClickRecorder::new(MouseButton::Right); + + if let Some((x, y)) = window.get_mouse_pos(MouseMode::Discard) { + //Left mouse actions + if LEFT_MOUSE_RECORDER.was_clicked(window) { + handle_left_mouse_clicked(&mandelbrot_model, x, y); + } + + //Right mouse actions + if RIGHT_MOUSE_RECORDER.was_clicked(window) { + handle_right_mouse_clicked(&mut mandelbrot_model, x, y); + } + } +} + +pub fn initialize_keybindings(key_bindings: &mut KeyBindings) { + let empty_closure = || (); + key_bindings.add(Key::Up, "Move up translation_amount pixels", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let rows = mandelbrot_model.vars.translation_amount; + rendering::translate_and_render_efficiently(&mut mandelbrot_model, rows.into(), 0); + mandelbrot_model.c.print(); + }); + key_bindings.add(Key::Down, "Move down translation_amount pixels", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let rows = -i16::from(mandelbrot_model.vars.translation_amount); + rendering::translate_and_render_efficiently(&mut mandelbrot_model, rows, 0); + mandelbrot_model.c.print(); + }); + key_bindings.add(Key::Left, "Move left translation_amount pixels", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let columns = -i16::from(mandelbrot_model.vars.translation_amount); + rendering::translate_and_render_efficiently(&mut mandelbrot_model, 0, columns); + mandelbrot_model.c.print(); + }); + key_bindings.add(Key::Right, "Move right translation_amount pixels", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let columns = mandelbrot_model.vars.translation_amount.into(); + rendering::translate_and_render_efficiently(&mut mandelbrot_model, 0, columns); + mandelbrot_model.c.print(); + }); + key_bindings.add(Key::R, "Reset the Mandelbrot set view to the starting view", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.c.reset(); + render(&mut mandelbrot_model); + }); + key_bindings.add(Key::NumPadPlus, "Increment translation_amount", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.vars.increment_translation_amount(); + println!("translation_amount: {}", mandelbrot_model.vars.translation_amount); + }); + key_bindings.add(Key::NumPadMinus, "Decrement translation amount", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.vars.decrement_translation_amount(); + println!("translation_amount: {}", mandelbrot_model.vars.translation_amount); + }); + key_bindings.add(Key::NumPadAsterisk, "Increment scale_numerator", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.vars.increment_scale_numerator(); + println!( + "scale factor: {}/{}", + mandelbrot_model.vars.scale_numerator, mandelbrot_model.vars.scale_denominator + ); + }); + key_bindings.add(Key::NumPadSlash, "Decrement scale_numerator", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.vars.decrement_scale_numerator(); + println!( + "scale factor: {}/{}", + mandelbrot_model.vars.scale_numerator, mandelbrot_model.vars.scale_denominator + ); + }); + key_bindings.add(Key::LeftBracket, "Scale the view by scaling_factor, effectively zooming in", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let scaling_factor = mandelbrot_model.vars.scaling_factor(); + mandelbrot_model.c.scale(scaling_factor); + render(&mut mandelbrot_model); + }); + key_bindings.add( + Key::RightBracket, + "Scale the view by inverse_scaling_factor, effectively zooming out", + || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + let inverse_scaling_factor = mandelbrot_model.vars.inverse_scaling_factor(); + mandelbrot_model.c.scale(inverse_scaling_factor); + render(&mut mandelbrot_model); + }, + ); + key_bindings.add(Key::V, "Prints the current Mandelbrot set view; the center and scale", || { + let mandelbrot_model = MandelbrotModel::get_instance(); + println!( + "Center: {:?}, scale: {:?}", + mandelbrot_model.c.center(), + mandelbrot_model.c.get_scale() + ); + }); + key_bindings.add(Key::Key1, "Renders VIEW_1", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_1) + }); + key_bindings.add(Key::Key2, "Renders VIEW_2", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_2) + }); + key_bindings.add(Key::Key3, "Renders VIEW_3", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_3) + }); + key_bindings.add(Key::Key4, "Renders VIEW_4", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_4) + }); + key_bindings.add(Key::Key5, "Renders VIEW_5", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_5) + }); + key_bindings.add(Key::Key6, "Renders VIEW_6", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_6) + }); + key_bindings.add(Key::Key7, "Renders VIEW_7", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_7) + }); + key_bindings.add(Key::Key8, "Renders VIEW_8", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_8) + }); + key_bindings.add(Key::Key9, "Renders VIEW_9", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_9) + }); + key_bindings.add(Key::Key0, "Renders VIEW_0", || { + set_view(&mut MandelbrotModel::get_instance(), &VIEW_0) + }); + key_bindings.add(Key::K, "Prints the keybindings", empty_closure); + key_bindings.add( + Key::S, + "Saves the current Mandelbrot set view as an image in the saved folder", + || { + let mandelbrot_model = MandelbrotModel::get_instance(); + image::save(&mandelbrot_model); + }, + ); + key_bindings.add(Key::I, "Manually input a Mandelbrot set view", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.c.set_view(&View::new(ask("x"), ask("y"), ask("scale"))); + render(&mut mandelbrot_model); + }); + key_bindings.add(Key::A, "Pick an algorithm to color the Mandelbrot set view", empty_closure); + key_bindings.add(Key::M, "Change the Mandelbrot set view max_iterations", || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.m.max_iterations = ask("max_iterations"); + render(&mut mandelbrot_model); + }); + key_bindings.add( + Key::O, + "Change the Mandelbrot set view color channel mapping, xyz -> RGB, where x,y,z ∈ {{'R','G','B'}} (case-insensitive)", + || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.p.color_channel_mapping = ask("color_channel_mapping"); + render(&mut mandelbrot_model); + }, + ); + key_bindings.add( + Key::Q, + "Change the window and image quality of the Mandelbrot set rendering by setting the SSAA multiplier, clamped from 1x to 64x", + || { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_model.config.supersampling_amount = ask::("supersampling_amount").clamp(1, 64); + render(&mut mandelbrot_model); + }, + ); + key_bindings.add( + Key::X, + "Change the image quality of the Mandelbrot set rendering by setting the SSAA multiplier, clamped from 1x to 64x", + empty_closure, + ); + key_bindings.add(Key::C, "Prints the configuration variables", || { + println!("{:?}", MandelbrotModel::get_instance().config); + }); +} + +pub fn run(mandelbrot_view: &mut MandelbrotView) -> Result<(), Box> { + let mut mandelbrot_model = MandelbrotModel::get_instance(); + //Initialize keybindings + let mut key_bindings: KeyBindings = KeyBindings::new(Vec::new()); //TODO: Make KeyBindings a singleton + initialize_keybindings(&mut key_bindings); + key_bindings.print(); + + println!("\nRendering Mandelbrot set starting view"); + render(&mut mandelbrot_model); + drop(mandelbrot_model); + + // Main loop + while mandelbrot_view.window.is_open() && !mandelbrot_view.window.is_key_down(Key::Escape) { + // Handle any window events + handle_key_events(&mandelbrot_view.window, &key_bindings); + + //Handle any mouse events + handle_mouse_events(&mandelbrot_view.window); + + // Update the window with the new buffer + let mandelbrot_model = MandelbrotModel::get_instance(); + mandelbrot_view.update(&mandelbrot_model); + } + + mandelbrot_view.exit(); + Ok(()) +} diff --git a/src/controller/minifb_key_bindings.rs b/src/controller/minifb_key_bindings.rs new file mode 100644 index 0000000..e47dba5 --- /dev/null +++ b/src/controller/minifb_key_bindings.rs @@ -0,0 +1,113 @@ +use std::fmt; + +use minifb::Key; + +static KEY_BINDING_DESCRIPTION_OFFSET: usize = 20; +static KEY_BINDING_SEPARATOR: &str = " -> "; + +//https://stackoverflow.com/questions/68066875/how-to-store-a-closure-inside-rust-struct +//https://stackoverflow.com/questions/65756096/how-can-i-store-a-closure-object-in-a-struct +pub struct KeyBinding { + pub key: Key, + pub description: &'static str, + action: Box, +} + +impl KeyBinding { + pub fn new(key: Key, description: &'static str, action: Box) -> KeyBinding { + KeyBinding { key, description, action } + } + + ///Run self.action + pub fn run(&self) { + (self.action)(); + } + + pub fn to_formatted_str(&self) -> String { + let mut key_binding_formatted = format!("{:?}", self); + while key_binding_formatted.contains(" ") { + key_binding_formatted = key_binding_formatted.replace(" ", " "); + } + key_binding_formatted + } +} + +impl fmt::Debug for KeyBinding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let key = format!("{:?}", &self.key); + let offset = KEY_BINDING_DESCRIPTION_OFFSET - key.len() - KEY_BINDING_SEPARATOR.len(); + write!(f, "{}{}{}{}", key, KEY_BINDING_SEPARATOR, " ".repeat(offset), &self.description) + } +} + +pub struct KeyBindings { + key_bindings: Vec, +} + +impl KeyBindings { + pub fn new(key_bindings: Vec) -> KeyBindings { + KeyBindings { key_bindings } + } + + ///Adds a `KeyBinding` to these `KeyBindings`, will remove any existing `KeyBinding` `x` where `x.key` == `key_action.key` + pub fn add_key(&mut self, key_action: KeyBinding) { + //Remove any KeyBinding x where x.key == key_action.key + self.key_bindings.retain(|x| x.key != key_action.key); + + self.key_bindings.push(key_action); + } + + ///Adds a `KeyBinding` to these `KeyBindings`, will remove any existing `KeyBinding` `x` where `x.key` == `key` + pub fn add(&mut self, key: Key, description: &'static str, action: F) { + self.add_key(KeyBinding::new(key, description, Box::new(action))); + } + + pub fn key_actions(&self) -> &Vec { + &self.key_bindings + } + + /// Prints all `KeyBinding`s in these `KeyBindings` to stdout + pub fn print(&self) { + println!("{:?}", self); + } + + pub fn print_key(&self, key: &Key) { + for key_action in &self.key_bindings { + if key_action.key == *key { + print!("{}", key_action.to_formatted_str()); + return; + } + } + //KeyBindings does not contain a KeyBinding x with x.key == key, unbound + print!("{:?}", key); + } + + pub fn run(&self, key: &Key) { + for key_binding in &self.key_bindings { + if key_binding.key == *key { + key_binding.run(); + break; + } + } + } +} + +impl fmt::Debug for KeyBindings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "KeyBindings {{")?; + for key_binding in &self.key_bindings { + writeln!(f, " {:?},", key_binding)?; + } + write!(f, "}}") + } +} + +impl Default for KeyBindings { + /// Define all your keybindings here + fn default() -> KeyBindings { + let mut key_bindings = KeyBindings::new(Vec::new()); + key_bindings.add(Key::A, "This is the A key", || println!("Action A")); + key_bindings.add(Key::B, "This is the B key", || println!("Action B")); + key_bindings + } +} diff --git a/src/controller/minifb_mouse_click_recorder.rs b/src/controller/minifb_mouse_click_recorder.rs new file mode 100644 index 0000000..8757051 --- /dev/null +++ b/src/controller/minifb_mouse_click_recorder.rs @@ -0,0 +1,34 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use minifb::{MouseButton, Window}; + +fn was_clicked(current: bool, previous: bool) -> bool { + current && !previous +} + +/////Mouse click recorder with interior mutability to toggle mouse clicks; +/// without such a (static function) variable, clicking the screen once would result in multiple actions +pub struct MouseClickRecorder { + mouse_button: MouseButton, + previous: AtomicBool, +} + +impl MouseClickRecorder { + pub const fn new(mouse_button: MouseButton) -> MouseClickRecorder { + MouseClickRecorder { + mouse_button, + previous: AtomicBool::new(false), + } + } + + ///Returns whether the `mouse_button` was clicked once + pub fn was_clicked(&self, window: &Window) -> bool { + let current = window.get_mouse_down(self.mouse_button); + let previous = self.previous.load(Ordering::Relaxed); + let result = was_clicked(current, previous); + if current != previous { + self.previous.store(current, Ordering::Relaxed) + } + result + } +} diff --git a/src/controller/mod.rs b/src/controller/mod.rs new file mode 100644 index 0000000..5192e4a --- /dev/null +++ b/src/controller/mod.rs @@ -0,0 +1,6 @@ +pub mod config; +pub mod interaction_variables; +pub mod minifb_controller; +pub mod minifb_key_bindings; +pub mod minifb_mouse_click_recorder; +pub mod user_input; diff --git a/src/user_input.rs b/src/controller/user_input.rs similarity index 68% rename from src/user_input.rs rename to src/controller/user_input.rs index 9f65d3b..9329b22 100644 --- a/src/user_input.rs +++ b/src/controller/user_input.rs @@ -1,24 +1,30 @@ -use std::{str::FromStr, fmt::Display, io::{self, Write}}; +use std::{ + fmt::Display, + io::{self, Write}, + str::FromStr, +}; /// Ask the user for `result` named `name` from stdin. If `result` can be parsed to a `T`, return `result`. In any other case, /// call `ask` again. /// # Panics /// If `io::stdout().flush().unwrap()` panics pub fn ask(name: &str) -> T - where ::Err: Display { - print!("Enter the {}:", name); - io::stdout().flush().unwrap(); - - let input = get_user_input(); - - //Try to convert input to a T, or ask again - return match input.parse::() { - Ok(value) => value, - Err(err) => { - println!("\tError: {}: {}", err, input); - ask(name) - } +where + ::Err: Display, +{ + print!("Enter the {}:", name); + io::stdout().flush().unwrap(); + + let input = get_user_input(); + + //Try to convert input to a T, or ask again + return match input.parse::() { + Ok(value) => value, + Err(err) => { + println!("\tError: {}: {}", err, input); + ask(name) } + }; } ///Reads a line from stdin, does not trim the input @@ -26,8 +32,8 @@ pub fn get_user_input_untrimmed() -> String { let mut user_input = String::new(); // Read the user's input from the standard input stdin io::stdin() - .read_line(&mut user_input) - .expect("Error: Failed to read the user's input from stdin."); + .read_line(&mut user_input) + .expect("Error: Failed to read the user's input from stdin."); user_input } @@ -43,9 +49,9 @@ pub fn get_user_input() -> String { pub fn pick_option(options: &[(&str, T)]) -> T { println!("Please pick an option:"); for (i, option) in options.iter().enumerate() { - println!("\t[{}]: {}",i,option.0); + println!("\t[{}]: {}", i, option.0); } - + let mut input: usize = ask("option"); while input >= options.len() { println!("\tError: the option is out of bounds: {}", input); @@ -55,4 +61,4 @@ pub fn pick_option(options: &[(&str, T)]) -> T { let (name, value) = options[input]; println!("\tPicked: {}", name); value -} \ No newline at end of file +} diff --git a/src/key_bindings.rs b/src/key_bindings.rs deleted file mode 100644 index 6ae4b85..0000000 --- a/src/key_bindings.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::fmt; - -use minifb::Key; - -//https://stackoverflow.com/questions/68066875/how-to-store-a-closure-inside-rust-struct -//https://stackoverflow.com/questions/65756096/how-can-i-store-a-closure-object-in-a-struct -pub struct KeyAction { - pub key: Key, - pub description: &'static str, - action: Box, -} - -impl KeyAction { - pub fn new(key: Key, description: &'static str, action: Box) -> KeyAction { - KeyAction { - key, - description, - action, - } - } - - ///Run self.action - pub fn action(&self) { - (self.action)(); - } -} - -impl fmt::Debug for KeyAction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f,"{:?} -> {}", &self.key, &self.description) - } -} - -pub struct KeyBindings { - key_actions: Vec, -} - -impl KeyBindings { - pub fn new(key_actions: Vec) -> KeyBindings { - KeyBindings { key_actions } - } - - ///Adds a `KeyAction` to these `KeyBindings`, will remove any existing `KeyAction` `x` where `x.key` == `key_action.key` - pub fn add_key(&mut self, key_action: KeyAction) { - //Remove any KeyAction x where x.key == key_action.key - self.key_actions.retain(|x| x.key != key_action.key); - - self.key_actions.push(key_action); - } - - ///Adds a `KeyAction` to these `KeyBindings`, will remove any existing `KeyAction` `x` where `x.key` == `key` - pub fn add(&mut self, key: Key, description: &'static str, action: F) { - self.add_key(KeyAction::new(key, description, Box::new(action))); - } - - pub fn key_actions(&self) -> &Vec { - &self.key_actions - } - - /// Prints all `KeyAction`s in these `KeyBindings` to stdout - pub fn print(&self) { - println!("{:?}",self); - } - - pub fn print_key(&self, key: &Key) { - for key_action in &self.key_actions { - if key_action.key == *key { - println!("{:?}", key_action); - return; - } - } - //KeyBindings does not contain a KeyAction x with x.key == key, unbound - println!("{:?}", key); - } -} - -impl fmt::Debug for KeyBindings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "KeyBindings {{")?; - for key_action in &self.key_actions { - writeln!(f, " {:?},", key_action)?; - } - write!(f, "}}") - } -} - -impl Default for KeyBindings { - /// Define all your keybindings here - fn default() -> KeyBindings { - let mut key_bindings = KeyBindings::new(Vec::new()); - key_bindings.add(Key::A, "This is the A key", ||println!("Action A")); - key_bindings.add(Key::B, "This is the B key", ||println!("Action B")); - key_bindings - } -} diff --git a/src/lib.rs b/src/lib.rs index 594f74e..7914d0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,39 +25,38 @@ clippy::many_single_char_names, clippy::cast_sign_loss )] +#![forbid(unsafe_code)] -use std::error::Error; -use std::sync::atomic::{AtomicBool, Ordering}; - -use coloring::ColorChannelMapping; -pub use config::Config; -use mandelbrot_set::MandelbrotSet; -use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions}; +pub mod controller; +pub mod model; +pub mod view; -use crate::coloring::TrueColor; -use crate::complex_plane::{ComplexPlane, View}; -use crate::key_bindings::KeyBindings; -use crate::pixel_buffer::PixelBuffer; -use crate::pixel_buffer::pixel_plane::PixelPlane; -use crate::user_input::{ask, pick_option}; +use std::error::Error; -pub mod complex_plane; -pub mod complex; -pub mod pixel_buffer; -pub mod mandelbrot_set; -pub mod rendering; -pub mod key_bindings; -pub mod coloring; -pub mod user_input; -pub mod config; +//Crate includes +use controller::config::Config; +use controller::minifb_controller; +use model::coloring::ColorChannelMapping; +use model::coloring::TrueColor; +use model::complex_plane::View; +use model::mandelbrot_model::ColoringFunction; +use model::mandelbrot_model::MandelbrotModel; +use view::minifb_mandelbrot_view::MandelbrotView; +use view::terminal::print_banner; +use view::terminal::print_command_info; //Coloring function -type ColoringFunction = fn(iterations: u32, max_iterations: u32) -> TrueColor; -static COLORING_FUNCTION : ColoringFunction = TrueColor::new_from_bernstein_polynomials; +static COLORING_FUNCTION: ColoringFunction = TrueColor::new_from_bernstein_polynomials; //Color channel mapping static COLOR_CHANNEL_MAPPING: ColorChannelMapping = ColorChannelMapping::RGB; +//Window title +static WINDOW_TITLE: &str = "Mandelbrot by Jort"; + +//Banner values +static VERSION: &str = "1.5"; + //Views static VIEW_1: View = View::new(-0.6604166666666667, 0.4437500000000001, 0.1); static VIEW_2: View = View::new(-1.0591666666666668, 0.2629166666666668, 0.01); @@ -65,223 +64,10 @@ static VIEW_3: View = View::new(-0.4624999999999999, 0.55, 0.1); static VIEW_4: View = View::new(-0.46395833333333325, 0.5531250000000001, 0.03); static VIEW_5: View = View::new(-0.4375218333333333, 0.5632133750000003, 0.00002000000000000002); static VIEW_6: View = View::new(-0.7498100000000001, -0.020300000000000054, 0.00006400000000000002); -static VIEW_7: View = View::new(-1.7862712000000047, 0.000052399999999991516, 0.00001677721600000001); -static VIEW_8: View = View::new(-1.7862581627050718, 0.00005198056959995248, 0.000006039797760000003); -static VIEW_9: View = View::new( -0.4687339999999999, 0.5425518958333333, 0.000010000000000000003); -static VIEW_0: View = View::new( -0.437520465811966, 0.5632133750000006, 0.000004000000000000004); - -//Banner values -static VERSION: &str = "1.4"; - -pub struct InteractionVariables{ - ///Variable determining the amount of rows and columns are translated by pressing the 4 arrow keys - pub translation_amount: u8, - ///Variable denoting the user scaling speed; the lower this value, the more aggressive the zooming will become - pub scale_denominator: f64, - pub scale_numerator: f64, -} - -impl InteractionVariables{ - pub fn new(translation_amount: u8, scale_numerator: f64, scale_denominator: f64) -> InteractionVariables { - InteractionVariables { translation_amount, scale_denominator, scale_numerator } - } - - pub fn scaling_factor(&self) -> f64 { - self.scale_numerator / self.scale_denominator - } - - pub fn inverse_scaling_factor(&self) -> f64 { - self.scale_denominator / self.scale_numerator - } - - pub fn increment_translation_amount(&mut self) { - self.translation_amount = self.translation_amount.saturating_add(1); - } - - pub fn decrement_translation_amount(&mut self) { - if self.translation_amount > 1 { - self.translation_amount -=1; - } - } - - pub fn increment_scale_numerator(&mut self) { - if self.scale_numerator < 9.0 { - self.scale_numerator += 1.0; - } - } - - pub fn decrement_scale_numerator(&mut self) { - if self.scale_numerator > 1.0 { - self.scale_numerator -= 1.0; - } - } -} - -impl Default for InteractionVariables{ - fn default() -> Self { - InteractionVariables { translation_amount:10, scale_numerator: 9.0, scale_denominator: 10.0 } - } -} - -// Handle any key events -fn handle_key_events(window: &Window, c: &mut ComplexPlane, p: &mut PixelBuffer, m: &mut MandelbrotSet, vars: &mut InteractionVariables, k: &KeyBindings, supersampling_amount: &mut u8, image_supersampling_amount: &mut u8,coloring_function: &mut ColoringFunction, config: &Config) { - if let Some(key) = window.get_keys_pressed(minifb::KeyRepeat::No).first() { - print!("\nKey pressed: "); - k.print_key(key); - match key { - Key::Up => rendering::translate_and_render_efficiently(c, p, m, vars.translation_amount.into(), 0, *supersampling_amount, *coloring_function), - Key::Down => rendering::translate_and_render_efficiently(c, p, m, -i16::from(vars.translation_amount), 0, *supersampling_amount, *coloring_function), - Key::Left => rendering::translate_and_render_efficiently(c, p, m, 0, -i16::from(vars.translation_amount), *supersampling_amount, *coloring_function), - Key::Right => rendering::translate_and_render_efficiently(c, p, m, 0, vars.translation_amount.into(), *supersampling_amount, *coloring_function), - Key::R => c.reset(), - Key::NumPadPlus => vars.increment_translation_amount(), - Key::NumPadMinus => vars.decrement_translation_amount(), - Key::NumPadAsterisk => vars.increment_scale_numerator(), - Key::NumPadSlash => vars.decrement_scale_numerator(), - Key::LeftBracket => c.scale(vars.scaling_factor()), - Key::RightBracket => c.scale(vars.inverse_scaling_factor()), - Key::V => println!("Center: {:?}, scale: {:?}", c.center(), c.get_scale()), - Key::Key1 => c.set_view(&VIEW_1), - Key::Key2 => c.set_view(&VIEW_2), - Key::Key3 => c.set_view(&VIEW_3), - Key::Key4 => c.set_view(&VIEW_4), - Key::Key5 => c.set_view(&VIEW_5), - Key::Key6 => c.set_view(&VIEW_6), - Key::Key7 => c.set_view(&VIEW_7), - Key::Key8 => c.set_view(&VIEW_8), - Key::Key9 => c.set_view(&VIEW_9), - Key::Key0 => c.set_view(&VIEW_0), - Key::K => k.print(), - Key::S => { - let time_stamp = chrono::Utc::now().to_string(); - if config.window_scale == 1.0 { - p.save_as_png(&time_stamp, &c.get_view(), m, *image_supersampling_amount); - } else { - let mut image_p: PixelBuffer = PixelBuffer::new(PixelPlane::new(config.image_width, config.image_height)); - let mut image_c: ComplexPlane = ComplexPlane::new(config.image_width, config.image_height); - image_p.color_channel_mapping = p.color_channel_mapping; - image_c.set_view(&c.get_view()); - rendering::render_complex_plane_into_buffer(&mut image_p, &image_c, m, *image_supersampling_amount, *coloring_function); - image_p.save_as_png(&time_stamp, &c.get_view(), m, *image_supersampling_amount); - } - } - Key::I => c.set_view(&View::new(ask("x"), ask("y"), ask("scale"))), - Key::A => *coloring_function = pick_option(&[("HSV", TrueColor::new_from_hsv_colors), ("Bernstein polynomials", TrueColor::new_from_bernstein_polynomials)]), - Key::M => m.max_iterations = ask("max_iterations"), - Key::O => p.color_channel_mapping = ask("color_channel_mapping"), - Key::Q => {*supersampling_amount = ask::("supersampling_amount").clamp(1, 64); *image_supersampling_amount = *supersampling_amount;}, - Key::X => *image_supersampling_amount = ask::("image_supersampling_amount").clamp(1, 64), - Key::C => println!("{:?}", config), - _ => (), - } - match key { - Key::NumPadPlus | Key::NumPadMinus => println!("translation_amount: {}", vars.translation_amount), - Key::NumPadSlash | Key::NumPadAsterisk => println!("scale factor: {}/{}",vars.scale_numerator,vars.scale_denominator), - Key::Up | Key::Down | Key::Left | Key::Right => c.print(), - Key::R | Key::Key1 | Key::Key2 | Key::Key3 | Key::Key4 | Key::Key5 | Key::Key6 | Key::Key7 | Key::Key8 | Key::Key9 | Key::Key0 | Key::LeftBracket | Key::RightBracket | Key::I | Key::A | Key::M | Key::O | Key::Q => { - rendering::render_complex_plane_into_buffer(p, c, m, *supersampling_amount, *coloring_function); - c.print(); - }, - _ => (), - } - } -} - -fn was_clicked(current: bool, previous: bool) -> bool { - current && !previous -} - -fn handle_left_mouse_clicked(x: f32, y: f32, c: &ComplexPlane) { - println!("\nMouseButton::Left -> Info at ({x}, {y})"); - //let iterations = p.iterations_at_point(x as usize, y as usize, m.max_iterations); //TODO: fix this - let complex = c.complex_from_pixel_plane(x.into(), y.into()); - println!("Complex: {:?}", complex); - //println!("iterations: {}", iterations); - println!(); -} - -fn handle_right_mouse_clicked(x: f32, y: f32, c: &mut ComplexPlane, p: &mut PixelBuffer, m: &MandelbrotSet, supersampling_amount: u8, coloring_function: ColoringFunction) { - println!("\nMouseButton::Right -> Move to ({x}, {y})"); - let new_center = c.complex_from_pixel_plane(x.into(), y.into()); - println!("c.center: {:?}", c.center()); - println!("new_center: {:?}", new_center); - - rendering::translate_to_center_and_render_efficiently(c, p, m, &new_center, supersampling_amount, coloring_function); - c.print(); - println!(); -} - -/////Mouse click recorder with interior mutability to toggle mouse clicks; -/// without such a (static function) variable, clicking the screen once would result in multiple actions -struct MouseClickRecorder { - mouse_button: MouseButton, - previous: AtomicBool -} - -impl MouseClickRecorder { - pub const fn new(mouse_button: MouseButton) -> MouseClickRecorder { - MouseClickRecorder { mouse_button, previous: AtomicBool::new(false) } - } - - ///Returns whether the `mouse_button` was clicked once - pub fn was_clicked(&self, window: &Window) -> bool { - let current = window.get_mouse_down(self.mouse_button); - let previous = self.previous.load(Ordering::Relaxed); - let result = was_clicked(current, previous); - if current != previous {self.previous.store(current, Ordering::Relaxed)} - result - } -} - -fn handle_mouse_events(window: &Window, c: &mut ComplexPlane, p: &mut PixelBuffer, m: &MandelbrotSet, supersampling_amount: u8, coloring_function: ColoringFunction) { - static LEFT_MOUSE_RECORDER: MouseClickRecorder = MouseClickRecorder::new(MouseButton::Left); //Static variable with interior mutability to toggle mouse clicks; without such a variable, clicking the screen once would result in multiple actions - static RIGHT_MOUSE_RECORDER: MouseClickRecorder = MouseClickRecorder::new(MouseButton::Right); - - if let Some((x, y)) = window.get_mouse_pos(MouseMode::Discard) { - - //Left mouse actions - if LEFT_MOUSE_RECORDER.was_clicked(window) { - handle_left_mouse_clicked(x, y, c); - } - - //Right mouse actions - if RIGHT_MOUSE_RECORDER.was_clicked(window) { - handle_right_mouse_clicked(x, y, c, p, m, supersampling_amount, coloring_function); - } - - } -} - -///Prints Mandelbrot ASCII art :)
-///Prints the `application_banner`, `author_banner`, and `version` -fn print_banner() -{ -//Made using: https://patorjk.com/software/taag/#p=display&f=Big&t=Mandelbrot -let application_banner = r" -__ __ _ _ _ _ -| \/ | | | | | | | | -| \ / | __ _ _ __ __| | ___| | |__ _ __ ___ | |_ -| |\/| |/ _` | '_ \ / _` |/ _ \ | '_ \| '__/ _ \| __| -| | | | (_| | | | | (_| | __/ | |_) | | | (_) | |_ -|_| |_|\__,_|_| |_|\__,_|\___|_|_.__/|_| \___/ \__|"; -//Made using: https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=by%20Jort -let author_banner = r" - __ __ __ - / / __ __ __ / /__ ____/ /_ - / _ \/ // / / // / _ \/ __/ __/ -/_.__/\_, / \___/\___/_/ \__/ - /___/ "; -let version = VERSION; -println!("{}{}v{}\n\n", application_banner, author_banner, version); -} - -///Prints a command info tip for the users benefit -fn print_command_info() { - let tip = "Run Mandelbrot using:"; - let command = "cargo run --release -- "; - let command_info = "where means substitute with the value of arg\nuse '-' to use the default value of arg"; - println!("{}\n\t{}\n{}\n",tip, command, command_info); -} +static VIEW_7: View = View::new(-1.7862712000000047, 0.000052399999999991516, 0.00001677721600000001); +static VIEW_8: View = View::new(-1.7862581627050718, 0.00005198056959995248, 0.000006039797760000003); +static VIEW_9: View = View::new(-0.4687339999999999, 0.5425518958333333, 0.000010000000000000003); +static VIEW_0: View = View::new(-0.437520465811966, 0.5632133750000006, 0.000004000000000000004); ///Holds all the logic currently in the main function that isn't involved with setting up configuration or handling errors, to make `main` concise and ///easy to verify by inspection @@ -289,99 +75,17 @@ fn print_command_info() { /// Will panic if minifb cannot open a Window /// # Errors /// Currently does not return any Errors -pub fn run(config: &Config) -> Result<(), Box> { - // Complex plane dimensions and increments - let mut c = ComplexPlane::new(config.window_width, config.window_height); - // Pixel plane and buffer - let mut p = PixelBuffer::new(PixelPlane::new(config.window_width, config.window_height)); - // User interaction variables - let mut vars = InteractionVariables::default(); - // Multithreading variables - let amount_of_threads = num_cpus::get(); //Amount of CPU threads to use, TODO: use this value in rendering functions - // Mandelbrot set iterator - let mut m: MandelbrotSet = MandelbrotSet::new(config.max_iterations, config.orbit_radius); - //Coloring function - let mut coloring_function = COLORING_FUNCTION; - //Color channel mapping - p.color_channel_mapping = COLOR_CHANNEL_MAPPING; - //SSAA multiplier - let mut supersampling_amount = config.supersampling_amount; - //Image SSAA multiplier - let mut image_supersampling_amount = supersampling_amount; - // Create a new window - let mut window = Window::new( - "Mandelbrot set viewer", - config.window_width, - config.window_height, - WindowOptions::default(), - ) - .unwrap_or_else(|e| { - panic!("{}", e); - }); +pub fn run() -> Result<(), Box> { + let mandelbrot_model = MandelbrotModel::get_instance(); + //Print the banner - print_banner(); + print_banner(VERSION); //Print command info print_command_info(); - //Initialize keybindings TODO: I want to have a vector of structs containing functions with different signatures, this is not easily possible. All functionality should be placed here, in the future, when - //I've figured out how to have closures with different signatures in the same struct field - //For now, use empty_closure, to have a closure that does nothing as action - let mut key_bindings: KeyBindings = KeyBindings::new(Vec::new()); - let empty_closure = || (); - key_bindings.add(Key::Up, "Move up translation_amount pixels", empty_closure); - key_bindings.add(Key::Down, "Move down translation_amount pixels", empty_closure); - key_bindings.add(Key::Left, "Move left translation_amount pixels", empty_closure); - key_bindings.add(Key::Right, "Move right translation_amount pixels", empty_closure); - key_bindings.add(Key::R, "Reset the Mandelbrot set view to the starting view", empty_closure); - key_bindings.add(Key::NumPadPlus, "Increment translation_amount", empty_closure); - key_bindings.add(Key::NumPadMinus, "Decrement translation amount", empty_closure); - key_bindings.add(Key::NumPadAsterisk, "Increment scale_numerator", empty_closure); - key_bindings.add(Key::NumPadSlash, "Decrement scale_numerator", empty_closure); - key_bindings.add(Key::LeftBracket, "Scale the view by scaling_factor, effectively zooming in",empty_closure); - key_bindings.add(Key::RightBracket, "Scale the view by inverse_scaling_factor, effectively zooming out", empty_closure); - key_bindings.add(Key::V, "Prints the current Mandelbrot set view; the center and scale", empty_closure); - key_bindings.add(Key::Key1, "Renders VIEW_1", empty_closure); - key_bindings.add(Key::Key2, "Renders VIEW_2", empty_closure); - key_bindings.add(Key::Key3, "Renders VIEW_3", empty_closure); - key_bindings.add(Key::Key4, "Renders VIEW_4", empty_closure); - key_bindings.add(Key::Key5, "Renders VIEW_5", empty_closure); - key_bindings.add(Key::Key6, "Renders VIEW_6", empty_closure); - key_bindings.add(Key::Key7, "Renders VIEW_7", empty_closure); - key_bindings.add(Key::Key8, "Renders VIEW_8", empty_closure); - key_bindings.add(Key::Key9, "Renders VIEW_9", empty_closure); - key_bindings.add(Key::Key0, "Renders VIEW_0", empty_closure); - key_bindings.add(Key::K, "Prints the keybindings", empty_closure); - key_bindings.add(Key::S, "Saves the current Mandelbrot set view as an image in the saved folder", empty_closure); - key_bindings.add(Key::I, "Manually input a Mandelbrot set view", empty_closure); - key_bindings.add(Key::A, "Pick an algorithm to color the Mandelbrot set view", empty_closure); - key_bindings.add(Key::M, "Change the Mandelbrot set view max_iterations", empty_closure); - key_bindings.add(Key::O, "Change the Mandelbrot set view color channel mapping, xyz -> RGB, where x,y,z ∈ {{'R','G','B'}} (case-insensitive)", empty_closure); - key_bindings.add(Key::Q, "Change the window and image quality of the Mandelbrot set rendering by setting the SSAA multiplier, clamped from 1x to 64x", empty_closure); - key_bindings.add(Key::X, "Change the image quality of the Mandelbrot set rendering by setting the SSAA multiplier, clamped from 1x to 64x", empty_closure); - key_bindings.add(Key::C, "Prints the configuration variables", empty_closure); - key_bindings.print(); - - p.pixel_plane.print(); - c.print(); - println!("Mandelbrot set parameters: max. iterations is {} and orbit radius is {}", config.max_iterations, config.orbit_radius); - println!("Amount of CPU threads that will be used for rendering: {}", amount_of_threads); - println!("Supersampling amount used for rendering: {}x", supersampling_amount); - println!(); - - println!("Rendering Mandelbrot set default view"); - rendering::render_complex_plane_into_buffer(&mut p, &c, &m, supersampling_amount, coloring_function); - - // Main loop - while window.is_open() && !window.is_key_down(Key::Escape) { - - // Update the window with the new buffer - window.update_with_buffer(&p.pixels, config.window_width, config.window_height).unwrap(); - - // Handle any window events - handle_key_events(&window, &mut c, &mut p, &mut m, &mut vars, &key_bindings, &mut supersampling_amount, &mut image_supersampling_amount, &mut coloring_function, config); - - //Handle any mouse events - handle_mouse_events(&window, &mut c, &mut p, &m, supersampling_amount, coloring_function); - } - + //Create the view + let mut mandelbrot_view = MandelbrotView::new(&mandelbrot_model); + //Run the controller + drop(mandelbrot_model); //TODO: Get rid of MandelbrotModel mutex, as this can cause deadlocks + minifb_controller::run(&mut mandelbrot_view)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 04d8f71..76e7c17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,13 @@ -use std::env; -use std::process; +use std::{env, process}; -use mandelbrot::{Config, self}; +use mandelbrot::{self}; fn main() { - let config = Config::build(env::args()).unwrap_or_else(|err| { - eprintln!("Problem parsing arguments: {}", err); - process::exit(1); - }); + //Turn on Rust backtrace + env::set_var("RUST_BACKTRACE", "1"); - if let Err(err) = mandelbrot::run(&config) { + //Run the application + if let Err(err) = mandelbrot::run() { eprintln!("Application error: {}", err); process::exit(1); } diff --git a/src/coloring.rs b/src/model/coloring.rs similarity index 82% rename from src/coloring.rs rename to src/model/coloring.rs index c282999..a81bd38 100644 --- a/src/coloring.rs +++ b/src/model/coloring.rs @@ -2,9 +2,9 @@ use std::{fmt, str::FromStr}; use angular_units::Deg; use num::traits::Pow; -use prisma::{Hsv, Rgb, FromColor}; +use prisma::{FromColor, Hsv, Rgb}; -#[derive(Debug,Clone,Copy)] +#[derive(Debug, Clone, Copy)] ///A mapping from ColorChannelMapping -> RGB, the first character denotes the new red channel, the second character the new green channel, /// the third character the new blue channel.
/// E.g: Ok(ColorChannelMapping::BGR) means that red will get the value of blue, green the value of green, and blue the value of red: @@ -40,7 +40,7 @@ pub enum ColorChannelMapping { } impl ColorChannelMapping { - pub fn new(r_g_b: &str) -> Result { + pub fn new(r_g_b: &str) -> Result { match &r_g_b.to_uppercase()[..] { "BBB" => Ok(ColorChannelMapping::BBB), "BBG" => Ok(ColorChannelMapping::BBG), @@ -69,13 +69,15 @@ impl ColorChannelMapping { "RRG" => Ok(ColorChannelMapping::RRG), "RRR" => Ok(ColorChannelMapping::RRR), "RGB" => Ok(ColorChannelMapping::RGB), - _ => Err(String::from("Invalid r_g_b string, should be a string of length three with characters 'R', 'G' or 'B'")) + _ => Err(String::from( + "Invalid r_g_b string, should be a string of length three with characters 'R', 'G' or 'B'", + )), } } ///Returns (x,y,z) where x,y,z ∈ {'R', 'G', 'B'} - pub fn get_r_g_b_mapping(&self) -> (char,char,char) { + pub fn get_r_g_b_mapping(&self) -> (char, char, char) { let r_g_b = self.to_string().chars().collect::>(); - (r_g_b[0],r_g_b[1],r_g_b[2]) + (r_g_b[0], r_g_b[1], r_g_b[2]) } } @@ -97,12 +99,12 @@ impl FromStr for ColorChannelMapping { pub struct TrueColor { pub red: u8, pub green: u8, - pub blue: u8 + pub blue: u8, } impl TrueColor { - pub fn new(red: u8, green: u8, blue: u8) -> TrueColor { - TrueColor { red, green, blue } + pub fn new(red: u8, green: u8, blue: u8) -> TrueColor { + TrueColor { red, green, blue } } /// Creates a 32-bit color. The encoding for each pixel is `0RGB`: @@ -110,12 +112,20 @@ impl TrueColor { /// afterwards for the green channel, and the lower 8-bits for the blue channel. pub fn to_32_bit(&self, mapping: &ColorChannelMapping) -> u32 { let (r_map, g_map, b_map) = mapping.get_r_g_b_mapping(); - let (r, g, b) = (u32::from(self.get_color(r_map)), u32::from(self.get_color(g_map)), u32::from(self.get_color(b_map))); + let (r, g, b) = ( + u32::from(self.get_color(r_map)), + u32::from(self.get_color(g_map)), + u32::from(self.get_color(b_map)), + ); (r << 16) | (g << 8) | b } pub fn get_color(&self, color: char) -> u8 { - assert!(color == 'R' || color == 'G' || color == 'B', "Error: color should be equal to R, G, or B, color = {}",color); + assert!( + color == 'R' || color == 'G' || color == 'B', + "Error: color should be equal to R, G, or B, color = {}", + color + ); match color { 'R' => self.red, 'G' => self.green, @@ -130,11 +140,15 @@ impl TrueColor { /// Source: [Bernstein polynomial coloring](https://solarianprogrammer.com/2013/02/28/mandelbrot-set-cpp-11/) fn new_from_bernstein_polynomials_normalized(t: f64) -> TrueColor { let t = t.abs().min(0.999); - let one_minus_t = 1.0-t; + let one_minus_t = 1.0 - t; let red: f64 = 9.0 * one_minus_t * t.pow(3) * 255.0; let green: f64 = 15.0 * one_minus_t * t.pow(2) * 255.0; let blue: f64 = 8.5 * one_minus_t * t * 255.0; - TrueColor { red: red as u8, green: green as u8, blue: blue as u8 } + TrueColor { + red: red as u8, + green: green as u8, + blue: blue as u8, + } } ///A `coloring_function` @@ -150,10 +164,10 @@ impl TrueColor { ///A `coloring_function` pub fn new_from_hsv_colors(iterations: u32, max_iterations: u32) -> TrueColor { let hue = 0.3 * f64::from(iterations); - let saturation = 1.0;//0.8; - let value: f64 = if iterations < max_iterations {1.0} else {0.0}; + let saturation = 1.0; //0.8; + let value: f64 = if iterations < max_iterations { 1.0 } else { 0.0 }; let hue_degree = Deg(hue % 359.999); - let hsv = Hsv::new(hue_degree,saturation,value); + let hsv = Hsv::new(hue_degree, saturation, value); let rgb = Rgb::from_color(&hsv); let red = normalized_to_byte(rgb.red()); let green = normalized_to_byte(rgb.green()); @@ -181,12 +195,11 @@ impl TrueColor { let green = green as u8; TrueColor { red, green, blue } } - } ///Maps a number t ∈ [0.0, 1.0] to a byte b ∈ [0, 255] fn normalized_to_byte(t: f64) -> u8 { let t = t.abs().min(1.0); let byte = (t * 255.0) as i16; - byte.unsigned_abs() as u8 + byte.unsigned_abs() as u8 } diff --git a/src/complex.rs b/src/model/complex.rs similarity index 90% rename from src/complex.rs rename to src/model/complex.rs index c229842..fcc85da 100644 --- a/src/complex.rs +++ b/src/model/complex.rs @@ -55,13 +55,7 @@ impl Complex { impl fmt::Debug for Complex { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {}i", - self.x, - if self.y > 0.0 { '+' } else { '-' }, - self.y.abs() - ) + write!(f, "{} {} {}i", self.x, if self.y > 0.0 { '+' } else { '-' }, self.y.abs()) } } //Complex diff --git a/src/complex_plane.rs b/src/model/complex_plane.rs similarity index 92% rename from src/complex_plane.rs rename to src/model/complex_plane.rs index 396a7ac..42c7152 100644 --- a/src/complex_plane.rs +++ b/src/model/complex_plane.rs @@ -1,4 +1,4 @@ -use crate::complex::Complex; +use crate::model::complex::Complex; /// # Complex plane /// In mathematics, the complex plane is the plane formed by the complex numbers, with a Cartesian coordinate system such that the x-axis, called the real axis, is formed by the real numbers, and the y-axis, called the imaginary axis, is formed by the imaginary numbers. @@ -88,11 +88,7 @@ impl ComplexPlane { pub fn print(&self) { println!("Complex plane:\tR ∈ [{},{}]", self.min_x, self.max_x); println!("\t\tC ∈ [{},{}]", self.min_y, self.max_y); - println!( - "\t\tCenter is {:?} and scale is {}", - self.center(), - self.get_scale() - ); + println!("\t\tCenter is {:?} and scale is {}", self.center(), self.get_scale()); } /// Resets the total translation and scaling applied to the Complex plane by the translate() and scale() functions @@ -135,10 +131,10 @@ impl ComplexPlane { let old = self.center(); let mut translation = center.subtract(&old); translation.y = -translation.y; //Negate because the Complex plane and pixel plane are flipped - /*println!("DEBUG set_center():"); - println!("\tcenter: {:?}", center); - println!("\told: {:?}", old); - println!("\ttranslation: {:?}", translation);*/ + /*println!("DEBUG set_center():"); + println!("\tcenter: {:?}", center); + println!("\told: {:?}", old); + println!("\ttranslation: {:?}", translation);*/ self.translate(translation.x, translation.y); translation } @@ -151,13 +147,12 @@ impl ComplexPlane { } /// Set the Complex plane at Center (x,y) at the given scale, where scale == 1 => max_x-min_x=2.5 - pub fn set_view(&mut self, view: &View) - { + pub fn set_view(&mut self, view: &View) { self.set_view_separated(view.x, view.y, view.scale); } pub fn pixels_to_imaginary(&self, amount: u8) -> f64 { - f64::from(amount) * self.increment_y + f64::from(amount) * self.increment_y } pub fn pixels_to_real(&self, amount: u8) -> f64 { @@ -182,12 +177,12 @@ impl ComplexPlane { pub struct View { x: f64, y: f64, - scale: f64 + scale: f64, } impl View { pub const fn new(x: f64, y: f64, scale: f64) -> View { - View {x,y,scale} + View { x, y, scale } } } @@ -195,4 +190,4 @@ impl std::fmt::Debug for View { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "z = {:?}, scale = {}", Complex::new(self.x, self.y), self.scale) } -} \ No newline at end of file +} diff --git a/src/mandelbrot_set.rs b/src/model/mandelbrot_function.rs similarity index 61% rename from src/mandelbrot_set.rs rename to src/model/mandelbrot_function.rs index f5790bb..5334fe5 100644 --- a/src/mandelbrot_set.rs +++ b/src/model/mandelbrot_function.rs @@ -1,15 +1,18 @@ -use crate::complex::Complex; +use crate::model::complex::Complex; #[derive(Clone)] -pub struct MandelbrotSet { +pub struct MandelbrotFunction { pub max_iterations: u32, ///If z remains within the orbit_radius in max_iterations, we assume c does not tend to infinity - pub orbit_radius: f64, + pub orbit_radius: f64, } -impl MandelbrotSet { - pub fn new(max_iterations: u32, orbit_radius: f64) -> MandelbrotSet { - MandelbrotSet { max_iterations, orbit_radius } +impl MandelbrotFunction { + pub fn new(max_iterations: u32, orbit_radius: f64) -> MandelbrotFunction { + MandelbrotFunction { + max_iterations, + orbit_radius, + } } /// Run the Mandelbrot set algorithm for a single Complex number @@ -17,11 +20,12 @@ impl MandelbrotSet { pub fn iterate(&self, c: &Complex) -> u32 { let mut z = Complex::new(0.0, 0.0); let mut iterations: u32 = 0; - let orbit_radius_squared = self.orbit_radius*self.orbit_radius; + let orbit_radius_squared = self.orbit_radius * self.orbit_radius; for _ in 0..self.max_iterations { z = z.squared().add(c); - if (z.x * z.x + z.y * z.y) > orbit_radius_squared { //Optimization: square both sides of the Mandelbrot set function, saves us taking the square root + if (z.x * z.x + z.y * z.y) > orbit_radius_squared { + //Optimization: square both sides of the Mandelbrot set function, saves us taking the square root break; } iterations += 1; @@ -30,8 +34,8 @@ impl MandelbrotSet { } } -impl std::fmt::Debug for MandelbrotSet { +impl std::fmt::Debug for MandelbrotFunction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "max_iterations = {}, orbit_radius = {}", self.max_iterations, self.orbit_radius) } -} \ No newline at end of file +} diff --git a/src/model/mandelbrot_model.rs b/src/model/mandelbrot_model.rs new file mode 100644 index 0000000..f143d71 --- /dev/null +++ b/src/model/mandelbrot_model.rs @@ -0,0 +1,59 @@ +use lazy_static::lazy_static; +use std::{ + env, process, + sync::{Mutex, MutexGuard}, +}; + +use crate::{ + controller::interaction_variables::InteractionVariables, model::coloring::TrueColor, Config, COLORING_FUNCTION, COLOR_CHANNEL_MAPPING, +}; + +use super::{complex_plane::ComplexPlane, mandelbrot_function::MandelbrotFunction, pixel_buffer::PixelBuffer, pixel_plane::PixelPlane}; + +lazy_static! { + static ref MANDELBROT_MODEL_INSTANCE: Mutex = Mutex::new(MandelbrotModel::new()); +} + +pub type ColoringFunction = fn(iterations: u32, max_iterations: u32) -> TrueColor; + +pub struct MandelbrotModel { + pub config: Config, + pub c: ComplexPlane, + pub p: PixelBuffer, + pub vars: InteractionVariables, + pub amount_of_threads: usize, + pub m: MandelbrotFunction, + pub coloring_function: ColoringFunction, +} + +impl MandelbrotModel { + pub fn new() -> MandelbrotModel { + let config = Config::build(env::args()).unwrap_or_else(|err| { + eprintln!("Problem parsing arguments: {}", err); + process::exit(1); + }); + + let mut result = MandelbrotModel { + config: config.clone(), + c: ComplexPlane::new(config.window_width, config.window_height), + p: PixelBuffer::new(PixelPlane::new(config.window_width, config.window_height)), + vars: InteractionVariables::default(), + amount_of_threads: num_cpus::get(), + m: MandelbrotFunction::new(config.max_iterations, config.orbit_radius), + coloring_function: COLORING_FUNCTION, + }; + //Color channel mapping + result.p.color_channel_mapping = COLOR_CHANNEL_MAPPING; + + result + } + + /// Returns the singleton MandelbrotModel instance. + pub fn get_instance() -> MutexGuard<'static, MandelbrotModel> { + let lock = MANDELBROT_MODEL_INSTANCE.try_lock(); + if let Ok(instance) = lock { + return instance; + } + panic!("[DEADLOCK]: You have called the singleton twice! This should never happen. It means that within the same scope, MandelbrotModel::get_instance() was called more than once."); + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..5046ffc --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,8 @@ +pub mod coloring; +pub mod complex; +pub mod complex_plane; +pub mod mandelbrot_function; +pub mod mandelbrot_model; +pub mod pixel_buffer; +pub mod pixel_plane; +pub mod rendering; diff --git a/src/pixel_buffer.rs b/src/model/pixel_buffer.rs similarity index 73% rename from src/pixel_buffer.rs rename to src/model/pixel_buffer.rs index d4e8934..008fe6d 100644 --- a/src/pixel_buffer.rs +++ b/src/model/pixel_buffer.rs @@ -1,10 +1,7 @@ -use std::{path::Path, fs::File, io::BufWriter}; +use std::{fs::File, io::BufWriter, path::Path}; -use crate::{coloring::{TrueColor, ColorChannelMapping}, complex_plane::View, mandelbrot_set::MandelbrotSet}; - -use self::pixel_plane::PixelPlane; - -pub mod pixel_plane; +use crate::model::coloring::{ColorChannelMapping, TrueColor}; +use crate::model::{complex_plane::View, mandelbrot_function::MandelbrotFunction, pixel_plane::PixelPlane}; #[derive(Clone)] pub struct PixelBuffer { @@ -21,7 +18,12 @@ impl PixelBuffer { let colors: Vec = vec![black; pixel_plane.width * pixel_plane.height]; let color_channel_mapping = ColorChannelMapping::RGB; let pixels: Vec = PixelBuffer::colors_to_pixels(&colors, &color_channel_mapping); - PixelBuffer { pixel_plane, colors, pixels, color_channel_mapping} + PixelBuffer { + pixel_plane, + colors, + pixels, + color_channel_mapping, + } } /// Converts a buffer index to a screen coordinate @@ -50,7 +52,7 @@ impl PixelBuffer { let pixel = self.buffer[index]; let iterations = crate::iterations_from_hsv_pixel(pixel, max_iterations); iterations - }*/ + }*/ /// Translate the complex plane in the `buffer` `rows` up and `columns` to the right. /// This operation is significantly less expensive than the `render_box_render_complex_plane_into_buffer` function, as it does not rerender anything in the complex plane, it simply @@ -58,12 +60,20 @@ impl PixelBuffer { /// Note: The removed rows and columns should be rerendered by the `render_box_render_complex_plane_into_buffer` function. pub fn translate_buffer(&mut self, rows_up: i128, columns_right: i128) { //Iterate over the correct y's in the correct order - let y_range : Vec = if rows_up > 0 {((rows_up as usize)..self.pixel_plane.height).rev().collect()} else {(0..((self.pixel_plane.height as i128 + rows_up) as usize)).collect()}; + let y_range: Vec = if rows_up > 0 { + ((rows_up as usize)..self.pixel_plane.height).rev().collect() + } else { + (0..((self.pixel_plane.height as i128 + rows_up) as usize)).collect() + }; //Iterate over the correct x's in the correct order - let x_range : Vec = if columns_right > 0 {((columns_right as usize)..self.pixel_plane.width).rev().collect()} else {(0..((self.pixel_plane.width as i128 + columns_right) as usize)).collect()}; + let x_range: Vec = if columns_right > 0 { + ((columns_right as usize)..self.pixel_plane.width).rev().collect() + } else { + (0..((self.pixel_plane.width as i128 + columns_right) as usize)).collect() + }; for y in y_range { - let other_y = (y as i128-rows_up) as usize; + let other_y = (y as i128 - rows_up) as usize; //println!("y: {y} and other_y: {other_y}"); for x in &x_range { let other_x = (*x as i128 - columns_right) as usize; @@ -82,10 +92,11 @@ impl PixelBuffer { ///Also stores author and application metadata /// # Panics /// If the file `saved/{file_name_without_extension}.png` cannot be created - pub fn save_as_png(&self, file_name_without_extension: &str, view: &View, m: &MandelbrotSet, supersampling_amount: u8) { + pub fn save_as_png(&self, file_name_without_extension: &str, view: &View, m: &MandelbrotFunction, supersampling_amount: u8) { let file_name_without_extension = file_name_without_extension.replace(':', "-").replace(' ', "_"); //Replace ':' with '-' for Windows file system. Replace ' ' with '_' because spaces are annoying in filenames. let file_name = format!("saved{}{}.png", std::path::MAIN_SEPARATOR_STR, file_name_without_extension); - match std::fs::create_dir_all("saved") { //Create the saved folder if it does not exist + match std::fs::create_dir_all("saved") { + //Create the saved folder if it does not exist Ok(()) => (), //Currently not doing anything with the Result of trying to create the saved folder Err(err) => eprintln!("{}", err), } @@ -98,13 +109,26 @@ impl PixelBuffer { let view_text = format!("{:?}", view); encoder.add_text_chunk(String::from("view"), view_text).unwrap(); let mandelbrot_set_text = format!("{:?}", m); - encoder.add_text_chunk(String::from("mandelbrot_set"), mandelbrot_set_text).unwrap(); + encoder + .add_text_chunk(String::from("mandelbrot_function"), mandelbrot_set_text) + .unwrap(); let supersampling_amount_text = format!("{}x", supersampling_amount); - encoder.add_text_chunk(String::from("supersampling_amount"), supersampling_amount_text).unwrap(); - encoder.add_text_chunk(String::from("application"), String::from("Mandelbrot by Jort (https://github.com/jortrr/mandelbrot)")).unwrap(); - encoder.add_text_chunk(String::from("author"), String::from("jortrr (https://github.com/jortrr/)")).unwrap(); + encoder + .add_text_chunk(String::from("supersampling_amount"), supersampling_amount_text) + .unwrap(); + encoder + .add_text_chunk( + String::from("application"), + String::from("Mandelbrot by Jort (https://github.com/jortrr/mandelbrot)"), + ) + .unwrap(); + encoder + .add_text_chunk(String::from("author"), String::from("jortrr (https://github.com/jortrr/)")) + .unwrap(); let color_channel_mapping_text = format!("{:?}", self.color_channel_mapping); - encoder.add_text_chunk(String::from("color_channel_mapping"), color_channel_mapping_text).unwrap(); + encoder + .add_text_chunk(String::from("color_channel_mapping"), color_channel_mapping_text) + .unwrap(); let mut data: Vec = Vec::new(); let (r_map, g_map, b_map) = self.color_channel_mapping.get_r_g_b_mapping(); for color in &self.colors { diff --git a/src/pixel_buffer/pixel_plane.rs b/src/model/pixel_plane.rs similarity index 50% rename from src/pixel_buffer/pixel_plane.rs rename to src/model/pixel_plane.rs index 974b9ad..e0c05c6 100644 --- a/src/pixel_buffer/pixel_plane.rs +++ b/src/model/pixel_plane.rs @@ -1,20 +1,27 @@ - #[derive(Clone)] pub struct PixelPlane { pub width: usize, pub height: usize, aspect_ratio_width: usize, - aspect_ratio_height: usize + aspect_ratio_height: usize, } impl PixelPlane { - pub fn new(width: usize, height: usize) -> PixelPlane{ + pub fn new(width: usize, height: usize) -> PixelPlane { let gcd = num::integer::gcd(width, height); //Needed to compute the aspect ratio of the pixel plane - PixelPlane {width, height, aspect_ratio_width: width/gcd, aspect_ratio_height: height/gcd} + PixelPlane { + width, + height, + aspect_ratio_width: width / gcd, + aspect_ratio_height: height / gcd, + } } /// Prints: "Pixel plane: size is {}x{} and aspect ratio is {}:{}", `self.width`, `self.height`, `self.aspect_ratio_width`, `self.aspect_ratio_height`) pub fn print(&self) { - println!("Pixel plane: size is {}x{} and aspect ratio is {}:{}",self.width, self.height, self.aspect_ratio_width, self.aspect_ratio_height); + println!( + "Pixel plane: size is {}x{} and aspect ratio is {}:{}", + self.width, self.height, self.aspect_ratio_width, self.aspect_ratio_height + ); } -} \ No newline at end of file +} diff --git a/src/rendering.rs b/src/model/rendering.rs similarity index 51% rename from src/rendering.rs rename to src/model/rendering.rs index 1196623..57a2bfc 100644 --- a/src/rendering.rs +++ b/src/model/rendering.rs @@ -1,30 +1,53 @@ //Temporary file to group together all rendering functionality -use std::{time::Instant, thread, sync::{Arc, Mutex, atomic::{AtomicU8, Ordering}}, io::{self, Write}}; +use std::{ + io::{self, Write}, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, Mutex, + }, + thread, + time::Instant, +}; use rand::Rng; -use crate::{pixel_buffer::PixelBuffer, complex_plane::ComplexPlane, mandelbrot_set::MandelbrotSet, complex::Complex, coloring::TrueColor}; +use crate::model::coloring::TrueColor; +use crate::{model::complex::Complex, MandelbrotModel}; + +use super::complex_plane::View; ///A box representing the area to render by rendering functions -#[derive(Clone,Copy)] +#[derive(Clone, Copy)] pub struct RenderBox { min_x: usize, max_x: usize, min_y: usize, - max_y: usize + max_y: usize, } impl RenderBox { - pub fn new(min_x: usize, max_x: usize, min_y: usize, max_y: usize) -> RenderBox { - RenderBox { min_x, max_x, min_y, max_y } + pub fn new(min_x: usize, max_x: usize, min_y: usize, max_y: usize) -> RenderBox { + RenderBox { + min_x, + max_x, + min_y, + max_y, + } } pub fn print(&self) { - println!("RenderBox: ({},{}) -> ({},{}) {{{} pixels}}",self.min_x,self.min_y,self.max_x,self.max_y, self.compute_pixel_count()); + println!( + "RenderBox: ({},{}) -> ({},{}) {{{} pixels}}", + self.min_x, + self.min_y, + self.max_x, + self.max_y, + self.compute_pixel_count() + ); } pub fn compute_pixel_count(&self) -> usize { - (self.max_x-self.min_x)*(self.max_y-self.min_y) + (self.max_x - self.min_x) * (self.max_y - self.min_y) } ///Returns whether the point (x,y) is inside the `RenderBox` @@ -33,18 +56,17 @@ impl RenderBox { } } - -/// Render the Complex plane c into the 32-bit pixel buffer by applying the Mandelbrot formula iteratively to every Complex point mapped to a pixel in the buffer. +/// Render the Complex plane c into the 32-bit pixel buffer by applying the Mandelbrot formula iteratively to every Complex point mapped to a pixel in the buffer. /// The buffer should have a size of width*height. /// `orbit_radius` determines when Zn is considered to have gone to infinity. /// `max_iterations` concerns the maximum amount of times the Mandelbrot formula will be applied to each Complex number. /// Note: This function is computationally intensive, and should not be used for translations -pub fn render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &ComplexPlane, m: &MandelbrotSet, supersampling_amount: u8,coloring_function: fn(iterations: u32, max_iterations: u32) -> TrueColor) { - let render_box = RenderBox::new(0, p.pixel_plane.width, 0, p.pixel_plane.height); - render_box_render_complex_plane_into_buffer(p, c, m, render_box, supersampling_amount,coloring_function); +pub fn render_complex_plane_into_buffer(mandelbrot_model: &mut MandelbrotModel) { + let render_box = RenderBox::new(0, mandelbrot_model.p.pixel_plane.width, 0, mandelbrot_model.p.pixel_plane.height); + render_box_render_complex_plane_into_buffer(mandelbrot_model, render_box); } -/// Render the Complex plane c into the 32-bit pixel buffer by applying the Mandelbrot formula iteratively to every Complex point mapped to a pixel in the buffer. +/// Render the Complex plane c into the 32-bit pixel buffer by applying the Mandelbrot formula iteratively to every Complex point mapped to a pixel in the buffer. /// The buffer should have a size of width*height. /// Only renders Pixels inside the render box denoted by `render_min_x`, `render_max_x`, `render_min_y`, `render_max_y` /// `orbit_radius` determines when Zn is considered to have gone to infinity. @@ -52,15 +74,16 @@ pub fn render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &ComplexPlane, m /// Note: This function is computationally intensive, and should not be used for translations /// Note: This function is multithreaded /// * `coloring_function` - e.g. `TrueColor::new_from_hsv` -/// # Panics +/// # Panics /// If `lock().unwrap()` panics -pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &ComplexPlane, m: &MandelbrotSet, render_box: RenderBox, supersampling_amount: u8, coloring_function: fn(iterations: u32, max_iterations: u32) -> TrueColor) { +pub fn render_box_render_complex_plane_into_buffer(mandelbrot_model: &mut MandelbrotModel, render_box: RenderBox) { let time = benchmark_start(); - let supersampling_amount = supersampling_amount.clamp(1, 64); //Supersampling_amount should be at least 1 and atmost 64 + let supersampling_amount = mandelbrot_model.config.supersampling_amount.clamp(1, 64); //Supersampling_amount should be at least 1 and atmost 64 render_box.print(); + println!("View: {:?}", mandelbrot_model.c.get_view()); println!("SSAA: {}x", supersampling_amount); - let chunk_size = p.pixel_plane.width; - let chunks: Vec> = p.colors.chunks(chunk_size).map(ToOwned::to_owned).collect(); + let chunk_size = mandelbrot_model.p.pixel_plane.width; + let chunks: Vec> = mandelbrot_model.p.colors.chunks(chunk_size).map(ToOwned::to_owned).collect(); let chunks_len = chunks.len(); //println!("chunks.len(): {}", chunks.len()); let mut handles = Vec::new(); @@ -68,15 +91,16 @@ pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &Comp let global_mutex = Arc::new(Mutex::new(0)); let max_progress: u8 = 30; let chunks_len_over_max_progress = chunks_len / max_progress as usize; - let current_progress_atomic: Arc>= Arc::new(Mutex::new(AtomicU8::new(0))); + let current_progress_atomic: Arc> = Arc::new(Mutex::new(AtomicU8::new(0))); for _thread_id in 0..amount_of_threads { - let plane = (*c).clone(); + let plane = (mandelbrot_model.c).clone(); let buf = chunks.clone(); let thread_mutex = Arc::clone(&global_mutex); - let pixel_buffer = (*p).clone(); - let ms = (*m).clone(); + let pixel_buffer = (mandelbrot_model.p).clone(); + let ms = (mandelbrot_model.m).clone(); let atm = Arc::clone(¤t_progress_atomic); + let coloring_function = (mandelbrot_model.coloring_function).clone(); let handle = thread::spawn(move || { let mut thread_chunks = Vec::new(); @@ -84,7 +108,7 @@ pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &Comp loop { let mut data = thread_mutex.lock().unwrap(); let current_chunk = *data; - *data+=1; + *data += 1; drop(data); if current_chunk >= chunks_len { return thread_chunks; @@ -93,19 +117,17 @@ pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &Comp if current_chunk % chunks_len_over_max_progress == 0 { let current_progress = atm.lock().unwrap().load(Ordering::Relaxed); print_progress_bar(current_progress, max_progress); - if current_progress < u8::MAX - { - atm.lock().unwrap().store(current_progress+1, Ordering::Relaxed); + if current_progress < u8::MAX { + atm.lock().unwrap().store(current_progress + 1, Ordering::Relaxed); } } let chunk_start = chunk_size * current_chunk; let mut chunk = buf[current_chunk].clone(); - + for (i, pixel) in chunk.iter_mut().enumerate() { let point = pixel_buffer.index_to_point(i + chunk_start); - if !render_box.contains(point) - { + if !render_box.contains(point) { continue; //Do not render Pixel points outside of the render box } let original_x: f64 = f64::from(point.0 as u32); @@ -113,12 +135,12 @@ pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &Comp //Supersampling, see: https://darkeclipz.github.io/fractals/paper/Fractals%20&%20Rendering%20Techniques.html let mut colors: Vec = Vec::new(); for _ in 0..supersampling_amount { - let (random_x, random_y): (f64, f64) = rand::thread_rng().gen::<(f64,f64)>(); - let (x, y) : (f64, f64) = (original_x+random_x, original_y+random_y); + let (random_x, random_y): (f64, f64) = rand::thread_rng().gen::<(f64, f64)>(); + let (x, y): (f64, f64) = (original_x + random_x, original_y + random_y); let complex = plane.complex_from_pixel_plane(x, y); let iterations = ms.iterate(&complex); let color = coloring_function(iterations, ms.max_iterations); - colors.push(color); + colors.push(color); } let supersampled_color = TrueColor::average(&colors); *pixel = supersampled_color; @@ -131,31 +153,48 @@ pub fn render_box_render_complex_plane_into_buffer(p: &mut PixelBuffer, c: &Comp for handle in handles { let thread_chunks = handle.join().unwrap(); - for (i, chunk) in thread_chunks{ - let mut index = i*p.pixel_plane.width; + for (i, chunk) in thread_chunks { + let mut index = i * mandelbrot_model.p.pixel_plane.width; for color in chunk { - p.colors[index] = color; - index+=1; + mandelbrot_model.p.colors[index] = color; + index += 1; } } } - p.update_pixels(); + mandelbrot_model.p.update_pixels(); println!(); benchmark("render_box_render_complex_plane_into_buffer()", time); } -pub fn translate_and_render_complex_plane_buffer(p: &mut PixelBuffer, c: &ComplexPlane, m: &MandelbrotSet, rows: i128, columns: i128, supersampling_amount: u8,coloring_function: fn(iterations: u32, max_iterations: u32) -> TrueColor) { - println!("rows: {}, columns: {}",rows, columns); - let max_x: usize = if columns > 0 {columns as usize} else {p.pixel_plane.width-1}; - let max_y: usize = if rows > 0 {rows as usize} else {p.pixel_plane.height-1}; - p.translate_buffer(rows, columns); +pub fn translate_and_render_complex_plane_buffer(mandelbrot_model: &mut MandelbrotModel, rows: i128, columns: i128) { + println!("rows: {}, columns: {}", rows, columns); + let max_x: usize = if columns > 0 { + columns as usize + } else { + mandelbrot_model.p.pixel_plane.width - 1 + }; + let max_y: usize = if rows > 0 { + rows as usize + } else { + mandelbrot_model.p.pixel_plane.height - 1 + }; + mandelbrot_model.p.translate_buffer(rows, columns); if rows == 0 { - let render_box = RenderBox::new((max_x as i128-columns.abs()) as usize, max_x, 0, p.pixel_plane.height); - render_box_render_complex_plane_into_buffer(p, c, m, render_box, supersampling_amount, coloring_function); - } - else if columns == 0 { - let render_box = RenderBox::new(0, p.pixel_plane.width, (max_y as i128 -rows.abs()) as usize, max_y); - render_box_render_complex_plane_into_buffer(p, c, m, render_box, supersampling_amount, coloring_function); + let render_box = RenderBox::new( + (max_x as i128 - columns.abs()) as usize, + max_x, + 0, + mandelbrot_model.p.pixel_plane.height, + ); + render_box_render_complex_plane_into_buffer(mandelbrot_model, render_box); + } else if columns == 0 { + let render_box = RenderBox::new( + 0, + mandelbrot_model.p.pixel_plane.width, + (max_y as i128 - rows.abs()) as usize, + max_y, + ); + render_box_render_complex_plane_into_buffer(mandelbrot_model, render_box); } else { println!("ERROR: translate_and_render_complex_plane_buffer() requires that rows == 0 || columns == 0"); } @@ -163,48 +202,65 @@ pub fn translate_and_render_complex_plane_buffer(p: &mut PixelBuffer, c: &Comple ///# Panics /// If `rows_up` != 0 && `columns_right` != 0 -pub fn translate_and_render_efficiently(c: &mut ComplexPlane, p: &mut PixelBuffer, m: &MandelbrotSet, rows_up: i16, columns_right: i16, supersampling_amount: u8,coloring_function: fn(iterations: u32, max_iterations: u32) -> TrueColor) { - assert!(rows_up == 0 || columns_right == 0, "translate_and_render_efficiently: rows_up should be 0 or columns_right should be 0!"); +pub fn translate_and_render_efficiently(mandelbrot_model: &mut MandelbrotModel, rows_up: i16, columns_right: i16) { + assert!( + rows_up == 0 || columns_right == 0, + "translate_and_render_efficiently: rows_up should be 0 or columns_right should be 0!" + ); - let row_sign: f64 = if rows_up > 0 {-1.0} else {1.0}; - let column_sign: f64 = if columns_right > 0 {1.0} else {-1.0}; - c.translate(column_sign*c.pixels_to_real(columns_right.unsigned_abs() as u8), row_sign*c.pixels_to_imaginary(rows_up.unsigned_abs() as u8)); - translate_and_render_complex_plane_buffer(p, c, m, rows_up.into(), (-columns_right).into(), supersampling_amount, coloring_function); + let row_sign: f64 = if rows_up > 0 { -1.0 } else { 1.0 }; + let column_sign: f64 = if columns_right > 0 { 1.0 } else { -1.0 }; + mandelbrot_model.c.translate( + column_sign * mandelbrot_model.c.pixels_to_real(columns_right.unsigned_abs() as u8), + row_sign * mandelbrot_model.c.pixels_to_imaginary(rows_up.unsigned_abs() as u8), + ); + translate_and_render_complex_plane_buffer(mandelbrot_model, rows_up.into(), (-columns_right).into()); } -pub fn translate_to_center_and_render_efficiently(c: &mut ComplexPlane, p: &mut PixelBuffer, m: &MandelbrotSet, new_center: &Complex, supersampling_amount: u8, coloring_function: fn(iterations: u32, max_iterations: u32) -> TrueColor) { - let mut translation: Complex = new_center.subtract(&c.center()); +pub fn translate_to_center_and_render_efficiently(mandelbrot_model: &mut MandelbrotModel, new_center: &Complex) { + let mut translation: Complex = new_center.subtract(&mandelbrot_model.c.center()); //Mirror the y translation because the screen y is mirrored compared to the complex plane y axis translation.y = -translation.y; //Translate x, to the right - c.translate(translation.x, 0.0); - let columns_right = -c.real_to_pixels(translation.x); + mandelbrot_model.c.translate(translation.x, 0.0); + let columns_right = -mandelbrot_model.c.real_to_pixels(translation.x); dbg!(columns_right); - translate_and_render_complex_plane_buffer(p, c, m, 0, columns_right.into(), supersampling_amount, coloring_function); + translate_and_render_complex_plane_buffer(mandelbrot_model, 0, columns_right.into()); //Translate y, up - c.translate(0.0, translation.y); - let rows_up = -c.imaginary_to_pixels(translation.y); + mandelbrot_model.c.translate(0.0, translation.y); + let rows_up = -mandelbrot_model.c.imaginary_to_pixels(translation.y); dbg!(rows_up); - translate_and_render_complex_plane_buffer(p, c, m, rows_up.into(), 0, supersampling_amount, coloring_function); + translate_and_render_complex_plane_buffer(mandelbrot_model, rows_up.into(), 0); } fn benchmark_start() -> Instant { Instant::now() } -fn benchmark(function: &str,time: Instant) { - println!("[Benchmark] {}: {:.2?}",function, time.elapsed()); +fn benchmark(function: &str, time: Instant) { + println!("[Benchmark] {}: {:.2?}", function, time.elapsed()); } ///Prints a progress bar on the current line, will print over the contents of the cursor's current line, so make sure the function is given a newline to print over fn print_progress_bar(current_progress: u8, max_progress: u8) { print!("\rProgress: ["); //Print a \r carriage return to return the cursor to the beginning of the line: https://stackoverflow.com/questions/59890270/how-do-i-overwrite-console-output for i in 0..max_progress { - let symbol = if i <= current_progress {'+'} else {'.'}; + let symbol = if i <= current_progress { '+' } else { '.' }; print!("{}", symbol); } print!("]"); io::stdout().flush().unwrap(); -} \ No newline at end of file +} + +pub fn render(mandelbrot_model: &mut MandelbrotModel) { + //TODO: Observer pattern view -> model to update the view, instead of rendering manually + render_complex_plane_into_buffer(mandelbrot_model); + mandelbrot_model.c.print(); +} + +pub fn set_view(mandelbrot_model: &mut MandelbrotModel, view: &View) { + mandelbrot_model.c.set_view(view); + render(mandelbrot_model); +} diff --git a/src/view/image.rs b/src/view/image.rs new file mode 100644 index 0000000..b8c7cbc --- /dev/null +++ b/src/view/image.rs @@ -0,0 +1,35 @@ +use crate::model::mandelbrot_model::MandelbrotModel; + +pub fn save(mandelbrot_model: &MandelbrotModel) { + let time_stamp = chrono::Utc::now().to_string(); + if mandelbrot_model.config.window_scale == 1.0 { + mandelbrot_model.p.save_as_png( + &time_stamp, + &mandelbrot_model.c.get_view(), + &mandelbrot_model.m, + mandelbrot_model.config.supersampling_amount, + ); + } else { + println!( + "[UNSUPPORTED]: There is currently no support for saving images in the window scale is not 1.0. The window scale is {}.", + mandelbrot_model.config.window_scale + ) + } + + /*else { + let mut image_p: PixelBuffer = PixelBuffer::new(PixelPlane::new( + mandelbrot_model.config.image_width, + mandelbrot_model.config.image_height, + )); + let mut image_c: ComplexPlane = ComplexPlane::new(mandelbrot_model.config.image_width, mandelbrot_model.config.image_height); + image_p.color_channel_mapping = mandelbrot_model.p.color_channel_mapping; + image_c.set_view(&mandelbrot_model.c.get_view()); + rendering::render_complex_plane_into_buffer(mandelbrot_model); + image_p.save_as_png( + &time_stamp, + &mandelbrot_model.c.get_view(), + &mandelbrot_model.m, + mandelbrot_model.config.supersampling_amount, + ); + }*/ +} diff --git a/src/view/minifb_mandelbrot_view.rs b/src/view/minifb_mandelbrot_view.rs new file mode 100644 index 0000000..b5ab171 --- /dev/null +++ b/src/view/minifb_mandelbrot_view.rs @@ -0,0 +1,75 @@ +use std::str::FromStr; + +use minifb::{Icon, Key, Window, WindowOptions}; + +use crate::{model::mandelbrot_model::MandelbrotModel, VERSION, WINDOW_TITLE}; + +pub struct MandelbrotView { + pub window: Window, +} + +impl MandelbrotView { + pub fn new(mandelbrot_model: &MandelbrotModel) -> MandelbrotView { + // Create a new window + let mut window = Window::new( + &format!("{} v{}", WINDOW_TITLE, VERSION), + mandelbrot_model.config.window_width, + mandelbrot_model.config.window_height, + WindowOptions { + borderless: false, + title: true, + resize: true, + scale: minifb::Scale::X1, + scale_mode: minifb::ScaleMode::Center, + topmost: false, + transparency: false, + none: false, + }, + ) + .unwrap_or_else(|e| { + panic!("{}", e); + }); + //Set window background color to darkgray + window.set_background_color(27, 27, 27); + + #[cfg(target_os = "windows")] + window.set_icon(Icon::from_str("icons/rust.ico").unwrap()); + + //Print info about the MandelbrotModel + mandelbrot_model.p.pixel_plane.print(); + mandelbrot_model.c.print(); + println!( + "Mandelbrot set parameters: max. iterations is {} and orbit radius is {}", + mandelbrot_model.config.max_iterations, mandelbrot_model.config.orbit_radius + ); + println!( + "Amount of CPU threads that will be used for rendering: {}", + mandelbrot_model.amount_of_threads + ); + println!( + "Supersampling amount used for rendering: {}x", + mandelbrot_model.config.supersampling_amount + ); + println!(); + + MandelbrotView { window } + } + + pub fn update(&mut self, mandelbrot_model: &MandelbrotModel) { + self.window + .update_with_buffer( + &mandelbrot_model.p.pixels, + mandelbrot_model.config.window_width, + mandelbrot_model.config.window_height, + ) + .unwrap(); + } + + pub fn exit(&self) { + if self.window.is_key_down(Key::Escape) { + println!("Escape pressed, application exited gracefully."); + } else { + println!("Window closed, application exited gracefully.") + } + } +} diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..6a27261 --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,3 @@ +pub mod image; +pub mod minifb_mandelbrot_view; +pub mod terminal; diff --git a/src/view/terminal.rs b/src/view/terminal.rs new file mode 100644 index 0000000..65bc2ce --- /dev/null +++ b/src/view/terminal.rs @@ -0,0 +1,28 @@ +///Prints Mandelbrot ASCII art :)
+///Prints the `application_banner`, `author_banner`, and `version` +pub fn print_banner(version: &str) { + //Made using: https://patorjk.com/software/taag/#MandelbrotModel::get_instance().p=display&f=Big&t=Mandelbrot + let application_banner = r" +__ __ _ _ _ _ +| \/ | | | | | | | | +| \ / | __ _ _ __ __| | ___| | |__ _ __ ___ | |_ +| |\/| |/ _` | '_ \ / _` |/ _ \ | '_ \| '__/ _ \| __| +| | | | (_| | | | | (_| | __/ | |_) | | | (_) | |_ +|_| |_|\__,_|_| |_|\__,_|\___|_|_.__/|_| \___/ \__|"; + //Made using: https://patorjk.com/software/taag/#MandelbrotModel::get_instance().p=display&f=Small%20Slant&t=by%20Jort + let author_banner = r" + __ __ __ + / / __ __ __ / /__ ____/ /_ + / _ \/ // / / // / _ \/ __/ __/ +/_.__/\_, / \___/\___/_/ \__/ + /___/ "; + println!("{}{}v{}\n\n", application_banner, author_banner, version); +} + +///Prints a command info tip for the users benefit +pub fn print_command_info() { + let tip = "Run Mandelbrot using:"; + let command = "cargo run --release -- "; + let command_info = "where means substitute with the value of arg\nuse '-' to use the default value of arg"; + println!("{}\n\t{}\n{}\n", tip, command, command_info); +}