feat: lossless share links + auto iteration count
This commit is contained in:
+34
@@ -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
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user