feat: add lambda fractal
This commit is contained in:
+94
-9
@@ -46,6 +46,7 @@ const KINDS: &[(FractalKind, &str)] = &[
|
|||||||
(FractalKind::Perpendicular, "Perpendicular"),
|
(FractalKind::Perpendicular, "Perpendicular"),
|
||||||
(FractalKind::Buffalo, "Buffalo"),
|
(FractalKind::Buffalo, "Buffalo"),
|
||||||
(FractalKind::Phoenix, "Phoenix"),
|
(FractalKind::Phoenix, "Phoenix"),
|
||||||
|
(FractalKind::Lambda, "Lambda"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// UI label for a fractal kind.
|
/// UI label for a fractal kind.
|
||||||
@@ -60,7 +61,7 @@ fn kind_label(kind: FractalKind) -> &'static str {
|
|||||||
type JuliaPreset = (&'static str, f64, f64, u32, Option<(f64, f64)>);
|
type JuliaPreset = (&'static str, f64, f64, u32, Option<(f64, f64)>);
|
||||||
|
|
||||||
/// Nice-looking Julia constants offered as presets.
|
/// Nice-looking Julia constants offered as presets.
|
||||||
const JULIA_PRESETS: [&[JuliaPreset]; FractalKind::Phoenix as usize + 1] = [
|
const JULIA_PRESETS: [&[JuliaPreset]; FractalKind::Lambda as usize + 1] = [
|
||||||
&[
|
&[
|
||||||
("dendrite", -0.8, 0.156, 400, None),
|
("dendrite", -0.8, 0.156, 400, None),
|
||||||
("rabbit", -0.123, 0.745, 400, None),
|
("rabbit", -0.123, 0.745, 400, None),
|
||||||
@@ -78,6 +79,7 @@ const JULIA_PRESETS: [&[JuliaPreset]; FractalKind::Phoenix as usize + 1] = [
|
|||||||
("archipelago 1", -0.415, -0.267, 500, Some((-0.556, 0.253))),
|
("archipelago 1", -0.415, -0.267, 500, Some((-0.556, 0.253))),
|
||||||
("archipelago 2", -0.556, 0.253, 500, Some((-0.415, -0.267))),
|
("archipelago 2", -0.556, 0.253, 500, Some((-0.415, -0.267))),
|
||||||
],
|
],
|
||||||
|
&[],
|
||||||
];
|
];
|
||||||
|
|
||||||
type SetPreset = (
|
type SetPreset = (
|
||||||
@@ -92,7 +94,7 @@ type SetPreset = (
|
|||||||
/// Curated beautiful locations offered as one-click presets.
|
/// Curated beautiful locations offered as one-click presets.
|
||||||
/// Each is `(name, center_re, center_im, half_height, iterations)`; the centers
|
/// Each is `(name, center_re, center_im, half_height, iterations)`; the centers
|
||||||
/// are decimals parsed at full precision so deep places stay sharp.
|
/// are decimals parsed at full precision so deep places stay sharp.
|
||||||
const SET_PRESETS: [&[SetPreset]; FractalKind::Phoenix as usize + 1] = [
|
const SET_PRESETS: [&[SetPreset]; FractalKind::Lambda as usize + 1] = [
|
||||||
&[
|
&[
|
||||||
(
|
(
|
||||||
"Seahorse Valley",
|
"Seahorse Valley",
|
||||||
@@ -142,6 +144,7 @@ const SET_PRESETS: [&[SetPreset]; FractalKind::Phoenix as usize + 1] = [
|
|||||||
1000,
|
1000,
|
||||||
Some((-0.9, -0.49)),
|
Some((-0.9, -0.49)),
|
||||||
)],
|
)],
|
||||||
|
&[],
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Parameters a reference orbit was (or will be) computed for. Used to decide
|
/// Parameters a reference orbit was (or will be) computed for. Used to decide
|
||||||
@@ -153,6 +156,7 @@ struct RequestKey {
|
|||||||
julia: bool,
|
julia: bool,
|
||||||
julia_c: (f64, f64),
|
julia_c: (f64, f64),
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
|
lambda_l: (f64, f64),
|
||||||
iter: u32,
|
iter: u32,
|
||||||
kind: FractalKind,
|
kind: FractalKind,
|
||||||
power: u32,
|
power: u32,
|
||||||
@@ -196,6 +200,13 @@ struct AnimState {
|
|||||||
phoenix_base: (f64, f64),
|
phoenix_base: (f64, f64),
|
||||||
phoenix_angle: f64,
|
phoenix_angle: f64,
|
||||||
|
|
||||||
|
/// Drift the Lambda distortion `λ` around a circle.
|
||||||
|
lambda: bool,
|
||||||
|
lambda_speed: f32,
|
||||||
|
lambda_radius: f64,
|
||||||
|
lambda_base: (f64, f64),
|
||||||
|
lambda_angle: f64,
|
||||||
|
|
||||||
/// Continuously zoom toward the current center.
|
/// Continuously zoom toward the current center.
|
||||||
zoom: bool,
|
zoom: bool,
|
||||||
/// e-folds per second; positive zooms in, negative zooms out.
|
/// e-folds per second; positive zooms in, negative zooms out.
|
||||||
@@ -217,6 +228,11 @@ impl Default for AnimState {
|
|||||||
phoenix_radius: 0.08,
|
phoenix_radius: 0.08,
|
||||||
phoenix_base: (0.0, 0.0),
|
phoenix_base: (0.0, 0.0),
|
||||||
phoenix_angle: 0.0,
|
phoenix_angle: 0.0,
|
||||||
|
lambda: false,
|
||||||
|
lambda_speed: 0.05,
|
||||||
|
lambda_radius: 0.08,
|
||||||
|
lambda_base: (0.0, 0.0),
|
||||||
|
lambda_angle: 0.0,
|
||||||
zoom: false,
|
zoom: false,
|
||||||
zoom_speed: 0.5,
|
zoom_speed: 0.5,
|
||||||
}
|
}
|
||||||
@@ -234,6 +250,8 @@ pub struct FractalApp {
|
|||||||
julia_c: (f64, f64),
|
julia_c: (f64, f64),
|
||||||
/// Distortion constant `p` for the Phoenix kind (`z^2 + c + p·z_{n-1}`).
|
/// Distortion constant `p` for the Phoenix kind (`z^2 + c + p·z_{n-1}`).
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
|
/// Distortion constant `l` for the Lambda kind (`l·z(1 - z_{n-1})`).
|
||||||
|
lambda_l: (f64, f64),
|
||||||
max_iterations: u32,
|
max_iterations: u32,
|
||||||
/// When set, `max_iterations` tracks the zoom depth automatically (so deep
|
/// When set, `max_iterations` tracks the zoom depth automatically (so deep
|
||||||
/// zooms stay sharp without hand-tuning); the manual slider takes over when
|
/// zooms stay sharp without hand-tuning); the manual slider takes over when
|
||||||
@@ -356,6 +374,7 @@ impl FractalApp {
|
|||||||
power: 3,
|
power: 3,
|
||||||
julia_c: (-0.8, 0.156),
|
julia_c: (-0.8, 0.156),
|
||||||
phoenix_p: (-0.5, 0.0),
|
phoenix_p: (-0.5, 0.0),
|
||||||
|
lambda_l: (-0.5, 0.0),
|
||||||
max_iterations: 512,
|
max_iterations: 512,
|
||||||
auto_iterations: true,
|
auto_iterations: true,
|
||||||
color_scale: 0.15,
|
color_scale: 0.15,
|
||||||
@@ -517,6 +536,7 @@ impl FractalApp {
|
|||||||
iterations: self.max_iterations,
|
iterations: self.max_iterations,
|
||||||
julia_c: self.julia_c,
|
julia_c: self.julia_c,
|
||||||
phoenix_p: self.phoenix_p,
|
phoenix_p: self.phoenix_p,
|
||||||
|
lambda_l: self.lambda_l,
|
||||||
color_scale: self.color_scale,
|
color_scale: self.color_scale,
|
||||||
color_offset: self.color_offset,
|
color_offset: self.color_offset,
|
||||||
palette: self.palette,
|
palette: self.palette,
|
||||||
@@ -534,6 +554,7 @@ impl FractalApp {
|
|||||||
self.power = s.power.clamp(2, 8);
|
self.power = s.power.clamp(2, 8);
|
||||||
self.julia_c = s.julia_c;
|
self.julia_c = s.julia_c;
|
||||||
self.phoenix_p = s.phoenix_p;
|
self.phoenix_p = s.phoenix_p;
|
||||||
|
self.lambda_l = s.lambda_l;
|
||||||
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;
|
self.palette = (s.palette as usize).min(PALETTE_NAMES.len() - 1) as u32;
|
||||||
@@ -582,6 +603,7 @@ impl FractalApp {
|
|||||||
FractalKind::Perpendicular => (-0.5, 0.0, 1.5),
|
FractalKind::Perpendicular => (-0.5, 0.0, 1.5),
|
||||||
FractalKind::Buffalo => (-0.5, -0.5, 1.5),
|
FractalKind::Buffalo => (-0.5, -0.5, 1.5),
|
||||||
FractalKind::Phoenix => (0.0, 0.0, 1.6),
|
FractalKind::Phoenix => (0.0, 0.0, 1.6),
|
||||||
|
FractalKind::Lambda => (0.0, 0.0, 1.6),
|
||||||
};
|
};
|
||||||
ViewState::with_center(big_from_f64(cr, 53), big_from_f64(ci, 53), hh)
|
ViewState::with_center(big_from_f64(cr, 53), big_from_f64(ci, 53), hh)
|
||||||
}
|
}
|
||||||
@@ -594,6 +616,7 @@ impl FractalApp {
|
|||||||
julia: matches!(self.mode, FractalMode::Julia),
|
julia: matches!(self.mode, FractalMode::Julia),
|
||||||
julia_c: self.julia_c,
|
julia_c: self.julia_c,
|
||||||
phoenix_p: self.phoenix_p,
|
phoenix_p: self.phoenix_p,
|
||||||
|
lambda_l: self.lambda_l,
|
||||||
iter: self.max_iterations,
|
iter: self.max_iterations,
|
||||||
kind: self.kind,
|
kind: self.kind,
|
||||||
power: self.power,
|
power: self.power,
|
||||||
@@ -609,7 +632,8 @@ impl FractalApp {
|
|||||||
|
|
||||||
/// Whether the reference should be (re)computed: parameters changed, or the
|
/// Whether the reference should be (re)computed: parameters changed, or the
|
||||||
/// view drifted / zoomed far enough that the current reference no longer
|
/// view drifted / zoomed far enough that the current reference no longer
|
||||||
/// serves it well.
|
/// serves it well. Lambda in Set mode has a static fractal (doesn't depend
|
||||||
|
/// on center), so we skip center drift checks but allow zoom precision updates.
|
||||||
fn should_request(&self) -> bool {
|
fn should_request(&self) -> bool {
|
||||||
let Some(key) = &self.last_request else {
|
let Some(key) = &self.last_request else {
|
||||||
return true;
|
return true;
|
||||||
@@ -617,12 +641,19 @@ impl FractalApp {
|
|||||||
if key.julia != matches!(self.mode, FractalMode::Julia)
|
if key.julia != matches!(self.mode, FractalMode::Julia)
|
||||||
|| key.julia_c != self.julia_c
|
|| key.julia_c != self.julia_c
|
||||||
|| key.phoenix_p != self.phoenix_p
|
|| key.phoenix_p != self.phoenix_p
|
||||||
|
|| key.lambda_l != self.lambda_l
|
||||||
|| key.iter != self.max_iterations
|
|| key.iter != self.max_iterations
|
||||||
|| key.kind != self.kind
|
|| key.kind != self.kind
|
||||||
|| key.power != self.power
|
|| key.power != self.power
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Lambda in Set mode is a static fractal; don't trigger recompute on center drift.
|
||||||
|
if self.kind == FractalKind::Lambda && matches!(self.mode, FractalMode::Mandelbrot) {
|
||||||
|
// But still recompute on significant zoom changes for precision
|
||||||
|
let ratio = self.view.half_height / key.half_height;
|
||||||
|
return !(0.5..=2.0).contains(&ratio);
|
||||||
|
}
|
||||||
let ratio = self.view.half_height / key.half_height;
|
let ratio = self.view.half_height / key.half_height;
|
||||||
self.drift_from(key) > 0.5 * self.view.half_height || !(0.5..=2.0).contains(&ratio)
|
self.drift_from(key) > 0.5 * self.view.half_height || !(0.5..=2.0).contains(&ratio)
|
||||||
}
|
}
|
||||||
@@ -650,10 +681,16 @@ impl FractalApp {
|
|||||||
/// thread and pick up completed results. Web: compute inline.
|
/// thread and pick up completed results. Web: compute inline.
|
||||||
fn ensure_reference(&mut self) {
|
fn ensure_reference(&mut self) {
|
||||||
if self.should_request() {
|
if self.should_request() {
|
||||||
let key = self.current_key();
|
let mut key = self.current_key();
|
||||||
let precision = self.view.precision_bits();
|
let precision = self.view.precision_bits();
|
||||||
let max_iter = key.iter.min(MAX_REF_POINTS as u32 - 1);
|
let max_iter = key.iter.min(MAX_REF_POINTS as u32 - 1);
|
||||||
|
|
||||||
|
// Lambda in Set mode has a static fractal centered at origin.
|
||||||
|
if key.kind == FractalKind::Lambda && !key.julia {
|
||||||
|
key.center_re = big_from_f64(0.0, precision);
|
||||||
|
key.center_im = big_from_f64(0.0, precision);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
self.worker.request(crate::worker::RefRequest {
|
self.worker.request(crate::worker::RefRequest {
|
||||||
@@ -667,6 +704,7 @@ impl FractalApp {
|
|||||||
kind: key.kind,
|
kind: key.kind,
|
||||||
power: key.power,
|
power: key.power,
|
||||||
phoenix_p: key.phoenix_p,
|
phoenix_p: key.phoenix_p,
|
||||||
|
lambda_l: key.lambda_l,
|
||||||
});
|
});
|
||||||
self.pending = true;
|
self.pending = true;
|
||||||
}
|
}
|
||||||
@@ -685,6 +723,7 @@ impl FractalApp {
|
|||||||
key.kind,
|
key.kind,
|
||||||
key.power,
|
key.power,
|
||||||
key.phoenix_p,
|
key.phoenix_p,
|
||||||
|
key.lambda_l,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
compute_set_reference(
|
compute_set_reference(
|
||||||
@@ -695,6 +734,7 @@ impl FractalApp {
|
|||||||
key.kind,
|
key.kind,
|
||||||
key.power,
|
key.power,
|
||||||
key.phoenix_p,
|
key.phoenix_p,
|
||||||
|
key.lambda_l,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
self.apply_reference(
|
self.apply_reference(
|
||||||
@@ -727,12 +767,13 @@ impl FractalApp {
|
|||||||
is_julia: matches!(self.mode, FractalMode::Julia) as u32,
|
is_julia: matches!(self.mode, FractalMode::Julia) as u32,
|
||||||
palette_id: self.palette,
|
palette_id: self.palette,
|
||||||
aa_level: if self.antialias { 2 } else { 1 },
|
aa_level: if self.antialias { 2 } else { 1 },
|
||||||
kind: self.kind.shader_id(),
|
kind: self.kind as u32,
|
||||||
power: self.power,
|
power: self.power,
|
||||||
dc_offset: self.dc_offset(),
|
dc_offset: self.dc_offset(),
|
||||||
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
||||||
|
lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32],
|
||||||
de_coloring: self.de_coloring as u32,
|
de_coloring: self.de_coloring as u32,
|
||||||
_pad: [0, 0, 0],
|
_pad: [0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,10 +1087,11 @@ impl FractalApp {
|
|||||||
/// repaint while active. Animations render at full resolution/AA (they do not
|
/// repaint while active. Animations render at full resolution/AA (they do not
|
||||||
/// trigger the interaction low-res pass).
|
/// trigger the interaction low-res pass).
|
||||||
fn tick_animations(&mut self, ui: &egui::Ui) {
|
fn tick_animations(&mut self, ui: &egui::Ui) {
|
||||||
// Julia c only matters in Julia mode; Phoenix p only for the Phoenix kind.
|
// Julia c only matters in Julia mode; Phoenix p only for the Phoenix kind; Lambda λ only for Lambda kind.
|
||||||
let julia_on = self.anim.julia && self.mode == FractalMode::Julia;
|
let julia_on = self.anim.julia && self.mode == FractalMode::Julia;
|
||||||
let phoenix_on = self.anim.phoenix && self.kind == FractalKind::Phoenix;
|
let phoenix_on = self.anim.phoenix && self.kind == FractalKind::Phoenix;
|
||||||
if !(self.anim.color || self.anim.zoom || julia_on || phoenix_on) {
|
let lambda_on = self.anim.lambda && self.kind == FractalKind::Lambda;
|
||||||
|
if !(self.anim.color || self.anim.zoom || julia_on || phoenix_on || lambda_on) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,6 +1118,15 @@ impl FractalApp {
|
|||||||
self.anim.phoenix_base.1 + self.anim.phoenix_radius * s,
|
self.anim.phoenix_base.1 + self.anim.phoenix_radius * s,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let lambda_on = self.anim.lambda && self.kind == FractalKind::Lambda;
|
||||||
|
if lambda_on {
|
||||||
|
self.anim.lambda_angle += std::f64::consts::TAU * self.anim.lambda_speed as f64 * dt;
|
||||||
|
let (s, c) = self.anim.lambda_angle.sin_cos();
|
||||||
|
self.lambda_l = (
|
||||||
|
self.anim.lambda_base.0 + self.anim.lambda_radius * c,
|
||||||
|
self.anim.lambda_base.1 + self.anim.lambda_radius * s,
|
||||||
|
);
|
||||||
|
}
|
||||||
if self.anim.zoom && self.anim.zoom_speed != 0.0 {
|
if self.anim.zoom && self.anim.zoom_speed != 0.0 {
|
||||||
let min_hh = DEFAULT_HALF_HEIGHT * 1.0e-26; // practical f32-perturbation depth
|
let min_hh = DEFAULT_HALF_HEIGHT * 1.0e-26; // practical f32-perturbation depth
|
||||||
let max_hh = DEFAULT_HALF_HEIGHT * 4.0;
|
let max_hh = DEFAULT_HALF_HEIGHT * 4.0;
|
||||||
@@ -1124,6 +1175,22 @@ impl FractalApp {
|
|||||||
ui.label("i");
|
ui.label("i");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if self.kind == FractalKind::Lambda {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("λ =");
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.lambda_l.0)
|
||||||
|
.speed(0.001)
|
||||||
|
.range(-2.0..=2.0),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.lambda_l.1)
|
||||||
|
.speed(0.001)
|
||||||
|
.range(-2.0..=2.0),
|
||||||
|
);
|
||||||
|
ui.label("i");
|
||||||
|
});
|
||||||
|
}
|
||||||
if self.kind != prev_kind {
|
if self.kind != prev_kind {
|
||||||
self.view = Self::default_view_for(self.mode, self.kind);
|
self.view = Self::default_view_for(self.mode, self.kind);
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1200,7 @@ impl FractalApp {
|
|||||||
ui.radio_value(&mut self.mode, FractalMode::Julia, "Julia");
|
ui.radio_value(&mut self.mode, FractalMode::Julia, "Julia");
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.mode == FractalMode::Julia {
|
if self.mode == FractalMode::Julia && self.kind != FractalKind::Lambda {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("c =");
|
ui.label("c =");
|
||||||
ui.add(
|
ui.add(
|
||||||
@@ -1271,6 +1338,24 @@ impl FractalApp {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.kind == FractalKind::Lambda {
|
||||||
|
if ui.checkbox(&mut self.anim.lambda, "Morph λ").changed() && self.anim.lambda {
|
||||||
|
self.anim.lambda_base = self.lambda_l;
|
||||||
|
self.anim.lambda_angle = 0.0;
|
||||||
|
}
|
||||||
|
if self.anim.lambda {
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.anim.lambda_speed, 0.005..=0.5)
|
||||||
|
.text("λ rev/s")
|
||||||
|
.logarithmic(true),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.anim.lambda_radius, 0.005..=0.5)
|
||||||
|
.text("λ radius")
|
||||||
|
.logarithmic(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|||||||
+98
-38
@@ -18,37 +18,23 @@ use crate::view::{Big, big_from_f64};
|
|||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum FractalKind {
|
pub enum FractalKind {
|
||||||
/// `z -> z^2 + c`.
|
/// `z -> z^2 + c`.
|
||||||
Mandelbrot,
|
Mandelbrot = 0,
|
||||||
/// `z -> (|Re z| + i|Im z|)^2 + c`.
|
/// `z -> (|Re z| + i|Im z|)^2 + c`.
|
||||||
BurningShip,
|
BurningShip = 1,
|
||||||
/// `z -> conj(z)^2 + c` (the Mandelbar).
|
/// `z -> conj(z)^2 + c` (the Mandelbar).
|
||||||
Tricorn,
|
Tricorn = 2,
|
||||||
/// `z -> z^power + c` (power >= 2).
|
/// `z -> z^power + c` (power >= 2).
|
||||||
Multibrot,
|
Multibrot = 3,
|
||||||
/// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square).
|
/// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square).
|
||||||
Celtic,
|
Celtic = 4,
|
||||||
/// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input).
|
/// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input).
|
||||||
Perpendicular,
|
Perpendicular = 5,
|
||||||
/// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs).
|
/// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs).
|
||||||
Buffalo,
|
Buffalo = 6,
|
||||||
/// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`).
|
/// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`).
|
||||||
Phoenix,
|
Phoenix = 7,
|
||||||
}
|
/// `z -> lambda·z(1 - z)` (logistic map).
|
||||||
|
Lambda = 8,
|
||||||
impl FractalKind {
|
|
||||||
/// Integer id matching the shader's `KIND_*` constants.
|
|
||||||
pub fn shader_id(self) -> u32 {
|
|
||||||
match self {
|
|
||||||
FractalKind::Mandelbrot => 0,
|
|
||||||
FractalKind::BurningShip => 1,
|
|
||||||
FractalKind::Tricorn => 2,
|
|
||||||
FractalKind::Multibrot => 3,
|
|
||||||
FractalKind::Celtic => 4,
|
|
||||||
FractalKind::Perpendicular => 5,
|
|
||||||
FractalKind::Buffalo => 6,
|
|
||||||
FractalKind::Phoenix => 7,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
||||||
@@ -70,6 +56,7 @@ pub fn compute_reference(
|
|||||||
kind: FractalKind,
|
kind: FractalKind,
|
||||||
power: u32,
|
power: u32,
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
|
lambda_l: (f64, f64),
|
||||||
) -> Vec<[f32; 2]> {
|
) -> Vec<[f32; 2]> {
|
||||||
let cr = c_re.clone().with_precision(precision).value();
|
let cr = c_re.clone().with_precision(precision).value();
|
||||||
let ci = c_im.clone().with_precision(precision).value();
|
let ci = c_im.clone().with_precision(precision).value();
|
||||||
@@ -82,6 +69,9 @@ pub fn compute_reference(
|
|||||||
// Phoenix distortion constant `p` (a small fixed complex number).
|
// Phoenix distortion constant `p` (a small fixed complex number).
|
||||||
let pr = big_from_f64(phoenix_p.0, precision);
|
let pr = big_from_f64(phoenix_p.0, precision);
|
||||||
let pi = big_from_f64(phoenix_p.1, precision);
|
let pi = big_from_f64(phoenix_p.1, precision);
|
||||||
|
// Lambda distortion constant `l` (a small fixed complex number).
|
||||||
|
let lr = big_from_f64(lambda_l.0, precision);
|
||||||
|
let li = big_from_f64(lambda_l.1, precision);
|
||||||
|
|
||||||
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
||||||
|
|
||||||
@@ -144,6 +134,14 @@ pub fn compute_reference(
|
|||||||
let pzi = &pr * &zi_prev + &pi * &zr_prev;
|
let pzi = &pr * &zi_prev + &pi * &zr_prev;
|
||||||
(re2 + &cr + pzr, im2 + &ci + pzi)
|
(re2 + &cr + pzr, im2 + &ci + pzi)
|
||||||
}
|
}
|
||||||
|
FractalKind::Lambda => {
|
||||||
|
// λ·z(1 - z): logistic map.
|
||||||
|
let re2 = 1 - &zr;
|
||||||
|
let im2 = -&zi;
|
||||||
|
let lzr = &lr * &zr - &li * &zi;
|
||||||
|
let lzi = &lr * &zi + &li * &zr;
|
||||||
|
(&lzr * &re2 - &lzi * &im2, re2 * lzi + lzr * im2)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shift the previous iterate (only the Phoenix arm reads it).
|
// Shift the previous iterate (only the Phoenix arm reads it).
|
||||||
@@ -182,6 +180,7 @@ fn complex_pow(zr: &Big, zi: &Big, power: u32, precision: usize) -> (Big, Big) {
|
|||||||
|
|
||||||
/// Convenience: parameter-plane ("Mandelbrot-set") reference (`z0 = 0`,
|
/// Convenience: parameter-plane ("Mandelbrot-set") reference (`z0 = 0`,
|
||||||
/// `c = center`) for any `kind`.
|
/// `c = center`) for any `kind`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn compute_set_reference(
|
pub fn compute_set_reference(
|
||||||
center_re: &Big,
|
center_re: &Big,
|
||||||
center_im: &Big,
|
center_im: &Big,
|
||||||
@@ -190,10 +189,11 @@ pub fn compute_set_reference(
|
|||||||
kind: FractalKind,
|
kind: FractalKind,
|
||||||
power: u32,
|
power: u32,
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
|
lambda_l: (f64, f64),
|
||||||
) -> Vec<[f32; 2]> {
|
) -> Vec<[f32; 2]> {
|
||||||
let zero = big_zero(precision);
|
let zero = big_zero(precision);
|
||||||
compute_reference(
|
compute_reference(
|
||||||
&zero, &zero, center_re, center_im, max_iter, precision, kind, power, phoenix_p,
|
&zero, &zero, center_re, center_im, max_iter, precision, kind, power, phoenix_p, lambda_l,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +207,16 @@ mod tests {
|
|||||||
fn reference_matches_naive_f64() {
|
fn reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.75_f64).unwrap();
|
let cr = Big::try_from(-0.75_f64).unwrap();
|
||||||
let ci = Big::try_from(0.1_f64).unwrap();
|
let ci = Big::try_from(0.1_f64).unwrap();
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 60, 200, FractalKind::Mandelbrot, 2, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Mandelbrot,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
// Independent naive f64 orbit.
|
// Independent naive f64 orbit.
|
||||||
let (c_re, c_im) = (-0.75_f64, 0.1_f64);
|
let (c_re, c_im) = (-0.75_f64, 0.1_f64);
|
||||||
@@ -238,8 +246,16 @@ mod tests {
|
|||||||
fn interior_orbit_runs_full_length() {
|
fn interior_orbit_runs_full_length() {
|
||||||
let cr = Big::try_from(-0.2_f64).unwrap();
|
let cr = Big::try_from(-0.2_f64).unwrap();
|
||||||
let ci = Big::try_from(0.0_f64).unwrap();
|
let ci = Big::try_from(0.0_f64).unwrap();
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 500, 120, FractalKind::Mandelbrot, 2, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
500,
|
||||||
|
120,
|
||||||
|
FractalKind::Mandelbrot,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,8 +264,16 @@ mod tests {
|
|||||||
fn burning_ship_reference_matches_naive_f64() {
|
fn burning_ship_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-1.75_f64).unwrap();
|
let cr = Big::try_from(-1.75_f64).unwrap();
|
||||||
let ci = Big::try_from(-0.03_f64).unwrap();
|
let ci = Big::try_from(-0.03_f64).unwrap();
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 60, 200, FractalKind::BurningShip, 2, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::BurningShip,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-1.75_f64, -0.03_f64);
|
let (c_re, c_im) = (-1.75_f64, -0.03_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -269,8 +293,16 @@ mod tests {
|
|||||||
fn multibrot3_reference_matches_naive_f64() {
|
fn multibrot3_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(0.3_f64).unwrap();
|
let cr = Big::try_from(0.3_f64).unwrap();
|
||||||
let ci = Big::try_from(0.2_f64).unwrap();
|
let ci = Big::try_from(0.2_f64).unwrap();
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 60, 200, FractalKind::Multibrot, 3, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Multibrot,
|
||||||
|
3,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (0.3_f64, 0.2_f64);
|
let (c_re, c_im) = (0.3_f64, 0.2_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -304,6 +336,7 @@ mod tests {
|
|||||||
FractalKind::Mandelbrot,
|
FractalKind::Mandelbrot,
|
||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
||||||
@@ -324,7 +357,16 @@ mod tests {
|
|||||||
fn celtic_reference_matches_naive_f64() {
|
fn celtic_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.6_f64).unwrap();
|
let cr = Big::try_from(-0.6_f64).unwrap();
|
||||||
let ci = Big::try_from(0.4_f64).unwrap();
|
let ci = Big::try_from(0.4_f64).unwrap();
|
||||||
let points = compute_set_reference(&cr, &ci, 60, 200, FractalKind::Celtic, 2, (0.0, 0.0));
|
let points = compute_set_reference(
|
||||||
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Celtic,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-0.6_f64, 0.4_f64);
|
let (c_re, c_im) = (-0.6_f64, 0.4_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -345,8 +387,16 @@ mod tests {
|
|||||||
fn perpendicular_reference_matches_naive_f64() {
|
fn perpendicular_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.7_f64).unwrap();
|
let cr = Big::try_from(-0.7_f64).unwrap();
|
||||||
let ci = Big::try_from(-0.2_f64).unwrap();
|
let ci = Big::try_from(-0.2_f64).unwrap();
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 60, 200, FractalKind::Perpendicular, 2, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Perpendicular,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-0.7_f64, -0.2_f64);
|
let (c_re, c_im) = (-0.7_f64, -0.2_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -367,7 +417,16 @@ mod tests {
|
|||||||
fn buffalo_reference_matches_naive_f64() {
|
fn buffalo_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-1.2_f64).unwrap();
|
let cr = Big::try_from(-1.2_f64).unwrap();
|
||||||
let ci = Big::try_from(-0.35_f64).unwrap();
|
let ci = Big::try_from(-0.35_f64).unwrap();
|
||||||
let points = compute_set_reference(&cr, &ci, 60, 200, FractalKind::Buffalo, 2, (0.0, 0.0));
|
let points = compute_set_reference(
|
||||||
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Buffalo,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-1.2_f64, -0.35_f64);
|
let (c_re, c_im) = (-1.2_f64, -0.35_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -389,7 +448,8 @@ mod tests {
|
|||||||
let cr = Big::try_from(0.5667_f64).unwrap();
|
let cr = Big::try_from(0.5667_f64).unwrap();
|
||||||
let ci = Big::try_from(0.0_f64).unwrap();
|
let ci = Big::try_from(0.0_f64).unwrap();
|
||||||
let p = (-0.5_f64, 0.0_f64);
|
let p = (-0.5_f64, 0.0_f64);
|
||||||
let points = compute_set_reference(&cr, &ci, 60, 200, FractalKind::Phoenix, 2, p);
|
let points =
|
||||||
|
compute_set_reference(&cr, &ci, 60, 200, FractalKind::Phoenix, 2, p, (0.0, 0.0));
|
||||||
|
|
||||||
let (c_re, c_im) = (0.5667_f64, 0.0_f64);
|
let (c_re, c_im) = (0.5667_f64, 0.0_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
|
|||||||
@@ -78,10 +78,13 @@ pub struct Uniforms {
|
|||||||
/// ignored by other kinds. Kept next to `dc_offset` so both `vec2`s land on
|
/// ignored by other kinds. Kept next to `dc_offset` so both `vec2`s land on
|
||||||
/// 8-byte boundaries, matching the shader's layout.
|
/// 8-byte boundaries, matching the shader's layout.
|
||||||
pub phoenix_p: [f32; 2],
|
pub phoenix_p: [f32; 2],
|
||||||
|
/// Distortion constant `l` for the Lambda map (`l·z(1 - z)`);
|
||||||
|
/// ignored by other kinds.
|
||||||
|
pub lambda_l: [f32; 2],
|
||||||
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
pub de_coloring: u32,
|
pub de_coloring: u32,
|
||||||
/// Padding to a 16-byte multiple (uniform buffer requirement).
|
/// Padding to a 16-byte multiple (uniform buffer requirement).
|
||||||
pub _pad: [u32; 3],
|
pub _pad: [u32; 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub struct ShareState {
|
|||||||
pub julia_c: (f64, f64),
|
pub julia_c: (f64, f64),
|
||||||
/// Distortion constant for the Phoenix kind (ignored by others).
|
/// Distortion constant for the Phoenix kind (ignored by others).
|
||||||
pub phoenix_p: (f64, f64),
|
pub phoenix_p: (f64, f64),
|
||||||
|
/// Distortion constant for the Lambda kind (ignored by others).
|
||||||
|
pub lambda_l: (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).
|
/// Palette index (`palette_id` in the shader).
|
||||||
@@ -44,6 +46,7 @@ impl ShareState {
|
|||||||
FractalKind::Perpendicular => "perp",
|
FractalKind::Perpendicular => "perp",
|
||||||
FractalKind::Buffalo => "buffalo",
|
FractalKind::Buffalo => "buffalo",
|
||||||
FractalKind::Phoenix => "phoenix",
|
FractalKind::Phoenix => "phoenix",
|
||||||
|
FractalKind::Lambda => "lambda",
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
s.push_str(&format!("&pw={}", self.power));
|
s.push_str(&format!("&pw={}", self.power));
|
||||||
@@ -53,6 +56,7 @@ impl ShareState {
|
|||||||
));
|
));
|
||||||
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!("&px={}&py={}", self.phoenix_p.0, self.phoenix_p.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!(
|
s.push_str(&format!(
|
||||||
"&cs={}&co={}&pal={}",
|
"&cs={}&co={}&pal={}",
|
||||||
self.color_scale, self.color_offset, self.palette
|
self.color_scale, self.color_offset, self.palette
|
||||||
@@ -82,6 +86,7 @@ impl ShareState {
|
|||||||
"perp" => FractalKind::Perpendicular,
|
"perp" => FractalKind::Perpendicular,
|
||||||
"buffalo" => FractalKind::Buffalo,
|
"buffalo" => FractalKind::Buffalo,
|
||||||
"phoenix" => FractalKind::Phoenix,
|
"phoenix" => FractalKind::Phoenix,
|
||||||
|
"lambda" => FractalKind::Lambda,
|
||||||
_ => FractalKind::Mandelbrot,
|
_ => FractalKind::Mandelbrot,
|
||||||
})
|
})
|
||||||
.unwrap_or(FractalKind::Mandelbrot),
|
.unwrap_or(FractalKind::Mandelbrot),
|
||||||
@@ -98,6 +103,10 @@ impl ShareState {
|
|||||||
map.get("px").and_then(|s| s.parse().ok()).unwrap_or(-0.5),
|
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),
|
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),
|
||||||
|
),
|
||||||
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),
|
palette: map.get("pal").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
@@ -121,6 +130,7 @@ mod tests {
|
|||||||
iterations: 4000,
|
iterations: 4000,
|
||||||
julia_c: (-0.123, 0.745),
|
julia_c: (-0.123, 0.745),
|
||||||
phoenix_p: (-0.5, 0.1),
|
phoenix_p: (-0.5, 0.1),
|
||||||
|
lambda_l: (-0.5, 0.0),
|
||||||
color_scale: 0.02,
|
color_scale: 0.02,
|
||||||
color_offset: 0.25,
|
color_offset: 0.25,
|
||||||
palette: 3,
|
palette: 3,
|
||||||
|
|||||||
+36
-25
@@ -30,6 +30,9 @@ struct Uniforms {
|
|||||||
// Distortion constant p for the Phoenix map (z^2 + c + p*z_{n-1}); unused
|
// Distortion constant p for the Phoenix map (z^2 + c + p*z_{n-1}); unused
|
||||||
// by other kinds. Placed by dc_offset so both vec2s stay 8-byte aligned.
|
// by other kinds. Placed by dc_offset so both vec2s stay 8-byte aligned.
|
||||||
phoenix_p: vec2<f32>,
|
phoenix_p: vec2<f32>,
|
||||||
|
// Distortion constant l for the Lambda map (l*z(1 - z_{n-1})); unused
|
||||||
|
// by other kinds.
|
||||||
|
lambda_l: vec2<f32>,
|
||||||
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
de_coloring: u32,
|
de_coloring: u32,
|
||||||
};
|
};
|
||||||
@@ -42,6 +45,7 @@ const KIND_CELTIC: u32 = 4u;
|
|||||||
const KIND_PERPENDICULAR: u32 = 5u;
|
const KIND_PERPENDICULAR: u32 = 5u;
|
||||||
const KIND_BUFFALO: u32 = 6u;
|
const KIND_BUFFALO: u32 = 6u;
|
||||||
const KIND_PHOENIX: u32 = 7u;
|
const KIND_PHOENIX: u32 = 7u;
|
||||||
|
const KIND_LAMBDA: u32 = 8u;
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
||||||
@@ -82,7 +86,7 @@ fn conj(a: vec2<f32>) -> vec2<f32> {
|
|||||||
// the sign flips that happen all along the axes, where the ship's detail lives.
|
// the sign flips that happen all along the axes, where the ship's detail lives.
|
||||||
fn diffabs(c: f32, d: f32) -> f32 {
|
fn diffabs(c: f32, d: f32) -> f32 {
|
||||||
let cd = c + d;
|
let cd = c + d;
|
||||||
if (c >= 0.0) {
|
if c >= 0.0 {
|
||||||
return select(-(2.0 * c + d), d, cd >= 0.0);
|
return select(-(2.0 * c + d), d, cd >= 0.0);
|
||||||
}
|
}
|
||||||
return select(-d, 2.0 * c + d, cd > 0.0);
|
return select(-d, 2.0 * c + d, cd > 0.0);
|
||||||
@@ -120,7 +124,7 @@ fn multibrot_delta(z: vec2<f32>, e: vec2<f32>, p: u32) -> vec2<f32> {
|
|||||||
// where `z` is the reference orbit value X_m. `step_add` (dc) is added by the
|
// where `z` is the reference orbit value X_m. `step_add` (dc) is added by the
|
||||||
// caller. Must match `FractalKind` on the CPU side.
|
// caller. Must match `FractalKind` on the CPU side.
|
||||||
fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
||||||
if (u.kind == KIND_BURNING_SHIP) {
|
if u.kind == KIND_BURNING_SHIP {
|
||||||
// (|x| + i|y|)^2 has real part x^2 - y^2 (an ordinary square delta) and
|
// (|x| + i|y|)^2 has real part x^2 - y^2 (an ordinary square delta) and
|
||||||
// imaginary part 2|x y|. The imaginary delta is 2(|x y| - |X Y|); diffabs
|
// imaginary part 2|x y|. The imaginary delta is 2(|x y| - |X Y|); diffabs
|
||||||
// computes it exactly, even where the product x y changes sign — which the
|
// computes it exactly, even where the product x y changes sign — which the
|
||||||
@@ -129,29 +133,33 @@ fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
|||||||
let base = 2.0 * cmul(z, e) + cmul(e, e);
|
let base = 2.0 * cmul(z, e) + cmul(e, e);
|
||||||
let dp = z.x * e.y + z.y * e.x + e.x * e.y;
|
let dp = z.x * e.y + z.y * e.x + e.x * e.y;
|
||||||
return vec2<f32>(base.x, 2.0 * diffabs(z.x * z.y, dp));
|
return vec2<f32>(base.x, 2.0 * diffabs(z.x * z.y, dp));
|
||||||
} else if (u.kind == KIND_TRICORN) {
|
} else if u.kind == KIND_TRICORN {
|
||||||
let cz = conj(z);
|
let cz = conj(z);
|
||||||
let ce = conj(e);
|
let ce = conj(e);
|
||||||
return 2.0 * cmul(cz, ce) + cmul(ce, ce);
|
return 2.0 * cmul(cz, ce) + cmul(ce, ce);
|
||||||
} else if (u.kind == KIND_MULTIBROT) {
|
} else if u.kind == KIND_MULTIBROT {
|
||||||
return multibrot_delta(z, e, clamp(u.power, 2u, 8u));
|
return multibrot_delta(z, e, clamp(u.power, 2u, 8u));
|
||||||
} else if (u.kind == KIND_CELTIC) {
|
} else if u.kind == KIND_CELTIC {
|
||||||
// z^2 delta split: sq.x = delta of Re(z^2), sq.y = delta of Im(z^2).
|
// z^2 delta split: sq.x = delta of Re(z^2), sq.y = delta of Im(z^2).
|
||||||
// Celtic abs the real output, so |Re(z^2)| delta = diffabs(Re(Z^2), sq.x).
|
// Celtic abs the real output, so |Re(z^2)| delta = diffabs(Re(Z^2), sq.x).
|
||||||
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
||||||
return vec2<f32>(diffabs(z.x * z.x - z.y * z.y, sq.x), sq.y);
|
return vec2<f32>(diffabs(z.x * z.x - z.y * z.y, sq.x), sq.y);
|
||||||
} else if (u.kind == KIND_BUFFALO) {
|
} else if u.kind == KIND_BUFFALO {
|
||||||
// Abs both outputs: real |Re(z^2)|, imag -|Im(z^2)| (Im(Z^2) = 2 X Y).
|
// Abs both outputs: real |Re(z^2)|, imag -|Im(z^2)| (Im(Z^2) = 2 X Y).
|
||||||
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
||||||
return vec2<f32>(diffabs(z.x * z.x - z.y * z.y, sq.x),
|
return vec2<f32>(diffabs(z.x * z.x - z.y * z.y, sq.x),
|
||||||
-diffabs(2.0 * z.x * z.y, sq.y));
|
-diffabs(2.0 * z.x * z.y, sq.y));
|
||||||
} else if (u.kind == KIND_PERPENDICULAR) {
|
} else if u.kind == KIND_PERPENDICULAR {
|
||||||
// real x^2 - y^2 (ordinary square delta), imag -2 x |y|.
|
// real x^2 - y^2 (ordinary square delta), imag -2 x |y|.
|
||||||
// d(-2 x |y|) = -2[ X·(|Y+ey|-|Y|) + ex·|Y+ey| ]; diffabs gives |Y+ey|-|Y|.
|
// d(-2 x |y|) = -2[ X·(|Y+ey|-|Y|) + ex·|Y+ey| ]; diffabs gives |Y+ey|-|Y|.
|
||||||
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
let sq = 2.0 * cmul(z, e) + cmul(e, e);
|
||||||
let da = diffabs(z.y, e.y); // |Y + ey| - |Y|
|
let da = diffabs(z.y, e.y); // |Y + ey| - |Y|
|
||||||
let abs_yf = abs(z.y) + da; // |Y + ey|
|
let abs_yf = abs(z.y) + da; // |Y + ey|
|
||||||
return vec2<f32>(sq.x, -2.0 * (z.x * da + e.x * abs_yf));
|
return vec2<f32>(sq.x, -2.0 * (z.x * da + e.x * abs_yf));
|
||||||
|
} else if u.kind == KIND_LAMBDA {
|
||||||
|
// Lambda map: z^{n+1} = λ·z·(1-z). Delta: e = λ·e·(1-2z-e).
|
||||||
|
let one_minus_2z_minus_e = vec2<f32>(1.0 - 2.0*z.x - e.x, -2.0*z.y - e.y);
|
||||||
|
return cmul(u.lambda_l, cmul(e, one_minus_2z_minus_e));
|
||||||
}
|
}
|
||||||
return 2.0 * cmul(z, e) + cmul(e, e); // Mandelbrot (and Phoenix square part)
|
return 2.0 * cmul(z, e) + cmul(e, e); // Mandelbrot (and Phoenix square part)
|
||||||
}
|
}
|
||||||
@@ -162,31 +170,34 @@ fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
|||||||
// Burning Ship / Tricorn we use |f'| ~ |2Z|, which keeps the DE magnitude close
|
// Burning Ship / Tricorn we use |f'| ~ |2Z|, which keeps the DE magnitude close
|
||||||
// enough to de-speckle filaments.
|
// enough to de-speckle filaments.
|
||||||
fn fprime(z: vec2<f32>) -> vec2<f32> {
|
fn fprime(z: vec2<f32>) -> vec2<f32> {
|
||||||
if (u.kind == KIND_MULTIBROT) {
|
if u.kind == KIND_MULTIBROT {
|
||||||
let p = clamp(u.power, 2u, 8u);
|
let p = clamp(u.power, 2u, 8u);
|
||||||
var zk = vec2<f32>(1.0, 0.0); // Z^0
|
var zk = vec2<f32>(1.0, 0.0); // Z^0
|
||||||
for (var k: u32 = 1u; k < p; k = k + 1u) {
|
for (var k: u32 = 1u; k < p; k = k + 1u) {
|
||||||
zk = cmul(zk, z); // -> Z^{p-1}
|
zk = cmul(zk, z); // -> Z^{p-1}
|
||||||
}
|
}
|
||||||
return f32(p) * zk;
|
return f32(p) * zk;
|
||||||
|
} else if u.kind == KIND_LAMBDA {
|
||||||
|
// Lambda: f'(z) = λ·(1-2z).
|
||||||
|
return cmul(u.lambda_l, vec2<f32>(1.0 - 2.0*z.x, -2.0*z.y));
|
||||||
}
|
}
|
||||||
return 2.0 * z;
|
return 2.0 * z;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
||||||
fn palette(id: u32, t: f32) -> vec3<f32> {
|
fn palette(id: u32, t: f32) -> vec3<f32> {
|
||||||
if (id == 4u) {
|
if id == 4u {
|
||||||
return vec3<f32>(t, t, t); // grayscale
|
return vec3<f32>(t, t, t); // grayscale
|
||||||
}
|
}
|
||||||
let a = vec3<f32>(0.5, 0.5, 0.5);
|
let a = vec3<f32>(0.5, 0.5, 0.5);
|
||||||
let b = vec3<f32>(0.5, 0.5, 0.5);
|
let b = vec3<f32>(0.5, 0.5, 0.5);
|
||||||
var c = vec3<f32>(1.0, 1.0, 1.0);
|
var c = vec3<f32>(1.0, 1.0, 1.0);
|
||||||
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
||||||
if (id == 1u) {
|
if id == 1u {
|
||||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
||||||
} else if (id == 2u) {
|
} else if id == 2u {
|
||||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
||||||
} else if (id == 3u) {
|
} else if id == 3u {
|
||||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
c = vec3<f32>(1.0, 1.0, 0.5);
|
||||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
||||||
}
|
}
|
||||||
@@ -220,7 +231,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
// y_{n-1}, and its derivative for DE). Both start at 0 (y_{-1} = 0).
|
// y_{n-1}, and its derivative for DE). Both start at 0 (y_{-1} = 0).
|
||||||
var e_prev = vec2<f32>(0.0, 0.0);
|
var e_prev = vec2<f32>(0.0, 0.0);
|
||||||
var dz_prev = vec2<f32>(0.0, 0.0);
|
var dz_prev = vec2<f32>(0.0, 0.0);
|
||||||
if (u.is_julia != 0u) {
|
if u.is_julia != 0u {
|
||||||
step_add = vec2<f32>(0.0, 0.0);
|
step_add = vec2<f32>(0.0, 0.0);
|
||||||
e = offset;
|
e = offset;
|
||||||
dz = vec2<f32>(1.0, 0.0);
|
dz = vec2<f32>(1.0, 0.0);
|
||||||
@@ -237,20 +248,20 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
z = xm + e;
|
z = xm + e;
|
||||||
|
|
||||||
let z2 = dot(z, z);
|
let z2 = dot(z, z);
|
||||||
if (z2 > u.bailout_sq) {
|
if z2 > u.bailout_sq {
|
||||||
escaped = true;
|
escaped = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (n >= u.max_iter) {
|
if n >= u.max_iter {
|
||||||
break; // interior
|
break; // interior
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propagate the derivative of the full orbit (unaffected by rebasing,
|
// Propagate the derivative of the full orbit (unaffected by rebasing,
|
||||||
// which only re-expresses the same value). Only when DE is enabled.
|
// which only re-expresses the same value). Only when DE is enabled.
|
||||||
// Phoenix's two-term map adds p·dz_{n-1} and carries the previous dz.
|
// Phoenix's two-term map adds p·dz_{n-1} and carries the previous dz.
|
||||||
if (u.de_coloring != 0u) {
|
if u.de_coloring != 0u {
|
||||||
var dz_new = cmul(fprime(z), dz) + dz_seed;
|
var dz_new = cmul(fprime(z), dz) + dz_seed;
|
||||||
if (u.kind == KIND_PHOENIX) {
|
if u.kind == KIND_PHOENIX {
|
||||||
dz_new = dz_new + cmul(u.phoenix_p, dz_prev);
|
dz_new = dz_new + cmul(u.phoenix_p, dz_prev);
|
||||||
dz_prev = dz;
|
dz_prev = dz;
|
||||||
}
|
}
|
||||||
@@ -261,7 +272,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
// Phoenix additionally adds p·e_{n-1} and carries the previous delta.
|
// Phoenix additionally adds p·e_{n-1} and carries the previous delta.
|
||||||
let e_old = e;
|
let e_old = e;
|
||||||
e = advance_delta(xm, e) + step_add;
|
e = advance_delta(xm, e) + step_add;
|
||||||
if (u.kind == KIND_PHOENIX) {
|
if u.kind == KIND_PHOENIX {
|
||||||
e = e + cmul(u.phoenix_p, e_prev);
|
e = e + cmul(u.phoenix_p, e_prev);
|
||||||
e_prev = e_old;
|
e_prev = e_old;
|
||||||
}
|
}
|
||||||
@@ -269,7 +280,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
n = n + 1u;
|
n = n + 1u;
|
||||||
|
|
||||||
// Keep the reference index valid and the delta small.
|
// Keep the reference index valid and the delta small.
|
||||||
if (m >= u.ref_len) {
|
if m >= u.ref_len {
|
||||||
// Reference exhausted: any pixel that followed it this far has
|
// Reference exhausted: any pixel that followed it this far has
|
||||||
// effectively escaped (interior pixels rebase before reaching here).
|
// effectively escaped (interior pixels rebase before reaching here).
|
||||||
z = ref_orbit[u.ref_len - 1u] + e;
|
z = ref_orbit[u.ref_len - 1u] + e;
|
||||||
@@ -277,12 +288,12 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let y = ref_orbit[m] + e;
|
let y = ref_orbit[m] + e;
|
||||||
if (dot(y, y) < dot(e, e)) {
|
if dot(y, y) < dot(e, e) {
|
||||||
// Rebase to index 0: carry the full value as the new delta. Valid
|
// Rebase to index 0: carry the full value as the new delta. Valid
|
||||||
// because y_n = X[0] + (y_n - X[0]); for Mandelbrot X[0]=0.
|
// because y_n = X[0] + (y_n - X[0]); for Mandelbrot X[0]=0.
|
||||||
// Phoenix: after rebasing the implied previous reference is Y[-1]=0,
|
// Phoenix: after rebasing the implied previous reference is Y[-1]=0,
|
||||||
// so the previous delta becomes the full previous value y_n (= z).
|
// so the previous delta becomes the full previous value y_n (= z).
|
||||||
if (u.kind == KIND_PHOENIX) {
|
if u.kind == KIND_PHOENIX {
|
||||||
e_prev = z;
|
e_prev = z;
|
||||||
}
|
}
|
||||||
e = y - z0;
|
e = y - z0;
|
||||||
@@ -290,7 +301,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!escaped) {
|
if !escaped {
|
||||||
return Sample(0.0, 1.0, false); // interior of the set
|
return Sample(0.0, 1.0, false); // interior of the set
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +317,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
let ci = sqrt(max(smooth_i, 0.0));
|
let ci = sqrt(max(smooth_i, 0.0));
|
||||||
|
|
||||||
var de = 1.0;
|
var de = 1.0;
|
||||||
if (u.de_coloring != 0u) {
|
if u.de_coloring != 0u {
|
||||||
// Exterior distance estimate (complex-plane units): |z|·ln|z| / |dz|.
|
// Exterior distance estimate (complex-plane units): |z|·ln|z| / |dz|.
|
||||||
// Divided by the pixel footprint it becomes a distance in pixels; we
|
// Divided by the pixel footprint it becomes a distance in pixels; we
|
||||||
// darken toward the boundary (< ~1 px away) so filaments stay crisp
|
// darken toward the boundary (< ~1 px away) so filaments stay crisp
|
||||||
@@ -324,7 +335,7 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
// only color-dependent step, so it can be redone without re-iterating. Interior
|
// only color-dependent step, so it can be redone without re-iterating. Interior
|
||||||
// samples are black.
|
// samples are black.
|
||||||
fn color_sample(s: Sample) -> vec3<f32> {
|
fn color_sample(s: Sample) -> vec3<f32> {
|
||||||
if (!s.escaped) {
|
if !s.escaped {
|
||||||
return vec3<f32>(0.0, 0.0, 0.0);
|
return vec3<f32>(0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
let t = fract(s.ci * u.color_scale + u.color_offset);
|
let t = fract(s.ci * u.color_scale + u.color_offset);
|
||||||
@@ -353,7 +364,7 @@ fn fs_data(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
let jx = (f32(sx) + 0.5) * inv - 0.5;
|
let jx = (f32(sx) + 0.5) * inv - 0.5;
|
||||||
let jy = (f32(sy) + 0.5) * inv - 0.5;
|
let jy = (f32(sy) + 0.5) * inv - 0.5;
|
||||||
let s = iterate_sample(base + jx * dx + jy * dy, px);
|
let s = iterate_sample(base + jx * dx + jy * dy, px);
|
||||||
if (s.escaped) {
|
if s.escaped {
|
||||||
ci_sum = ci_sum + s.ci;
|
ci_sum = ci_sum + s.ci;
|
||||||
de_sum = de_sum + s.de;
|
de_sum = de_sum + s.de;
|
||||||
escaped_n = escaped_n + 1u;
|
escaped_n = escaped_n + 1u;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub struct RefRequest {
|
|||||||
pub power: u32,
|
pub power: u32,
|
||||||
/// Distortion constant for the Phoenix map (ignored by other kinds).
|
/// Distortion constant for the Phoenix map (ignored by other kinds).
|
||||||
pub phoenix_p: (f64, f64),
|
pub phoenix_p: (f64, f64),
|
||||||
|
/// Distortion constant for the Lambda map (ignored by other kinds).
|
||||||
|
pub lambda_l: (f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RefResult {
|
pub struct RefResult {
|
||||||
@@ -104,6 +106,7 @@ fn compute(req: &RefRequest) -> Vec<[f32; 2]> {
|
|||||||
req.kind,
|
req.kind,
|
||||||
req.power,
|
req.power,
|
||||||
req.phoenix_p,
|
req.phoenix_p,
|
||||||
|
req.lambda_l,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
compute_set_reference(
|
compute_set_reference(
|
||||||
@@ -114,6 +117,7 @@ fn compute(req: &RefRequest) -> Vec<[f32; 2]> {
|
|||||||
req.kind,
|
req.kind,
|
||||||
req.power,
|
req.power,
|
||||||
req.phoenix_p,
|
req.phoenix_p,
|
||||||
|
req.lambda_l,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user