feat: improve UI/UX

This commit is contained in:
2026-09-20 13:13:42 +02:00
parent 95b85fdfae
commit 2110d038a1
+40 -29
View File
@@ -43,6 +43,7 @@ const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"];
pub enum FractalMode { pub enum FractalMode {
Mandelbrot, Mandelbrot,
Julia, Julia,
Buddhabrot,
} }
/// Selectable fractal formulas, with UI labels. /// Selectable fractal formulas, with UI labels.
@@ -307,10 +308,6 @@ pub struct FractalApp {
/// List of enabled lights in the world /// List of enabled lights in the world
lights: Vec<Light>, lights: Vec<Light>,
/// Render as a Buddhabrot (Monte-Carlo orbit-density histogram) instead of
/// the ordinary escape-time set. Plain f32 view — no deep zoom, no
/// perturbation/reference-orbit machinery (see `fractal::buddhabrot`).
buddhabrot: bool,
/// Nested escape-iteration caps for the R/G/B histogram channels /// Nested escape-iteration caps for the R/G/B histogram channels
/// (Nebulabrot coloring); kept ordered r <= g <= b by the UI. /// (Nebulabrot coloring); kept ordered r <= g <= b by the UI.
buddha_r_cap: u32, buddha_r_cap: u32,
@@ -436,6 +433,8 @@ impl FractalApp {
} }
} }
cc.egui_ctx.set_zoom_factor(1.1);
// Debug/testing hooks, driven by CLI flags. // Debug/testing hooks, driven by CLI flags.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
app.apply_cli(Cli::parse()); app.apply_cli(Cli::parse());
@@ -475,7 +474,6 @@ impl FractalApp {
de_coloring: false, de_coloring: false,
shadow: false, shadow: false,
lights: vec![Light::default()], lights: vec![Light::default()],
buddhabrot: false,
buddha_r_cap: 50, buddha_r_cap: 50,
buddha_g_cap: 500, buddha_g_cap: 500,
buddha_b_cap: 2000, buddha_b_cap: 2000,
@@ -557,7 +555,7 @@ impl FractalApp {
self.de_coloring = true; self.de_coloring = true;
} }
if cli.buddhabrot { if cli.buddhabrot {
self.buddhabrot = true; self.mode = FractalMode::Buddhabrot;
} }
if let Some(p) = cli.buddha_palette { if let Some(p) = cli.buddha_palette {
self.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1); self.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1);
@@ -996,7 +994,7 @@ impl FractalApp {
if self.export.is_some() { if self.export.is_some() {
return; // one export at a time return; // one export at a time
} }
if self.buddhabrot { if self.mode == FractalMode::Buddhabrot {
self.status = Some("PNG export isn't available in Buddhabrot mode yet".into()); self.status = Some("PNG export isn't available in Buddhabrot mode yet".into());
return; return;
} }
@@ -1246,22 +1244,22 @@ impl FractalApp {
.strong() .strong()
.heading(), .heading(),
); );
let mode_label = if self.buddhabrot { let mode_label = match self.mode {
"Buddhabrot mode — orbit density (random c, z₀ = 0)" FractalMode::Mandelbrot => {
} else { "Mandelbrot mode — parameter space (c varies per pixel, z₀ = 0)"
match self.mode {
FractalMode::Mandelbrot => {
"Mandelbrot mode — parameter space (c varies per pixel, z₀ = 0)"
}
FractalMode::Julia => {
"Julia mode — dynamical plane for a fixed c (z₀ varies per pixel)"
}
} }
FractalMode::Julia => {
"Julia mode — dynamical plane for a fixed c (z₀ varies per pixel)"
}
FractalMode::Buddhabrot => "Buddhabrot mode — orbit density (random c, z₀ = 0)",
}; };
ui.label(mode_label); ui.label(mode_label);
ui.separator(); ui.separator();
ui.label(format!("formula: {}", kind_formula(self.kind, self.power))); ui.label(format!(
"formula: {}",
kind_formula(self.kind, self.power, self.complex_power)
));
if self.mode == FractalMode::Julia { if self.mode == FractalMode::Julia {
ui.label(format!("c = {:.6} {:+.6}i", self.julia_c.0, self.julia_c.1)); ui.label(format!("c = {:.6} {:+.6}i", self.julia_c.0, self.julia_c.1));
} }
@@ -1503,6 +1501,7 @@ impl FractalApp {
fn controls_ui(&mut self, ui: &mut egui::Ui) { fn controls_ui(&mut self, ui: &mut egui::Ui) {
ui.heading("Fractal Explorer"); ui.heading("Fractal Explorer");
ui.separator(); ui.separator();
ui.add_space(4.);
// Fractal formula. Switching kinds jumps to a sensible default view, // Fractal formula. Switching kinds jumps to a sensible default view,
// since interesting regions differ between fractals. // since interesting regions differ between fractals.
@@ -1569,16 +1568,22 @@ impl FractalApp {
self.view = Self::default_view_for(self.mode, self.kind); self.view = Self::default_view_for(self.mode, self.kind);
} }
ui.checkbox(&mut self.buddhabrot, "Buddhabrot") ui.horizontal(|ui| {
.on_hover_text( ui.radio_value(&mut self.mode, FractalMode::Mandelbrot, "Set");
"Monte-Carlo density of escaping orbits instead of the ordinary \ ui.radio_value(&mut self.mode, FractalMode::Julia, "Julia");
ui.radio_value(&mut self.mode, FractalMode::Buddhabrot, "Buddhabrot")
.on_hover_text(
"Monte-Carlo density of escaping orbits instead of the ordinary \
escape-time set. Plain f32 view (no deep zoom); the image \ escape-time set. Plain f32 view (no deep zoom); the image \
progressively sharpens while the view stays still.", progressively sharpens while the view stays still.",
); );
});
if self.buddhabrot { if self.mode == FractalMode::Buddhabrot {
self.buddhabrot_ui(ui); self.buddhabrot_ui(ui);
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
if ui.button("Reset view").clicked() { if ui.button("Reset view").clicked() {
self.view = Self::default_view_for(self.mode, self.kind); self.view = Self::default_view_for(self.mode, self.kind);
} }
@@ -1587,11 +1592,6 @@ impl FractalApp {
return; return;
} }
ui.horizontal(|ui| {
ui.radio_value(&mut self.mode, FractalMode::Mandelbrot, "Set");
ui.radio_value(&mut self.mode, FractalMode::Julia, "Julia");
});
if self.mode == FractalMode::Julia && self.kind != FractalKind::Lambda { if self.mode == FractalMode::Julia && self.kind != FractalKind::Lambda {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("c ="); ui.label("c =");
@@ -1609,6 +1609,7 @@ impl FractalApp {
}); });
if !JULIA_PRESETS[self.kind as usize].is_empty() { if !JULIA_PRESETS[self.kind as usize].is_empty() {
ui.label("places:");
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
for &(name, re, im, iterations, phoenix) in JULIA_PRESETS[self.kind as usize] { for &(name, re, im, iterations, phoenix) in JULIA_PRESETS[self.kind as usize] {
if ui.small_button(name).clicked() { if ui.small_button(name).clicked() {
@@ -1639,7 +1640,9 @@ impl FractalApp {
}); });
} }
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
ui.checkbox(&mut self.auto_iterations, "Auto iterations") ui.checkbox(&mut self.auto_iterations, "Auto iterations")
.on_hover_text("Scale the iteration count with zoom depth so deep zooms stay sharp."); .on_hover_text("Scale the iteration count with zoom depth so deep zooms stay sharp.");
if self.auto_iterations { if self.auto_iterations {
@@ -1693,7 +1696,9 @@ impl FractalApp {
}); });
}); });
} }
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
ui.checkbox(&mut self.antialias, "Antialiasing (2×2)") ui.checkbox(&mut self.antialias, "Antialiasing (2×2)")
.on_hover_text("Supersample each pixel for smoother edges (~4× slower)."); .on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
if !self.shadow { if !self.shadow {
@@ -1784,7 +1789,9 @@ impl FractalApp {
} }
}); });
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
// Editable center coordinates. Shown at full precision; parsed // Editable center coordinates. Shown at full precision; parsed
// losslessly on commit (Enter or focus loss). While a field is focused // losslessly on commit (Enter or focus loss). While a field is focused
// we leave the user's text alone; otherwise we refresh it from the live // we leave the user's text alone; otherwise we refresh it from the live
@@ -1860,7 +1867,9 @@ impl FractalApp {
ui.colored_label(egui::Color32::LIGHT_YELLOW, "computing reference…"); ui.colored_label(egui::Color32::LIGHT_YELLOW, "computing reference…");
} }
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
let exporting = self.export.is_some(); let exporting = self.export.is_some();
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Copy link").clicked() { if ui.button("Copy link").clicked() {
@@ -1903,7 +1912,9 @@ impl FractalApp {
ui.small(status); ui.small(status);
} }
ui.add_space(4.);
ui.separator(); ui.separator();
ui.add_space(4.);
if ui.button("Reset view").clicked() { if ui.button("Reset view").clicked() {
self.view = Self::default_view_for(self.mode, self.kind); self.view = Self::default_view_for(self.mode, self.kind);
} }
@@ -2091,7 +2102,7 @@ impl FractalApp {
} }
} }
if self.buddhabrot { if self.mode == FractalMode::Buddhabrot {
// No reference orbit / perturbation machinery: iterate directly in // No reference orbit / perturbation machinery: iterate directly in
// f32 from the live view. Progressive accumulation means this // f32 from the live view. Progressive accumulation means this
// needs its own continuous repaint, separate from the escape-time // needs its own continuous repaint, separate from the escape-time