First commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
//! GPU fractal rendering: wgpu pipeline, uniforms, reference orbit, and the
|
||||
//! egui paint callback.
|
||||
|
||||
pub mod reference;
|
||||
pub mod renderer;
|
||||
pub mod share;
|
||||
|
||||
pub use reference::{compute_mandelbrot_reference, compute_reference};
|
||||
pub use renderer::{FractalCallback, FractalRenderer, Uniforms, MAX_REF_POINTS, encode_png};
|
||||
pub use share::ShareState;
|
||||
@@ -0,0 +1,136 @@
|
||||
//! High-precision reference-orbit computation for perturbation rendering.
|
||||
//!
|
||||
//! We iterate `Z_{n+1} = Z_n^2 + C` at high precision (`dashu-float`), storing
|
||||
//! each `Z_n` as an `f32` pair. Every pixel is then rendered on the GPU as a
|
||||
//! small `f32` delta from this orbit — that is what makes deep zoom cheap. See
|
||||
//! `shaders/mandelbrot.wgsl` for the delta side.
|
||||
//!
|
||||
//! The `(z0, c)` form serves both fractals:
|
||||
//! * Mandelbrot: `z0 = 0`, `c = view center` (the c-plane point per pixel).
|
||||
//! * Julia: `z0 = view center`, `c = julia constant` (fixed for all pixels).
|
||||
|
||||
use crate::view::Big;
|
||||
|
||||
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
||||
/// bailout so pixels escaping alongside the reference can still reach their
|
||||
/// bailout before the stored orbit runs out.
|
||||
const REFERENCE_ESCAPE_SQ: f64 = 1.0e10;
|
||||
|
||||
/// Compute the reference orbit `Z_0..Z_{len-1}` where `Z_0 = z0` and
|
||||
/// `Z_{n+1} = Z_n^2 + c`, up to `max_iter` steps at `precision` bits. Each entry
|
||||
/// is `[re, im]` in f32.
|
||||
pub fn compute_reference(
|
||||
z0_re: &Big,
|
||||
z0_im: &Big,
|
||||
c_re: &Big,
|
||||
c_im: &Big,
|
||||
max_iter: u32,
|
||||
precision: usize,
|
||||
) -> Vec<[f32; 2]> {
|
||||
let cr = c_re.clone().with_precision(precision).value();
|
||||
let ci = c_im.clone().with_precision(precision).value();
|
||||
|
||||
let mut zr = z0_re.clone().with_precision(precision).value();
|
||||
let mut zi = z0_im.clone().with_precision(precision).value();
|
||||
|
||||
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
||||
|
||||
for _ in 0..=max_iter {
|
||||
let fr = zr.to_f64().value() as f32;
|
||||
let fi = zi.to_f64().value() as f32;
|
||||
points.push([fr, fi]);
|
||||
|
||||
let mag = (fr as f64) * (fr as f64) + (fi as f64) * (fi as f64);
|
||||
if mag > REFERENCE_ESCAPE_SQ {
|
||||
break;
|
||||
}
|
||||
|
||||
// Z = Z^2 + C, with Z^2 = (zr^2 - zi^2) + (2 zr zi) i.
|
||||
let zr2 = zr.sqr();
|
||||
let zi2 = zi.sqr();
|
||||
let new_zr = ((&zr2 - &zi2) + &cr).with_precision(precision).value();
|
||||
let two_zr_zi = (&zr * &zi) << 1; // exact multiply-by-2 in base 2
|
||||
let new_zi = (two_zr_zi + &ci).with_precision(precision).value();
|
||||
|
||||
zr = new_zr;
|
||||
zi = new_zi;
|
||||
}
|
||||
|
||||
points
|
||||
}
|
||||
|
||||
fn big_zero(precision: usize) -> Big {
|
||||
Big::from(0i32).with_precision(precision).value()
|
||||
}
|
||||
|
||||
/// Convenience: Mandelbrot reference (`z0 = 0`, `c = center`).
|
||||
pub fn compute_mandelbrot_reference(
|
||||
center_re: &Big,
|
||||
center_im: &Big,
|
||||
max_iter: u32,
|
||||
precision: usize,
|
||||
) -> Vec<[f32; 2]> {
|
||||
let zero = big_zero(precision);
|
||||
compute_reference(&zero, &zero, center_re, center_im, max_iter, precision)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The high-precision reference must agree with a plain f64 iteration for a
|
||||
/// shallow point (where f64 is accurate).
|
||||
#[test]
|
||||
fn reference_matches_naive_f64() {
|
||||
let cr = Big::try_from(-0.75_f64).unwrap();
|
||||
let ci = Big::try_from(0.1_f64).unwrap();
|
||||
let points = compute_mandelbrot_reference(&cr, &ci, 60, 200);
|
||||
|
||||
// Independent naive f64 orbit.
|
||||
let (c_re, c_im) = (-0.75_f64, 0.1_f64);
|
||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||
for point in &points {
|
||||
// Tolerance is relative to magnitude: f32 storage only keeps ~7
|
||||
// significant figures.
|
||||
let tol_re = 1e-4 * (1.0 + zr.abs());
|
||||
let tol_im = 1e-4 * (1.0 + zi.abs());
|
||||
assert!((point[0] as f64 - zr).abs() < tol_re, "re mismatch: {point:?} vs {zr}");
|
||||
assert!((point[1] as f64 - zi).abs() < tol_im, "im mismatch: {point:?} vs {zi}");
|
||||
let nzr = zr * zr - zi * zi + c_re;
|
||||
let nzi = 2.0 * zr * zi + c_im;
|
||||
zr = nzr;
|
||||
zi = nzi;
|
||||
}
|
||||
}
|
||||
|
||||
/// A point inside the main cardioid never escapes: full-length orbit.
|
||||
#[test]
|
||||
fn interior_orbit_runs_full_length() {
|
||||
let cr = Big::try_from(-0.2_f64).unwrap();
|
||||
let ci = Big::try_from(0.0_f64).unwrap();
|
||||
let points = compute_mandelbrot_reference(&cr, &ci, 500, 120);
|
||||
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
||||
}
|
||||
|
||||
/// Julia orbit (fixed c, z0 = center) matches a naive f64 iteration.
|
||||
#[test]
|
||||
fn julia_reference_matches_naive_f64() {
|
||||
let z0_re = Big::try_from(0.15_f64).unwrap();
|
||||
let z0_im = Big::try_from(-0.1_f64).unwrap();
|
||||
let c_re = Big::try_from(-0.8_f64).unwrap();
|
||||
let c_im = Big::try_from(0.156_f64).unwrap();
|
||||
let points = compute_reference(&z0_re, &z0_im, &c_re, &c_im, 60, 200);
|
||||
|
||||
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
||||
let (cr, ci) = (-0.8_f64, 0.156_f64);
|
||||
for point in &points {
|
||||
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||||
assert!((point[0] as f64 - zr).abs() < tol);
|
||||
assert!((point[1] as f64 - zi).abs() < tol);
|
||||
let nzr = zr * zr - zi * zi + cr;
|
||||
let nzi = 2.0 * zr * zi + ci;
|
||||
zr = nzr;
|
||||
zi = nzi;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
//! wgpu resources for the fractal: the render pipeline, the uniform buffer, the
|
||||
//! reference-orbit storage buffer, and the egui paint callback that drives them.
|
||||
//!
|
||||
//! Rendering strategy: a single fullscreen triangle is drawn into the rectangle
|
||||
//! egui allocates for the fractal widget (egui presets the render pass viewport
|
||||
//! for us). The fragment shader iterates each pixel as an f32 perturbation delta
|
||||
//! from the high-precision reference orbit stored in `ref_buffer`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use eframe::egui_wgpu::{self, wgpu};
|
||||
|
||||
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
|
||||
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
|
||||
pub const MAX_REF_POINTS: usize = 1 << 17;
|
||||
|
||||
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
|
||||
/// WGSL shader; total size is a multiple of 16 bytes for uniform-buffer rules.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Uniforms {
|
||||
/// Complex-plane span (width, height) covered by the view. Per-pixel `dc`
|
||||
/// is `centered * span`, where `centered` is in [-0.5, 0.5].
|
||||
pub span: [f32; 2],
|
||||
pub max_iter: u32,
|
||||
pub ref_len: u32,
|
||||
pub color_offset: f32,
|
||||
pub color_scale: f32,
|
||||
pub bailout_sq: f32,
|
||||
/// 0 = Mandelbrot, 1 = Julia.
|
||||
pub is_julia: u32,
|
||||
pub palette_id: u32,
|
||||
pub _pad0: u32,
|
||||
/// Complex offset of the view center from the reference center, so a stale
|
||||
/// or reused reference (computed at a slightly different center) still maps
|
||||
/// correctly. Added to every pixel's per-pixel offset.
|
||||
pub dc_offset: [f32; 2],
|
||||
}
|
||||
|
||||
pub struct FractalRenderer {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
ref_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
target_format: wgpu::TextureFormat,
|
||||
/// Generation of the reference orbit currently uploaded to `ref_buffer`.
|
||||
uploaded_generation: u64,
|
||||
}
|
||||
|
||||
impl FractalRenderer {
|
||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("mandelbrot"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/mandelbrot.wgsl").into()),
|
||||
});
|
||||
|
||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("fractal uniforms"),
|
||||
size: std::mem::size_of::<Uniforms>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let ref_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("reference orbit"),
|
||||
size: (MAX_REF_POINTS * std::mem::size_of::<[f32; 2]>()) as u64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("fractal bind group layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("fractal bind group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: ref_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("fractal pipeline layout"),
|
||||
bind_group_layouts: &[Some(&bind_group_layout)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("fractal pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: target_format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
uniform_buffer,
|
||||
ref_buffer,
|
||||
bind_group,
|
||||
target_format,
|
||||
uploaded_generation: u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload a reference orbit to the storage buffer (used by PNG export to
|
||||
/// guarantee the buffer is current before an offscreen render).
|
||||
pub fn upload_reference(&self, queue: &wgpu::Queue, points: &[[f32; 2]]) {
|
||||
let count = points.len().min(MAX_REF_POINTS);
|
||||
if count > 0 {
|
||||
queue.write_buffer(&self.ref_buffer, 0, bytemuck::cast_slice(&points[..count]));
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the render target stores bytes as BGRA (so a PNG needs R/B
|
||||
/// swapped). Surfaces are usually `Bgra8UnormSrgb`.
|
||||
pub fn needs_rb_swap(&self) -> bool {
|
||||
matches!(
|
||||
self.target_format,
|
||||
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the current fractal (using `uniforms` and the already-uploaded
|
||||
/// reference orbit) into an offscreen texture at `width`x`height`, then copy
|
||||
/// it into a mappable buffer. Returns the buffer and its padded row stride.
|
||||
/// The caller maps the buffer (blocking on native, async on web).
|
||||
pub fn render_to_readback(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
width: u32,
|
||||
height: u32,
|
||||
uniforms: Uniforms,
|
||||
) -> (wgpu::Buffer, u32) {
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("export target"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: self.target_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let unpadded_bpr = width * 4;
|
||||
let padded_bpr = unpadded_bpr.div_ceil(align) * align;
|
||||
|
||||
let readback = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("export readback"),
|
||||
size: (padded_bpr * height) as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("export"),
|
||||
});
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("export pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &readback,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded_bpr),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
(readback, padded_bpr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a padded BGRA/RGBA readback into tightly-packed RGBA8 and encode it
|
||||
/// as PNG bytes.
|
||||
pub fn encode_png(
|
||||
padded: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
padded_bpr: u32,
|
||||
swap_rb: bool,
|
||||
) -> Vec<u8> {
|
||||
let row = (width * 4) as usize;
|
||||
let mut rgba = vec![0u8; row * height as usize];
|
||||
for y in 0..height as usize {
|
||||
let src_off = y * padded_bpr as usize;
|
||||
let src = &padded[src_off..src_off + row];
|
||||
let dst = &mut rgba[y * row..y * row + row];
|
||||
if swap_rb {
|
||||
for x in 0..width as usize {
|
||||
dst[x * 4] = src[x * 4 + 2];
|
||||
dst[x * 4 + 1] = src[x * 4 + 1];
|
||||
dst[x * 4 + 2] = src[x * 4];
|
||||
dst[x * 4 + 3] = src[x * 4 + 3];
|
||||
}
|
||||
} else {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
{
|
||||
let mut encoder = png::Encoder::new(&mut out, width, height);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().expect("png header");
|
||||
writer.write_image_data(&rgba).expect("png data");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A per-frame paint callback. Carries this frame's uniforms plus a reference to
|
||||
/// the current reference orbit (cheap `Arc` clone). The orbit is only re-uploaded
|
||||
/// to the GPU when its `generation` changes.
|
||||
pub struct FractalCallback {
|
||||
pub uniforms: Uniforms,
|
||||
pub reference: Arc<Vec<[f32; 2]>>,
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
impl egui_wgpu::CallbackTrait for FractalCallback {
|
||||
fn prepare(
|
||||
&self,
|
||||
_device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
|
||||
_egui_encoder: &mut wgpu::CommandEncoder,
|
||||
resources: &mut egui_wgpu::CallbackResources,
|
||||
) -> Vec<wgpu::CommandBuffer> {
|
||||
if let Some(renderer) = resources.get_mut::<FractalRenderer>() {
|
||||
queue.write_buffer(
|
||||
&renderer.uniform_buffer,
|
||||
0,
|
||||
bytemuck::bytes_of(&self.uniforms),
|
||||
);
|
||||
|
||||
if renderer.uploaded_generation != self.generation && !self.reference.is_empty() {
|
||||
let count = self.reference.len().min(MAX_REF_POINTS);
|
||||
queue.write_buffer(
|
||||
&renderer.ref_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&self.reference[..count]),
|
||||
);
|
||||
renderer.uploaded_generation = self.generation;
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&self,
|
||||
_info: egui::PaintCallbackInfo,
|
||||
render_pass: &mut wgpu::RenderPass<'static>,
|
||||
resources: &egui_wgpu::CallbackResources,
|
||||
) {
|
||||
if let Some(renderer) = resources.get::<FractalRenderer>() {
|
||||
render_pass.set_pipeline(&renderer.pipeline);
|
||||
render_pass.set_bind_group(0, &renderer.bind_group, &[]);
|
||||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! 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&re=<dec>&im=<dec>&hh=<f64>&it=<u32>&cs=<f32>&co=<f32>` with
|
||||
//! `m=j&jr=<f64>&ji=<f64>` added for Julia. `re`/`im` are full-precision decimal
|
||||
//! strings.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShareState {
|
||||
pub julia: bool,
|
||||
pub center_re: String,
|
||||
pub center_im: String,
|
||||
pub half_height: f64,
|
||||
pub iterations: u32,
|
||||
pub julia_c: (f64, f64),
|
||||
pub color_scale: f32,
|
||||
pub color_offset: f32,
|
||||
}
|
||||
|
||||
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!(
|
||||
"&re={}&im={}&hh={}&it={}",
|
||||
self.center_re, self.center_im, self.half_height, self.iterations
|
||||
));
|
||||
if self.julia {
|
||||
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));
|
||||
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),
|
||||
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),
|
||||
),
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let s = ShareState {
|
||||
julia: true,
|
||||
center_re: "-0.743643887037158704752191506114774".into(),
|
||||
center_im: "0.131825904205311970493132056385139".into(),
|
||||
half_height: 1.5e-20,
|
||||
iterations: 4000,
|
||||
julia_c: (-0.123, 0.745),
|
||||
color_scale: 0.02,
|
||||
color_offset: 0.25,
|
||||
};
|
||||
let d = ShareState::decode(&s.encode()).unwrap();
|
||||
assert_eq!(d.julia, s.julia);
|
||||
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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user