feat: lossless share links + auto iteration count
This commit is contained in:
+34
@@ -124,6 +124,10 @@ pub struct FractalApp {
|
||||
power: u32,
|
||||
julia_c: (f64, f64),
|
||||
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_offset: f32,
|
||||
palette: u32,
|
||||
@@ -222,6 +226,7 @@ impl FractalApp {
|
||||
power: 3,
|
||||
julia_c: (-0.8, 0.156),
|
||||
max_iterations: 512,
|
||||
auto_iterations: true,
|
||||
color_scale: 0.15,
|
||||
color_offset: 0.0,
|
||||
palette: 0,
|
||||
@@ -338,16 +343,28 @@ impl FractalApp {
|
||||
{
|
||||
self.mode = FractalMode::Mandelbrot;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn share_state(&self) -> ShareState {
|
||||
let sig_digits = sig_digits_for(self.view.precision_bits());
|
||||
ShareState {
|
||||
julia: matches!(self.mode, FractalMode::Julia),
|
||||
kind: self.kind,
|
||||
power: self.power,
|
||||
center_re: big_to_decimal_str(&self.view.center_re, sig_digits),
|
||||
center_im: big_to_decimal_str(&self.view.center_im, sig_digits),
|
||||
half_height: self.view.half_height,
|
||||
@@ -355,6 +372,7 @@ impl FractalApp {
|
||||
julia_c: self.julia_c,
|
||||
color_scale: self.color_scale,
|
||||
color_offset: self.color_offset,
|
||||
palette: self.palette,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,9 +384,14 @@ impl FractalApp {
|
||||
FractalMode::Mandelbrot
|
||||
};
|
||||
self.kind = s.kind;
|
||||
self.power = s.power.clamp(2, 8);
|
||||
self.julia_c = s.julia_c;
|
||||
self.color_scale = s.color_scale;
|
||||
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);
|
||||
let bits = precision_for(s.half_height);
|
||||
if let (Some(re), Some(im)) = (
|
||||
@@ -808,11 +831,17 @@ impl FractalApp {
|
||||
}
|
||||
|
||||
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(
|
||||
egui::Slider::new(&mut self.max_iterations, 32..=100_000)
|
||||
.text("iterations")
|
||||
.logarithmic(true),
|
||||
);
|
||||
}
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.color_scale, 0.01..=1.0)
|
||||
.text("color scale")
|
||||
@@ -1006,6 +1035,11 @@ impl FractalApp {
|
||||
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();
|
||||
|
||||
// Poll the worker roughly every 30 ms while a reference is computing,
|
||||
|
||||
+14
-3
@@ -14,6 +14,8 @@ use crate::fractal::FractalKind;
|
||||
pub struct ShareState {
|
||||
pub julia: bool,
|
||||
pub kind: FractalKind,
|
||||
/// Exponent for the Multibrot kind (ignored by others).
|
||||
pub power: u32,
|
||||
pub center_re: String,
|
||||
pub center_im: String,
|
||||
pub half_height: f64,
|
||||
@@ -21,6 +23,8 @@ pub struct ShareState {
|
||||
pub julia_c: (f64, f64),
|
||||
pub color_scale: f32,
|
||||
pub color_offset: f32,
|
||||
/// Palette index (`palette_id` in the shader).
|
||||
pub palette: u32,
|
||||
}
|
||||
|
||||
impl ShareState {
|
||||
@@ -33,14 +37,15 @@ impl ShareState {
|
||||
FractalKind::Multibrot => "multi",
|
||||
FractalKind::Tricorn => "tricorn",
|
||||
}));
|
||||
s.push_str(&format!("&pw={}", self.power));
|
||||
s.push_str(&format!(
|
||||
"&re={}&im={}&hh={}&it={}",
|
||||
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!(
|
||||
"&cs={}&co={}",
|
||||
self.color_scale, self.color_offset
|
||||
"&cs={}&co={}&pal={}",
|
||||
self.color_scale, self.color_offset, self.palette
|
||||
));
|
||||
s
|
||||
}
|
||||
@@ -66,6 +71,7 @@ impl ShareState {
|
||||
_ => 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_im: (*map.get("im")?).to_string(),
|
||||
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_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() {
|
||||
let s = ShareState {
|
||||
julia: true,
|
||||
kind: FractalKind::Tricorn,
|
||||
kind: FractalKind::Multibrot,
|
||||
power: 5,
|
||||
center_re: "-0.743643887037158704752191506114774".into(),
|
||||
center_im: "0.131825904205311970493132056385139".into(),
|
||||
half_height: 1.5e-20,
|
||||
@@ -96,15 +104,18 @@ mod tests {
|
||||
julia_c: (-0.123, 0.745),
|
||||
color_scale: 0.02,
|
||||
color_offset: 0.25,
|
||||
palette: 3,
|
||||
};
|
||||
let d = ShareState::decode(&s.encode()).unwrap();
|
||||
assert_eq!(d.julia, s.julia);
|
||||
assert_eq!(d.kind, s.kind);
|
||||
assert_eq!(d.power, s.power);
|
||||
assert_eq!(d.center_re, s.center_re);
|
||||
assert_eq!(d.center_im, s.center_im);
|
||||
assert_eq!(d.half_height, s.half_height);
|
||||
assert_eq!(d.iterations, s.iterations);
|
||||
assert_eq!(d.julia_c, s.julia_c);
|
||||
assert_eq!(d.palette, s.palette);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user