mirror of
https://github.com/CrispyPin/julia-fractal-renderer.git
synced 2025-04-03 17:04:01 +02:00
Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
1eb3c04c7f | |||
0052bd1686 | |||
af608fdab0 | |||
f4394ad273 | |||
12664a5817 | |||
b4b82a3cc5 | |||
153d10ba7a | |||
fc209a78ad |
6 changed files with 260 additions and 949 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
/target
|
||||
*.zip
|
||||
*.xz
|
||||
|
||||
*.png
|
||||
!images/*.png
|
||||
!images/*
|
||||
fractal_settings.json
|
||||
|
|
913
Cargo.lock
generated
913
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,16 @@
|
|||
[package]
|
||||
name = "julia-fractal-renderer"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.22.0"
|
||||
eframe = { version = "0.22.0", default-features = false, features = ["glow", "default_fonts"] }
|
||||
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"
|
||||
|
||||
|
|
18
Makefile
Normal file
18
Makefile
Normal file
|
@ -0,0 +1,18 @@
|
|||
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 .
|
||||
|
122
src/generate.rs
122
src/generate.rs
|
@ -1,16 +1,20 @@
|
|||
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: u32,
|
||||
pub height: u32,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub unit_width: f64,
|
||||
pub iterations: u32,
|
||||
#[serde(alias = "iterations")]
|
||||
pub max_iter: u16,
|
||||
pub cx: f64,
|
||||
pub cy: f64,
|
||||
pub fill_style: FillStyle,
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
@ -25,65 +29,93 @@ impl Default for RenderOptions {
|
|||
width: 512,
|
||||
height: 512,
|
||||
unit_width: 4.0,
|
||||
iterations: 128,
|
||||
max_iter: 128,
|
||||
cx: 0.4,
|
||||
cy: -0.2,
|
||||
fill_style: FillStyle::Bright,
|
||||
invert: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
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 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<F>(mut image: RgbImage, q: &RenderOptions, op: F) -> RgbImage
|
||||
where
|
||||
F: Fn(f64, f64) -> Option<Rgb<u8>>,
|
||||
{
|
||||
let width = q.width as f64;
|
||||
let height = q.height as f64;
|
||||
let ppu = width / q.unit_width;
|
||||
let target = Vec2::new(q.cx as f32, q.cy as f32);
|
||||
|
||||
for y in 0..q.height {
|
||||
for x in 0..q.width {
|
||||
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);
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
fn julia(mut x: f64, mut y: f64, cx: f64, cy: f64, max_iter: u32) -> u32 {
|
||||
pub fn color_iteration(iter: u16, color: (u8, u8, u8), invert: bool) -> Rgb<u8> {
|
||||
let i = iter.min(255) as u8;
|
||||
let (r, g, b) = (
|
||||
i.saturating_mul(color.0),
|
||||
i.saturating_mul(color.1),
|
||||
i.saturating_mul(color.2),
|
||||
);
|
||||
if invert {
|
||||
Rgb([255 - r, 255 - g, 255 - b])
|
||||
} else {
|
||||
Rgb([r, g, b])
|
||||
}
|
||||
}
|
||||
|
||||
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([q.invert as u8 * 255; 3]),
|
||||
FillStyle::Bright => color_iteration(q.max_iter, color, q.invert),
|
||||
};
|
||||
|
||||
(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, q.invert));
|
||||
}
|
||||
}
|
||||
row
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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 {
|
||||
let mut iter = 0;
|
||||
while (x * x + y * y) < 4.0 && iter < max_iter {
|
||||
(x, y) = (
|
||||
|
|
146
src/main.rs
146
src/main.rs
|
@ -13,7 +13,7 @@ use eframe::{
|
|||
epaint::{TextureHandle, Vec2},
|
||||
Frame, NativeOptions,
|
||||
};
|
||||
use generate::{render, view_point, FillStyle, RenderOptions};
|
||||
use generate::{render_c, render_julia, FillStyle, RenderOptions};
|
||||
use image::EncodableLayout;
|
||||
use native_dialog::FileDialog;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -39,20 +39,25 @@ fn main() {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
struct JuliaGUI {
|
||||
color: (u8, u8, u8),
|
||||
settings: RenderOptions,
|
||||
export_res_power: u8,
|
||||
#[serde(alias = "export_iterations")]
|
||||
export_max_iter: u16,
|
||||
preview_point: bool,
|
||||
#[serde(default = "default_color_presets")]
|
||||
color_presets: Vec<(String, (u8, u8, u8))>,
|
||||
#[serde(skip)]
|
||||
preview: Option<TextureHandle>,
|
||||
settings: RenderOptions,
|
||||
#[serde(skip)]
|
||||
preview_render_ms: f64,
|
||||
#[serde(skip)]
|
||||
export_render_ms: Option<f64>,
|
||||
export_res_power: u8,
|
||||
export_iterations: u32,
|
||||
#[serde(skip)]
|
||||
export_path: PathBuf,
|
||||
#[serde(skip)]
|
||||
settings_changed: bool,
|
||||
preview_point: bool,
|
||||
#[serde(skip)]
|
||||
new_color_preset_name: String,
|
||||
#[serde(skip)]
|
||||
render_thread_handle: Option<JoinHandle<()>>,
|
||||
#[serde(skip)]
|
||||
|
@ -63,6 +68,17 @@ struct JuliaGUI {
|
|||
waiting: bool,
|
||||
}
|
||||
|
||||
fn default_color_presets() -> Vec<(String, (u8, u8, u8))> {
|
||||
vec![
|
||||
("pink".into(), (8, 2, 6)),
|
||||
("blue".into(), (2, 4, 8)),
|
||||
("green".into(), (2, 8, 4)),
|
||||
("salmon".into(), (8, 4, 4)),
|
||||
("purple".into(), (5, 2, 11)),
|
||||
("yellow".into(), (9, 6, 1)),
|
||||
]
|
||||
}
|
||||
|
||||
enum RenderJob {
|
||||
Render(PathBuf, RenderOptions, (u8, u8, u8)),
|
||||
Exit,
|
||||
|
@ -72,13 +88,15 @@ impl Default for JuliaGUI {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
color: (12, 5, 10),
|
||||
color_presets: default_color_presets(),
|
||||
new_color_preset_name: String::new(),
|
||||
preview: None,
|
||||
settings: RenderOptions::default(),
|
||||
preview_render_ms: 0.0,
|
||||
export_render_ms: None,
|
||||
export_res_power: 3,
|
||||
export_iterations: 512,
|
||||
export_path: "".into(),
|
||||
export_max_iter: 512,
|
||||
export_path: PathBuf::new(),
|
||||
settings_changed: true,
|
||||
preview_point: false,
|
||||
render_thread_handle: None,
|
||||
|
@ -113,7 +131,7 @@ impl JuliaGUI {
|
|||
RenderJob::Exit => break,
|
||||
RenderJob::Render(path, options, color) => {
|
||||
let start_time = SystemTime::now();
|
||||
let image = render(&options, color);
|
||||
let image = render_julia(&options, color);
|
||||
if let Err(err) = image.save(&path) {
|
||||
println!("Failed to save render: {err}");
|
||||
}
|
||||
|
@ -142,9 +160,9 @@ impl JuliaGUI {
|
|||
|
||||
fn update_preview(&mut self) {
|
||||
let start_time = SystemTime::now();
|
||||
let mut frame = render(&self.settings, self.color);
|
||||
let mut frame = render_julia(&self.settings, self.color);
|
||||
if self.preview_point {
|
||||
frame = view_point(&self.settings, frame);
|
||||
frame = render_c(&self.settings, frame);
|
||||
}
|
||||
|
||||
if let Some(preview) = &mut self.preview {
|
||||
|
@ -166,7 +184,7 @@ impl JuliaGUI {
|
|||
let settings = RenderOptions {
|
||||
width: self.settings.width * res_mul,
|
||||
height: self.settings.height * res_mul,
|
||||
iterations: self.export_iterations,
|
||||
max_iter: self.export_max_iter,
|
||||
..self.settings.clone()
|
||||
};
|
||||
|
||||
|
@ -211,51 +229,46 @@ impl eframe::App for JuliaGUI {
|
|||
.exact_width(200.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(format!(
|
||||
"Preview render took {:.2}ms",
|
||||
self.preview_render_ms
|
||||
"Preview render took {}",
|
||||
format_time(self.preview_render_ms, true)
|
||||
));
|
||||
|
||||
let set_point_vis = ui.checkbox(&mut self.preview_point, "View C point");
|
||||
ui.label("C point (X, Y):");
|
||||
let set_point_vis = ui.checkbox(&mut self.preview_point, "View offset point");
|
||||
ui.label("Offset point (x, y):");
|
||||
let set_cx =
|
||||
ui.add(Slider::new(&mut self.settings.cx, -1.0..=1.0).clamp_to_range(false));
|
||||
let set_cy =
|
||||
ui.add(Slider::new(&mut self.settings.cy, -1.0..=1.0).clamp_to_range(false));
|
||||
ui.label("render width:");
|
||||
ui.label("Width:");
|
||||
let set_unit_width = ui.add(Slider::new(&mut self.settings.unit_width, 0.1..=6.0));
|
||||
ui.label("Fill style:");
|
||||
ui.horizontal(|ui| {
|
||||
let set_black =
|
||||
ui.radio_value(&mut self.settings.fill_style, FillStyle::Black, "Black");
|
||||
let set_bright =
|
||||
ui.radio_value(&mut self.settings.fill_style, FillStyle::Bright, "Bright");
|
||||
if set_bright.changed() || set_black.changed() {
|
||||
self.settings_changed = true;
|
||||
}
|
||||
ui.label("Fill style:");
|
||||
self.settings_changed |= ui
|
||||
.radio_value(&mut self.settings.fill_style, FillStyle::Black, "Black")
|
||||
.changed();
|
||||
self.settings_changed |= ui
|
||||
.radio_value(&mut self.settings.fill_style, FillStyle::Bright, "Bright")
|
||||
.changed();
|
||||
});
|
||||
let set_invert = ui.checkbox(&mut self.settings.invert, "invert");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Colour (RGB)");
|
||||
ui.menu_button("presets", |ui| {
|
||||
if ui.button("pink").clicked() {
|
||||
self.color = (8, 2, 6);
|
||||
self.settings_changed = true;
|
||||
let mut to_remove = None;
|
||||
for (i, (name, col)) in self.color_presets.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(name).clicked() {
|
||||
self.color = *col;
|
||||
self.settings_changed = true;
|
||||
}
|
||||
if ui.button("x").clicked() {
|
||||
to_remove = Some(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ui.button("blue").clicked() {
|
||||
self.color = (2, 4, 8);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
if ui.button("green").clicked() {
|
||||
self.color = (2, 8, 4);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
if ui.button("salmon").clicked() {
|
||||
self.color = (8, 4, 4);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
if ui.button("purple").clicked() {
|
||||
self.color = (5, 2, 11);
|
||||
self.settings_changed = true;
|
||||
if let Some(i) = to_remove {
|
||||
self.color_presets.remove(i);
|
||||
}
|
||||
if ui.button("randomise").clicked() {
|
||||
self.color = (
|
||||
|
@ -265,6 +278,13 @@ impl eframe::App for JuliaGUI {
|
|||
);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.text_edit_singleline(&mut self.new_color_preset_name);
|
||||
if ui.button("add").clicked() {
|
||||
self.color_presets
|
||||
.push((self.new_color_preset_name.clone(), self.color));
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
let set_red = ui.add(Slider::new(&mut self.color.0, 0..=16));
|
||||
|
@ -272,8 +292,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.iterations, 5..=256).clamp_to_range(false));
|
||||
let set_iter =
|
||||
ui.add(Slider::new(&mut self.settings.max_iter, 5..=256).clamp_to_range(false));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Preview resolution:");
|
||||
|
@ -313,7 +333,7 @@ impl eframe::App for JuliaGUI {
|
|||
});
|
||||
|
||||
ui.label("Render iterations:");
|
||||
ui.add(Slider::new(&mut self.export_iterations, 5..=1024).clamp_to_range(false));
|
||||
ui.add(Slider::new(&mut self.export_max_iter, 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,24 +370,19 @@ impl eframe::App for JuliaGUI {
|
|||
|
||||
let predicted_render_time = (self.preview_render_ms
|
||||
* (1 << (self.export_res_power * 2)) as f64
|
||||
* (self.export_iterations as f64 / self.settings.iterations as f64)
|
||||
/ 1000.0)
|
||||
* (self.export_max_iter as f64 / self.settings.max_iter as f64))
|
||||
.floor();
|
||||
if predicted_render_time < 60.0 {
|
||||
ui.label(format!("Predicted render time: {predicted_render_time}s"));
|
||||
} else {
|
||||
ui.label(format!(
|
||||
"Predicted render time: {:.1} min",
|
||||
predicted_render_time / 60.0
|
||||
));
|
||||
}
|
||||
ui.label(format!(
|
||||
"Predicted render time: {}",
|
||||
format_time(predicted_render_time, false)
|
||||
));
|
||||
|
||||
ui.label(
|
||||
self.export_render_ms
|
||||
.map(|ms| format!("took {ms:.2}ms"))
|
||||
.map(|ms| format!("Took {}", format_time(ms, true)))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
ui.label(format!("version {}", env!("CARGO_PKG_VERSION")));
|
||||
ui.label(format!("Version: {}", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
if set_cx.changed()
|
||||
|| set_cy.changed() || set_unit_width.changed()
|
||||
|
@ -375,6 +390,7 @@ impl eframe::App for JuliaGUI {
|
|||
|| set_red.changed() || set_green.changed()
|
||||
|| set_blue.changed()
|
||||
|| set_point_vis.changed()
|
||||
|| set_invert.changed()
|
||||
{
|
||||
self.settings_changed = true;
|
||||
}
|
||||
|
@ -388,10 +404,24 @@ impl eframe::App for JuliaGUI {
|
|||
}
|
||||
|
||||
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
|
||||
self.save_settings();
|
||||
if let Some(channel) = &self.render_thread {
|
||||
channel.send(RenderJob::Exit).unwrap();
|
||||
}
|
||||
self.render_thread_handle.take().unwrap().join().unwrap();
|
||||
self.save_settings();
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(ms: f64, precise: bool) -> String {
|
||||
if ms < 1000.0 {
|
||||
if precise {
|
||||
format!("{ms:.2}ms")
|
||||
} else {
|
||||
"<1s".into()
|
||||
}
|
||||
} else if ms < 60_000.0 {
|
||||
format!("{:.1}s", ms / 1000.0)
|
||||
} else {
|
||||
format!("{:.1}m", ms / 60_000.0)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue