feat: lossless share links + auto iteration count

This commit is contained in:
2026-09-15 15:50:57 +02:00
parent c92c685ade
commit acbf25d64e
2 changed files with 53 additions and 8 deletions
+34
View File
@@ -124,6 +124,10 @@ pub struct FractalApp {
power: u32, power: u32,
julia_c: (f64, f64), julia_c: (f64, f64),
max_iterations: u32, max_iterations: u32,
/// When set, `max_iterations` tracks the zoom depth automatically (so deep
/// zooms stay sharp without hand-tuning); the manual slider takes over when
/// unset. Turned off when a preset or share link supplies an explicit count.
auto_iterations: bool,
color_scale: f32, color_scale: f32,
color_offset: f32, color_offset: f32,
palette: u32, palette: u32,
@@ -222,6 +226,7 @@ impl FractalApp {
power: 3, power: 3,
julia_c: (-0.8, 0.156), julia_c: (-0.8, 0.156),
max_iterations: 512, max_iterations: 512,
auto_iterations: true,
color_scale: 0.15, color_scale: 0.15,
color_offset: 0.0, color_offset: 0.0,
palette: 0, palette: 0,
@@ -338,16 +343,28 @@ impl FractalApp {
{ {
self.mode = FractalMode::Mandelbrot; self.mode = FractalMode::Mandelbrot;
self.view = ViewState::with_center(cre, cim, half_height); self.view = ViewState::with_center(cre, cim, half_height);
// Presets carry a hand-tuned count; don't let the auto-scaler clobber it.
self.auto_iterations = false;
self.max_iterations = iterations.clamp(32, MAX_REF_POINTS as u32 - 1); self.max_iterations = iterations.clamp(32, MAX_REF_POINTS as u32 - 1);
} }
} }
/// Iteration count scaled to the current zoom depth, used while
/// `auto_iterations` is on. Grows roughly linearly with zoom decades so deep
/// zooms keep enough iterations to stay sharp instead of banding.
fn auto_iteration_count(&self) -> u32 {
let decades = self.view.magnification().log10().max(0.0);
let iters = 400.0 + 900.0 * decades;
(iters.round() as u32).clamp(200, MAX_REF_POINTS as u32 - 1)
}
/// Snapshot the current view as a shareable state. /// Snapshot the current view as a shareable state.
fn share_state(&self) -> ShareState { fn share_state(&self) -> ShareState {
let sig_digits = sig_digits_for(self.view.precision_bits()); let sig_digits = sig_digits_for(self.view.precision_bits());
ShareState { ShareState {
julia: matches!(self.mode, FractalMode::Julia), julia: matches!(self.mode, FractalMode::Julia),
kind: self.kind, kind: self.kind,
power: self.power,
center_re: big_to_decimal_str(&self.view.center_re, sig_digits), center_re: big_to_decimal_str(&self.view.center_re, sig_digits),
center_im: big_to_decimal_str(&self.view.center_im, sig_digits), center_im: big_to_decimal_str(&self.view.center_im, sig_digits),
half_height: self.view.half_height, half_height: self.view.half_height,
@@ -355,6 +372,7 @@ impl FractalApp {
julia_c: self.julia_c, julia_c: self.julia_c,
color_scale: self.color_scale, color_scale: self.color_scale,
color_offset: self.color_offset, color_offset: self.color_offset,
palette: self.palette,
} }
} }
@@ -366,9 +384,14 @@ impl FractalApp {
FractalMode::Mandelbrot FractalMode::Mandelbrot
}; };
self.kind = s.kind; self.kind = s.kind;
self.power = s.power.clamp(2, 8);
self.julia_c = s.julia_c; self.julia_c = s.julia_c;
self.color_scale = s.color_scale; self.color_scale = s.color_scale;
self.color_offset = s.color_offset; self.color_offset = s.color_offset;
self.palette = (s.palette as usize).min(PALETTE_NAMES.len() - 1) as u32;
// The link carries an explicit iteration count; honor it rather than
// letting the auto-scaler immediately overwrite it.
self.auto_iterations = false;
self.max_iterations = s.iterations.clamp(32, MAX_REF_POINTS as u32 - 1); self.max_iterations = s.iterations.clamp(32, MAX_REF_POINTS as u32 - 1);
let bits = precision_for(s.half_height); let bits = precision_for(s.half_height);
if let (Some(re), Some(im)) = ( if let (Some(re), Some(im)) = (
@@ -808,11 +831,17 @@ impl FractalApp {
} }
ui.separator(); ui.separator();
ui.checkbox(&mut self.auto_iterations, "Auto iterations")
.on_hover_text("Scale the iteration count with zoom depth so deep zooms stay sharp.");
if self.auto_iterations {
ui.label(format!("iterations: {} (auto)", self.max_iterations));
} else {
ui.add( ui.add(
egui::Slider::new(&mut self.max_iterations, 32..=100_000) egui::Slider::new(&mut self.max_iterations, 32..=100_000)
.text("iterations") .text("iterations")
.logarithmic(true), .logarithmic(true),
); );
}
ui.add( ui.add(
egui::Slider::new(&mut self.color_scale, 0.01..=1.0) egui::Slider::new(&mut self.color_scale, 0.01..=1.0)
.text("color scale") .text("color scale")
@@ -1006,6 +1035,11 @@ impl FractalApp {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
// Keep the iteration count matched to the zoom depth while auto is on.
if self.auto_iterations {
self.max_iterations = self.auto_iteration_count();
}
self.ensure_reference(); self.ensure_reference();
// Poll the worker roughly every 30 ms while a reference is computing, // Poll the worker roughly every 30 ms while a reference is computing,
+14 -3
View File
@@ -14,6 +14,8 @@ use crate::fractal::FractalKind;
pub struct ShareState { pub struct ShareState {
pub julia: bool, pub julia: bool,
pub kind: FractalKind, pub kind: FractalKind,
/// Exponent for the Multibrot kind (ignored by others).
pub power: u32,
pub center_re: String, pub center_re: String,
pub center_im: String, pub center_im: String,
pub half_height: f64, pub half_height: f64,
@@ -21,6 +23,8 @@ pub struct ShareState {
pub julia_c: (f64, f64), pub julia_c: (f64, f64),
pub color_scale: f32, pub color_scale: f32,
pub color_offset: f32, pub color_offset: f32,
/// Palette index (`palette_id` in the shader).
pub palette: u32,
} }
impl ShareState { impl ShareState {
@@ -33,14 +37,15 @@ impl ShareState {
FractalKind::Multibrot => "multi", FractalKind::Multibrot => "multi",
FractalKind::Tricorn => "tricorn", FractalKind::Tricorn => "tricorn",
})); }));
s.push_str(&format!("&pw={}", self.power));
s.push_str(&format!( s.push_str(&format!(
"&re={}&im={}&hh={}&it={}", "&re={}&im={}&hh={}&it={}",
self.center_re, self.center_im, self.half_height, self.iterations self.center_re, self.center_im, self.half_height, self.iterations
)); ));
s.push_str(&format!("&jr={}&ji={}", self.julia_c.0, self.julia_c.1)); s.push_str(&format!("&jr={}&ji={}", self.julia_c.0, self.julia_c.1));
s.push_str(&format!( s.push_str(&format!(
"&cs={}&co={}", "&cs={}&co={}&pal={}",
self.color_scale, self.color_offset self.color_scale, self.color_offset, self.palette
)); ));
s s
} }
@@ -66,6 +71,7 @@ impl ShareState {
_ => FractalKind::Mandelbrot _ => FractalKind::Mandelbrot
}) })
.unwrap_or(FractalKind::Mandelbrot), .unwrap_or(FractalKind::Mandelbrot),
power: map.get("pw").and_then(|s| s.parse().ok()).unwrap_or(2),
center_re: (*map.get("re")?).to_string(), center_re: (*map.get("re")?).to_string(),
center_im: (*map.get("im")?).to_string(), center_im: (*map.get("im")?).to_string(),
half_height: map.get("hh")?.parse().ok()?, half_height: map.get("hh")?.parse().ok()?,
@@ -76,6 +82,7 @@ impl ShareState {
), ),
color_scale: map.get("cs").and_then(|s| s.parse().ok()).unwrap_or(0.02), color_scale: map.get("cs").and_then(|s| s.parse().ok()).unwrap_or(0.02),
color_offset: map.get("co").and_then(|s| s.parse().ok()).unwrap_or(0.0), color_offset: map.get("co").and_then(|s| s.parse().ok()).unwrap_or(0.0),
palette: map.get("pal").and_then(|s| s.parse().ok()).unwrap_or(0),
}) })
} }
} }
@@ -88,7 +95,8 @@ mod tests {
fn round_trip() { fn round_trip() {
let s = ShareState { let s = ShareState {
julia: true, julia: true,
kind: FractalKind::Tricorn, kind: FractalKind::Multibrot,
power: 5,
center_re: "-0.743643887037158704752191506114774".into(), center_re: "-0.743643887037158704752191506114774".into(),
center_im: "0.131825904205311970493132056385139".into(), center_im: "0.131825904205311970493132056385139".into(),
half_height: 1.5e-20, half_height: 1.5e-20,
@@ -96,15 +104,18 @@ mod tests {
julia_c: (-0.123, 0.745), julia_c: (-0.123, 0.745),
color_scale: 0.02, color_scale: 0.02,
color_offset: 0.25, color_offset: 0.25,
palette: 3,
}; };
let d = ShareState::decode(&s.encode()).unwrap(); let d = ShareState::decode(&s.encode()).unwrap();
assert_eq!(d.julia, s.julia); assert_eq!(d.julia, s.julia);
assert_eq!(d.kind, s.kind); assert_eq!(d.kind, s.kind);
assert_eq!(d.power, s.power);
assert_eq!(d.center_re, s.center_re); assert_eq!(d.center_re, s.center_re);
assert_eq!(d.center_im, s.center_im); assert_eq!(d.center_im, s.center_im);
assert_eq!(d.half_height, s.half_height); assert_eq!(d.half_height, s.half_height);
assert_eq!(d.iterations, s.iterations); assert_eq!(d.iterations, s.iterations);
assert_eq!(d.julia_c, s.julia_c); assert_eq!(d.julia_c, s.julia_c);
assert_eq!(d.palette, s.palette);
} }
#[test] #[test]