Compare commits

..
2 Commits
Author SHA1 Message Date
survandClaude Sonnet 5 783e3e1f30 feat: add keyboard shortcuts for pan/zoom/iterations/AA
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 20:50:46 +02:00
survandClaude Sonnet 5 0f01a16d60 feat: add fractal info and help overlays
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 20:50:40 +02:00
2 changed files with 75 additions and 52 deletions
+70 -27
View File
@@ -72,15 +72,26 @@ fn kind_label(kind: FractalKind) -> &'static str {
/// Multibrot. /// Multibrot.
fn kind_formula(kind: FractalKind, power: u32) -> String { fn kind_formula(kind: FractalKind, power: u32) -> String {
match kind { match kind {
FractalKind::Mandelbrot => "z = z² + c".to_string(), FractalKind::Mandelbrot => "z → z² + c".to_string(),
FractalKind::BurningShip => "z = (|Re(z)| + i|Im(z)|)² + c".to_string(), FractalKind::BurningShip => "z → (|Re z| + i|Im z|)² + c".to_string(),
FractalKind::Tricorn => "z = conj(z)² + c".to_string(), FractalKind::Tricorn => "z → conj(z)² + c".to_string(),
FractalKind::Multibrot => format!("z = z^{power} + c"), FractalKind::Multibrot => format!("z → z^{power} + c"),
FractalKind::Celtic => "z = |Re(z²)| + i·Im(z²) + c".to_string(), FractalKind::Celtic => "z → |Re(z²)| + i·Im(z²) + c".to_string(),
FractalKind::Perpendicular => "z = (x² − y²) − 2x|y|i + c".to_string(), FractalKind::Perpendicular => "z → (x² − y²) − 2x|y|i + c".to_string(),
FractalKind::Buffalo => "z = |Re(z²)| − i|Im(z²)| + c".to_string(), FractalKind::Buffalo => "z → |Re(z²)| − i|Im(z²)| + c".to_string(),
FractalKind::Phoenix => "z = z² + c + p·z_prev".to_string(), FractalKind::Phoenix => "z → z² + c + p·z_prev".to_string(),
FractalKind::Lambda => "z = λ·z(1 − z)".to_string(), FractalKind::Lambda => "z → λ·z(1 − z)".to_string(),
}
}
/// Shorten a long decimal string for compact display, keeping the leading
/// (most significant) digits and marking the cut with "…".
fn truncate_digits(s: &str, max_len: usize) -> String {
if s.chars().count() <= max_len {
s.to_string()
} else {
let head: String = s.chars().take(max_len).collect();
format!("{head}…")
} }
} }
@@ -1188,7 +1199,7 @@ impl FractalApp {
.shadow(egui::Shadow::NONE) .shadow(egui::Shadow::NONE)
.show(ui, |ui| { .show(ui, |ui| {
if ui if ui
.button("Fractal infos") .button("ⓘ Info")
.on_hover_text("Show details about the current fractal") .on_hover_text("Show details about the current fractal")
.clicked() .clicked()
{ {
@@ -1215,17 +1226,13 @@ 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)"
} else {
match self.mode {
FractalMode::Mandelbrot => { FractalMode::Mandelbrot => {
"Mandelbrot mode — parameter space (c varies per pixel, z₀ = 0)" "Mandelbrot mode — parameter space (c varies per pixel, z₀ = 0)"
} }
FractalMode::Julia => { FractalMode::Julia => {
"Julia mode — dynamical plane for a fixed c (z₀ varies per pixel)" "Julia mode — dynamical plane for a fixed c (z₀ varies per pixel)"
} }
}
}; };
ui.label(mode_label); ui.label(mode_label);
ui.separator(); ui.separator();
@@ -1248,7 +1255,32 @@ impl FractalApp {
} }
ui.separator(); ui.separator();
ui.label(self.kind.description()); if self.buddhabrot {
ui.label("rendering: Buddhabrot (Monte-Carlo orbit density)");
ui.label(format!(
"sample caps: R {} / G {} / B {}",
self.buddha_r_cap, self.buddha_g_cap, self.buddha_b_cap
));
} else {
ui.label(format!(
"magnification: {}×",
format_magnification(self.view.magnification())
));
ui.label(format!(
"iterations: {}{}",
self.max_iterations,
if self.auto_iterations { " (auto)" } else { "" }
));
ui.label(format!("precision: {} bits", self.view.precision_bits()));
ui.label(format!(
"center re: {}",
truncate_digits(&self.center_re_edit, 24)
));
ui.label(format!(
"center im: {}",
truncate_digits(&self.center_im_edit, 24)
));
}
}); });
self.info_open = open; self.info_open = open;
} }
@@ -1270,16 +1302,19 @@ impl FractalApp {
ui.label( ui.label(
"A deep-zoom fractal explorer. It renders the Mandelbrot set \ "A deep-zoom fractal explorer. It renders the Mandelbrot set \
and several related fractals (Burning Ship, Tricorn, \ and several related fractals (Burning Ship, Tricorn, \
Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda).", Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda), \
and can zoom in far beyond what ordinary floating-point math \
allows by using perturbation theory: one very precise \
reference orbit is computed once, and every pixel on screen \
is then rendered cheaply as a small offset from that shared \
orbit.",
); );
ui.add_space(4.0); ui.add_space(4.0);
ui.label( ui.label(
"Each fractals can be rendered in different modes: \n\ "Mandelbrot mode plots one point per pixel in parameter space \
• Mandelbrot mode fixes z₀=0 and then for each pixel, set c as it's position \ (does c escape?). Julia mode fixes c and instead varies the \
in the complex plane. \n\ starting point z₀ across the plane. Buddhabrot mode switches \
• Julia mode fixes c and instead varies the \ to a different, Monte-Carlo rendering of orbit density \
starting point z₀ across the plane. \n\
• Buddhabrot mode switches to a different, Monte-Carlo rendering of orbit density \
instead of the ordinary escape-time set.", instead of the ordinary escape-time set.",
); );
ui.separator(); ui.separator();
@@ -1331,11 +1366,20 @@ impl FractalApp {
ui.label("Toggle antialiasing (2×2)"); ui.label("Toggle antialiasing (2×2)");
ui.end_row(); ui.end_row();
}); });
ui.label(
"Shortcuts are ignored while a text field (e.g. the center/zoom \
edit boxes) has focus. Esc / F11 toggle fullscreen via the \
browser or OS, as usual.",
);
ui.separator(); ui.separator();
ui.heading("Tips"); ui.heading("Tips");
ui.label( ui.label(
"• \"Copy link\" (in the panel) encodes the exact view so it \ "• The controls panel (top-left \"Menu\" button) holds the \
fractal formula, coloring, animation, and export options.\n\
• The bottom-left \"Info\" button shows details about what's \
currently on screen.\n\
• \"Copy link\" (in the panel) encodes the exact view so it \
can be reopened later or sent to someone else.", can be reopened later or sent to someone else.",
); );
}); });
@@ -1826,11 +1870,10 @@ impl FractalApp {
ui.add( ui.add(
egui::DragValue::new(&mut self.export_scale) egui::DragValue::new(&mut self.export_scale)
.range(1.0..=16.0) .range(1.0..=16.0)
.speed(0.25) .speed(0.5),
.custom_formatter(|x, _| format!("x{:.1}", x)),
); );
ui.label(format!( ui.label(format!(
"= {}×{}", "→ {}×{}",
(self.last_size_px.x * self.export_scale) as u32, (self.last_size_px.x * self.export_scale) as u32,
(self.last_size_px.y * self.export_scale) as u32, (self.last_size_px.y * self.export_scale) as u32,
)); ));
-20
View File
@@ -37,26 +37,6 @@ pub enum FractalKind {
Lambda = 8, Lambda = 8,
} }
impl FractalKind {
pub fn description(&self) -> &str {
match self {
FractalKind::Mandelbrot => {
"The Mandelbrot set is the most famous fractal set, obtained with the simplest escape-time formula. This set represents all Julia fractals: each points of the Mandelbrot set is related to a specific Julia fractal."
}
FractalKind::BurningShip => {
"A variation of the famous Mandelbrot set, using absolute values on the real and imaginary part of each iterations."
}
FractalKind::Tricorn => "The Tricorn set is obtained using the same formula as the Mandelbrot set, taking the complex conjugate of the previous iteration.",
FractalKind::Multibrot => "Multibrot use the same formula as the Mandelbrot set, with a bigger exposant.",
FractalKind::Celtic => "",
FractalKind::Perpendicular => "",
FractalKind::Buffalo => "",
FractalKind::Phoenix => "",
FractalKind::Lambda => "",
}
}
}
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel /// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
/// bailout so pixels escaping alongside the reference can still reach their /// bailout so pixels escaping alongside the reference can still reach their
/// bailout before the stored orbit runs out. /// bailout before the stored orbit runs out.