diff --git a/.gitignore b/.gitignore index 6e6bb4e..e02e30c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ /target -*.zip -*.xz - *.png -!images/* +!images/*.png fractal_settings.json diff --git a/Cargo.lock b/Cargo.lock index 430bc5a..21f6d53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,40 +608,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", -] - [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1271,7 +1237,6 @@ dependencies = [ "image", "native-dialog", "rand", - "rayon", "serde", "serde_json", ] @@ -1378,15 +1343,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1549,16 +1505,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" version = "0.5.11" @@ -1857,28 +1803,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index ad12788..ac55378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ eframe = "0.22.0" image = { version = "0.24.6", default-features = false, features = ["png"] } native-dialog = "0.6.4" rand = "0.8.5" -rayon = "1.7.0" serde = { version = "1.0.171", features = ["derive"] } serde_json = "1.0.102" diff --git a/Makefile b/Makefile deleted file mode 100644 index 0af9c66..0000000 --- a/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -APP_NAME=julia-fractal-renderer - -WIN_TARGET=x86_64-pc-windows-gnu -RELEASE_W=target/$(WIN_TARGET)/release -RELEASE_L=target/release - -release: release_windows release_linux - -release_linux: - cargo build --release - cd $(RELEASE_L) && tar -caf $(APP_NAME)-linux.tar.xz $(APP_NAME) - mv $(RELEASE_L)/$(APP_NAME)-linux.tar.xz . - -release_windows: - cargo build --release --target $(WIN_TARGET) - cd $(RELEASE_W) && zip -9 $(APP_NAME)-windows.zip $(APP_NAME).exe - mv $(RELEASE_W)/$(APP_NAME)-windows.zip . - diff --git a/src/generate.rs b/src/generate.rs index babe5da..dc2e2b9 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,14 +1,13 @@ use eframe::epaint::Vec2; use image::{Rgb, RgbImage}; -use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize)] pub struct RenderOptions { - pub width: usize, - pub height: usize, + pub width: u32, + pub height: u32, pub unit_width: f64, - pub max_iter: u16, + pub iterations: u32, pub cx: f64, pub cy: f64, pub fill_style: FillStyle, @@ -26,7 +25,7 @@ impl Default for RenderOptions { width: 512, height: 512, unit_width: 4.0, - max_iter: 128, + iterations: 128, cx: 0.4, cy: -0.2, fill_style: FillStyle::Bright, @@ -34,79 +33,57 @@ impl Default for RenderOptions { } } -pub fn render_c(q: &RenderOptions, mut image: RgbImage) -> RgbImage { - let width = q.width as f32; - let height = q.height as f32; - let ppu = width / (q.unit_width as f32); +pub fn view_point(q: &RenderOptions, image: RgbImage) -> RgbImage { + apply_fn(image, q, |x, y| { + let len = (Vec2::new(x as f32, y as f32) - Vec2::new(q.cx as f32, q.cy as f32)).length(); + if len < 0.03 { + Some(Rgb([0, 120, 120])) + } else if len < 0.04 { + Some(Rgb([255; 3])) + } else { + None + } + }) +} - let target = Vec2::new(q.cx as f32, q.cy as f32); +pub fn render(q: &RenderOptions, color: (u8, u8, u8)) -> RgbImage { + let img = RgbImage::new(q.width, q.height); + apply_fn(img, q, |x, y| { + let i = julia(x, y, q.cx, q.cy, q.iterations); + if q.fill_style == FillStyle::Black && i == q.iterations { + None + } else { + let i = i.min(255) as u8; + Some(Rgb([ + i.saturating_mul(color.0), + i.saturating_mul(color.1), + i.saturating_mul(color.2), + ])) + } + }) +} + +fn apply_fn(mut image: RgbImage, q: &RenderOptions, op: F) -> RgbImage +where + F: Fn(f64, f64) -> Option>, +{ + let width = q.width as f64; + let height = q.height as f64; + let ppu = width / q.unit_width; for y in 0..q.height { for x in 0..q.width { - let sx = (x as f32 - width / 2.0) / ppu; - let sy = (y as f32 - height / 2.0) / ppu; - - let len = (Vec2::new(sx, sy) - target).length(); - if len < 0.03 { - image.put_pixel(x as u32, y as u32, Rgb([0, 120, 120])); - } else if len < 0.04 { - image.put_pixel(x as u32, y as u32, Rgb([255; 3])); + let sx = (x as f64 - width / 2.0) / ppu; + let sy = (y as f64 - height / 2.0) / ppu; + if let Some(pixel) = op(sx, sy) { + image.put_pixel(x, y, pixel); } } } image } -pub fn color_iteration(iter: u16, color: (u8, u8, u8)) -> Rgb { - let i = iter.min(255) as u8; - Rgb([ - i.saturating_mul(color.0), - i.saturating_mul(color.1), - i.saturating_mul(color.2), - ]) -} - -pub fn render_julia(q: &RenderOptions, color: (u8, u8, u8)) -> RgbImage { - let mut image = RgbImage::new(q.width as u32, q.height as u32); - - let width = q.width as f64; - let height = q.height as f64; - let ppu = width / q.unit_width; - - let fill = match q.fill_style { - FillStyle::Black => Rgb([0; 3]), - FillStyle::Bright => color_iteration(q.max_iter, color), - }; - - (0..q.height) - .into_par_iter() - .map(|y| { - let mut row = Vec::with_capacity(q.width); - for x in 0..q.width { - let sx = (x as f64 - width / 2.0) / ppu; - let sy = (y as f64 - height / 2.0) / ppu; - let i = julia(sx, sy, q.cx, q.cy, q.max_iter); - - if i == q.max_iter { - row.push(fill); - } else { - row.push(color_iteration(i, color)); - } - } - row - }) - .collect::>() - .into_iter() - .enumerate() - .for_each(|(y, row)| { - for (x, i) in row.into_iter().enumerate() { - image.put_pixel(x as u32, y as u32, i); - } - }); - image -} - -fn julia(mut x: f64, mut y: f64, cx: f64, cy: f64, max_iter: u16) -> u16 { +fn julia(mut x: f64, mut y: f64, cx: f64, cy: f64, max_iter: u32) -> u32 { let mut iter = 0; while (x * x + y * y) < 4.0 && iter < max_iter { (x, y) = ( diff --git a/src/main.rs b/src/main.rs index 2ad86de..82d8118 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use eframe::{ epaint::{TextureHandle, Vec2}, Frame, NativeOptions, }; -use generate::{render_c, render_julia, FillStyle, RenderOptions}; +use generate::{render, view_point, FillStyle, RenderOptions}; use image::EncodableLayout; use native_dialog::FileDialog; use serde::{Deserialize, Serialize}; @@ -47,7 +47,7 @@ struct JuliaGUI { #[serde(skip)] export_render_ms: Option, export_res_power: u8, - export_max_iter: u16, + export_iterations: u32, #[serde(skip)] export_path: PathBuf, #[serde(skip)] @@ -77,7 +77,7 @@ impl Default for JuliaGUI { preview_render_ms: 0.0, export_render_ms: None, export_res_power: 3, - export_max_iter: 512, + export_iterations: 512, export_path: "".into(), settings_changed: true, preview_point: false, @@ -113,7 +113,7 @@ impl JuliaGUI { RenderJob::Exit => break, RenderJob::Render(path, options, color) => { let start_time = SystemTime::now(); - let image = render_julia(&options, color); + let image = render(&options, color); if let Err(err) = image.save(&path) { println!("Failed to save render: {err}"); } @@ -142,9 +142,9 @@ impl JuliaGUI { fn update_preview(&mut self) { let start_time = SystemTime::now(); - let mut frame = render_julia(&self.settings, self.color); + let mut frame = render(&self.settings, self.color); if self.preview_point { - frame = render_c(&self.settings, frame); + frame = view_point(&self.settings, frame); } if let Some(preview) = &mut self.preview { @@ -166,7 +166,7 @@ impl JuliaGUI { let settings = RenderOptions { width: self.settings.width * res_mul, height: self.settings.height * res_mul, - max_iter: self.export_max_iter, + iterations: self.export_iterations, ..self.settings.clone() }; @@ -272,8 +272,8 @@ impl eframe::App for JuliaGUI { let set_blue = ui.add(Slider::new(&mut self.color.2, 0..=16)); ui.label("Preview iterations:"); - let set_iter = - ui.add(Slider::new(&mut self.settings.max_iter, 5..=256).clamp_to_range(false)); + let set_iter = ui + .add(Slider::new(&mut self.settings.iterations, 5..=256).clamp_to_range(false)); ui.horizontal(|ui| { ui.label("Preview resolution:"); @@ -313,7 +313,7 @@ impl eframe::App for JuliaGUI { }); ui.label("Render iterations:"); - ui.add(Slider::new(&mut self.export_max_iter, 5..=1024).clamp_to_range(false)); + ui.add(Slider::new(&mut self.export_iterations, 5..=1024).clamp_to_range(false)); ui.label("Render resolution:"); ui.add(Slider::new(&mut self.export_res_power, 0..=6).clamp_to_range(false)); ui.label(format!( @@ -350,7 +350,7 @@ impl eframe::App for JuliaGUI { let predicted_render_time = (self.preview_render_ms * (1 << (self.export_res_power * 2)) as f64 - * (self.export_max_iter as f64 / self.settings.max_iter as f64) + * (self.export_iterations as f64 / self.settings.iterations as f64) / 1000.0) .floor(); if predicted_render_time < 60.0 {