Files
mandelbrot/src/fractal/share.rs
T
2026-09-27 12:17:08 +02:00

162 lines
6.2 KiB
Rust

//! Encode/decode a full view (fractal mode, high-precision center, zoom,
//! iterations, Julia constant, coloring) as a compact URL fragment so deep-zoom
//! locations can be shared or bookmarked.
//!
//! Format: `m=m&f=<str>&re=<dec>&im=<dec>&hh=<sci>&it=<u32>&cs=<f32>&co=<f32>` with
//! `m=j&jr=<f64>&ji=<f64>` added for Julia. `re`/`im` are full-precision decimal
//! strings; `hh` is a `Scale` in scientific notation (any exponent).
use std::collections::HashMap;
use crate::fractal::FractalKind;
use crate::view::Scale;
#[derive(Clone, Debug)]
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: Scale,
pub iterations: u32,
pub julia_c: (f64, f64),
/// Distortion constant for the Phoenix kind (ignored by others).
pub phoenix_p: (f64, f64),
/// Distortion constant for the Lambda kind (ignored by others).
pub lambda_l: (f64, f64),
/// Complex exponent for the Complex Multibrot kind (ignored by others).
pub complex_power: (f64, f64),
pub color_scale: f32,
pub color_offset: f32,
/// Palette index (`palette_id` in the shader).
pub palette: u32,
/// Shadow palette index (`shadow_palette_id` in the shader).
pub shadow_palette: u32,
}
impl ShareState {
pub fn encode(&self) -> String {
let mut s = String::new();
s.push_str(if self.julia { "m=j" } else { "m=m" });
s.push_str(&format!("&f={}", self.kind.share_tag()));
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!("&px={}&py={}", self.phoenix_p.0, self.phoenix_p.1));
s.push_str(&format!("&lx={}&ly={}", self.lambda_l.0, self.lambda_l.1));
s.push_str(&format!(
"&cpr={}&cpi={}",
self.complex_power.0, self.complex_power.1
));
s.push_str(&format!(
"&cs={}&co={}&pal={}&spal={}",
self.color_scale, self.color_offset, self.palette, self.shadow_palette
));
s
}
pub fn decode(fragment: &str) -> Option<ShareState> {
let fragment = fragment.trim_start_matches(['#', '?']);
let mut map: HashMap<&str, &str> = HashMap::new();
for kv in fragment.split('&') {
if let Some((k, v)) = kv.split_once('=') {
map.insert(k, v);
}
}
Some(ShareState {
julia: map.get("m").map(|m| *m == "j").unwrap_or(false),
kind: map
.get("f")
.and_then(|f| FractalKind::from_share_tag(f))
.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()?,
iterations: map.get("it").and_then(|s| s.parse().ok()).unwrap_or(512),
julia_c: (
map.get("jr").and_then(|s| s.parse().ok()).unwrap_or(-0.8),
map.get("ji").and_then(|s| s.parse().ok()).unwrap_or(0.156),
),
phoenix_p: (
map.get("px").and_then(|s| s.parse().ok()).unwrap_or(-0.5),
map.get("py").and_then(|s| s.parse().ok()).unwrap_or(0.0),
),
lambda_l: (
map.get("lx").and_then(|s| s.parse().ok()).unwrap_or(-0.5),
map.get("ly").and_then(|s| s.parse().ok()).unwrap_or(0.0),
),
complex_power: (
map.get("cpr").and_then(|s| s.parse().ok()).unwrap_or(2.0),
map.get("cpi").and_then(|s| s.parse().ok()).unwrap_or(0.0),
),
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),
shadow_palette: map.get("spal").and_then(|s| s.parse().ok()).unwrap_or(0),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let s = ShareState {
julia: true,
kind: FractalKind::Phoenix,
power: 5,
center_re: "-0.743643887037158704752191506114774".into(),
center_im: "0.131825904205311970493132056385139".into(),
half_height: Scale::from_f64(1.5e-20),
iterations: 4000,
julia_c: (-0.123, 0.745),
phoenix_p: (-0.5, 0.1),
lambda_l: (-0.5, 0.0),
complex_power: (2.5, 0.3),
color_scale: 0.02,
color_offset: 0.25,
palette: 3,
shadow_palette: 1,
};
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.phoenix_p, s.phoenix_p);
assert_eq!(d.complex_power, s.complex_power);
assert_eq!(d.palette, s.palette);
assert_eq!(d.shadow_palette, s.shadow_palette);
}
#[test]
fn decode_with_leading_hash() {
let d = ShareState::decode("#m=m&re=0.0&im=0.0&hh=1.25&it=256").unwrap();
assert!(!d.julia);
assert_eq!(d.iterations, 256);
assert_eq!(d.half_height, Scale::from_f64(1.25));
}
/// Zooms past f64's range survive a round trip.
#[test]
fn round_trip_past_f64_range() {
let d = ShareState::decode("#m=m&re=0.0&im=0.0&hh=1.5e-1234&it=256").unwrap();
assert_eq!(d.half_height, "1.5e-1234".parse().unwrap());
let d2 = ShareState::decode(&d.encode()).unwrap();
assert_eq!(d2.half_height, d.half_height);
}
}