162 lines
6.2 KiB
Rust
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);
|
|
}
|
|
}
|