1use std::fmt;
10use glam::{Vec2, Vec3, Vec4};
11
12#[derive(Clone, Copy, PartialEq, Debug, Default)]
16pub struct Complex {
17 pub re: f64,
18 pub im: f64,
19}
20
21impl Complex {
22 pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
23 pub const ONE: Self = Self { re: 1.0, im: 0.0 };
24 pub const I: Self = Self { re: 0.0, im: 1.0 };
25
26 #[inline] pub fn new(re: f64, im: f64) -> Self { Self { re, im } }
27 #[inline] pub fn from_polar(r: f64, theta: f64) -> Self {
28 Self { re: r * theta.cos(), im: r * theta.sin() }
29 }
30 #[inline] pub fn from_vec2(v: Vec2) -> Self { Self { re: v.x as f64, im: v.y as f64 } }
31 #[inline] pub fn to_vec2(self) -> Vec2 { Vec2::new(self.re as f32, self.im as f32) }
32
33 #[inline] pub fn conj(self) -> Self { Self::new(self.re, -self.im) }
36 #[inline] pub fn norm_sq(self) -> f64 { self.re * self.re + self.im * self.im }
37 #[inline] pub fn norm(self) -> f64 { self.norm_sq().sqrt() }
38 #[inline] pub fn arg(self) -> f64 { self.im.atan2(self.re) }
39 #[inline] pub fn abs(self) -> f64 { self.norm() }
40 #[inline] pub fn is_zero(self) -> bool { self.re == 0.0 && self.im == 0.0 }
41
42 pub fn normalize(self) -> Self {
44 let n = self.norm();
45 if n < 1e-300 { Self::ZERO } else { Self::new(self.re / n, self.im / n) }
46 }
47
48 pub fn add(self, rhs: Self) -> Self {
51 Self::new(self.re + rhs.re, self.im + rhs.im)
52 }
53 pub fn sub(self, rhs: Self) -> Self {
54 Self::new(self.re - rhs.re, self.im - rhs.im)
55 }
56 pub fn mul(self, rhs: Self) -> Self {
57 Self::new(
58 self.re * rhs.re - self.im * rhs.im,
59 self.re * rhs.im + self.im * rhs.re,
60 )
61 }
62 pub fn div(self, rhs: Self) -> Self {
63 let d = rhs.norm_sq();
64 Self::new(
65 (self.re * rhs.re + self.im * rhs.im) / d,
66 (self.im * rhs.re - self.re * rhs.im) / d,
67 )
68 }
69 pub fn scale(self, s: f64) -> Self { Self::new(self.re * s, self.im * s) }
70 pub fn neg(self) -> Self { Self::new(-self.re, -self.im) }
71 pub fn recip(self) -> Self { Self::ONE.div(self) }
72
73 pub fn exp(self) -> Self {
76 let e = self.re.exp();
77 Self::new(e * self.im.cos(), e * self.im.sin())
78 }
79
80 pub fn ln(self) -> Self {
81 Self::new(self.norm().ln(), self.arg())
82 }
83
84 pub fn sqrt(self) -> Self {
85 let r = self.norm().sqrt();
86 let phi = self.arg() * 0.5;
87 Self::from_polar(r, phi)
88 }
89
90 pub fn pow(self, w: Self) -> Self {
92 if self.is_zero() { return Self::ZERO; }
93 self.ln().mul(w).exp()
94 }
95
96 pub fn powi(self, n: i32) -> Self {
98 if n == 0 { return Self::ONE; }
99 if n < 0 { return self.recip().powi(-n); }
100 let mut result = Self::ONE;
101 let mut base = self;
102 let mut exp = n as u32;
103 while exp > 0 {
104 if exp & 1 == 1 { result = result.mul(base); }
105 base = base.mul(base);
106 exp >>= 1;
107 }
108 result
109 }
110
111 pub fn sin(self) -> Self {
112 Self::new(
114 self.re.sin() * self.im.cosh(),
115 self.re.cos() * self.im.sinh(),
116 )
117 }
118
119 pub fn cos(self) -> Self {
120 Self::new(
122 self.re.cos() * self.im.cosh(),
123 -self.re.sin() * self.im.sinh(),
124 )
125 }
126
127 pub fn tan(self) -> Self {
128 self.sin().div(self.cos())
129 }
130
131 pub fn sinh(self) -> Self {
132 Self::new(self.re.sinh() * self.im.cos(), self.re.cosh() * self.im.sin())
133 }
134
135 pub fn cosh(self) -> Self {
136 Self::new(self.re.cosh() * self.im.cos(), self.re.sinh() * self.im.sin())
137 }
138
139 pub fn tanh(self) -> Self {
140 self.sinh().div(self.cosh())
141 }
142
143 pub fn asin(self) -> Self {
144 let iz = Self::I.mul(self);
146 let sq = Self::ONE.sub(self.mul(self)).sqrt();
147 Self::I.neg().mul(iz.add(sq).ln())
148 }
149
150 pub fn acos(self) -> Self {
151 let sq = Self::ONE.sub(self.mul(self)).sqrt();
153 Self::I.neg().mul(self.add(Self::I.mul(sq)).ln())
154 }
155
156 pub fn atan(self) -> Self {
157 let half_i = Self::new(0.0, 0.5);
159 let iz_plus = Self::I.add(self);
160 let iz_minus = Self::I.sub(self);
161 half_i.mul(iz_plus.div(iz_minus).ln())
162 }
163
164 #[inline] pub fn mandelbrot_step(self, c: Self) -> Self {
168 self.mul(self).add(c)
169 }
170
171 #[inline] pub fn burning_ship_step(self, c: Self) -> Self {
173 let abs_z = Self::new(self.re.abs(), self.im.abs());
174 abs_z.mul(abs_z).add(c)
175 }
176
177 #[inline] pub fn tricorn_step(self, c: Self) -> Self {
179 self.conj().mul(self.conj()).add(c)
180 }
181}
182
183impl fmt::Display for Complex {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 if self.im >= 0.0 {
186 write!(f, "{:.4}+{:.4}i", self.re, self.im)
187 } else {
188 write!(f, "{:.4}{:.4}i", self.re, self.im)
189 }
190 }
191}
192
193impl std::ops::Add for Complex {
194 type Output = Self;
195 fn add(self, rhs: Self) -> Self { Complex::add(self, rhs) }
196}
197impl std::ops::Sub for Complex {
198 type Output = Self;
199 fn sub(self, rhs: Self) -> Self { Complex::sub(self, rhs) }
200}
201impl std::ops::Mul for Complex {
202 type Output = Self;
203 fn mul(self, rhs: Self) -> Self { Complex::mul(self, rhs) }
204}
205impl std::ops::Div for Complex {
206 type Output = Self;
207 fn div(self, rhs: Self) -> Self { Complex::div(self, rhs) }
208}
209impl std::ops::Neg for Complex {
210 type Output = Self;
211 fn neg(self) -> Self { Complex::neg(self) }
212}
213
214#[derive(Clone, Copy, Debug)]
218pub struct EscapeResult {
219 pub escaped: bool,
221 pub iterations: u32,
223 pub smooth: f64,
225 pub final_norm: f64,
227 pub final_z: Complex,
229}
230
231impl EscapeResult {
232 pub fn normalized(&self, max_iter: u32) -> f64 {
234 self.smooth / max_iter as f64
235 }
236
237 pub fn orbit_angle(&self) -> f64 {
239 self.final_z.arg()
240 }
241}
242
243#[derive(Clone, Debug)]
247pub struct MandelbrotParams {
248 pub power: f64,
250 pub max_iter: u32,
252 pub escape_r_sq: f64,
254}
255
256impl Default for MandelbrotParams {
257 fn default() -> Self {
258 Self { power: 2.0, max_iter: 256, escape_r_sq: 4.0 }
259 }
260}
261
262impl MandelbrotParams {
263 pub fn new(power: f64, max_iter: u32) -> Self {
264 Self { power, max_iter, escape_r_sq: 4.0 }
265 }
266
267 pub fn sample(&self, c: Complex) -> EscapeResult {
268 let mut z = Complex::ZERO;
269 let pw = Complex::new(self.power, 0.0);
270 for i in 0..self.max_iter {
271 z = z.pow(pw).add(c);
272 let n = z.norm_sq();
273 if n > self.escape_r_sq {
274 let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
276 return EscapeResult {
277 escaped: true, iterations: i,
278 smooth, final_norm: n.sqrt(), final_z: z,
279 };
280 }
281 }
282 EscapeResult {
283 escaped: false, iterations: self.max_iter,
284 smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
285 }
286 }
287}
288
289#[derive(Clone, Debug)]
293pub struct JuliaParams {
294 pub c: Complex,
296 pub power: f64,
297 pub max_iter: u32,
298 pub escape_r_sq: f64,
299}
300
301impl JuliaParams {
302 pub fn new(c: Complex, max_iter: u32) -> Self {
303 Self { c, power: 2.0, max_iter, escape_r_sq: 4.0 }
304 }
305
306 pub fn preset(idx: usize) -> Self {
308 let presets = [
309 Complex::new(-0.7269, 0.1889), Complex::new(-0.4, 0.6), Complex::new(0.285, 0.01), Complex::new(-0.835, -0.2321), Complex::new(-0.7, 0.27015), Complex::new(0.45, 0.1428), Complex::new(-0.123, 0.745), Complex::new(0.0, 0.8), ];
318 let c = presets[idx % presets.len()];
319 Self { c, power: 2.0, max_iter: 256, escape_r_sq: 4.0 }
320 }
321
322 pub fn sample(&self, z0: Complex) -> EscapeResult {
323 let mut z = z0;
324 let pw = Complex::new(self.power, 0.0);
325 for i in 0..self.max_iter {
326 z = z.pow(pw).add(self.c);
327 let n = z.norm_sq();
328 if n > self.escape_r_sq {
329 let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
330 return EscapeResult {
331 escaped: true, iterations: i,
332 smooth, final_norm: n.sqrt(), final_z: z,
333 };
334 }
335 }
336 EscapeResult {
337 escaped: false, iterations: self.max_iter,
338 smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
339 }
340 }
341}
342
343pub struct BurningShip {
346 pub max_iter: u32,
347 pub escape_r_sq: f64,
348}
349
350impl Default for BurningShip {
351 fn default() -> Self { Self { max_iter: 256, escape_r_sq: 4.0 } }
352}
353
354impl BurningShip {
355 pub fn sample(&self, c: Complex) -> EscapeResult {
356 let mut z = Complex::ZERO;
357 for i in 0..self.max_iter {
358 z = z.burning_ship_step(c);
359 let n = z.norm_sq();
360 if n > self.escape_r_sq {
361 let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
362 return EscapeResult {
363 escaped: true, iterations: i,
364 smooth, final_norm: n.sqrt(), final_z: z,
365 };
366 }
367 }
368 EscapeResult {
369 escaped: false, iterations: self.max_iter,
370 smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
371 }
372 }
373}
374
375pub struct NewtonFractal {
380 pub coeffs: Vec<Complex>,
382 pub max_iter: u32,
383 pub tol: f64,
384 pub relax: Complex, }
386
387impl NewtonFractal {
388 pub fn cubic_unity() -> Self {
390 Self {
391 coeffs: vec![
392 Complex::new(-1.0, 0.0),
393 Complex::ZERO,
394 Complex::ZERO,
395 Complex::ONE,
396 ],
397 max_iter: 50,
398 tol: 1e-6,
399 relax: Complex::ONE,
400 }
401 }
402
403 pub fn quartic_unity() -> Self {
405 Self {
406 coeffs: vec![
407 Complex::new(-1.0, 0.0),
408 Complex::ZERO, Complex::ZERO, Complex::ZERO,
409 Complex::ONE,
410 ],
411 max_iter: 50,
412 tol: 1e-6,
413 relax: Complex::ONE,
414 }
415 }
416
417 fn eval(&self, z: Complex) -> Complex {
419 let mut result = Complex::ZERO;
420 let mut zpow = Complex::ONE;
421 for &c in &self.coeffs {
422 result = result.add(c.mul(zpow));
423 zpow = zpow.mul(z);
424 }
425 result
426 }
427
428 fn eval_deriv(&self, z: Complex) -> Complex {
430 let mut result = Complex::ZERO;
431 let mut zpow = Complex::ONE;
432 for (i, &c) in self.coeffs.iter().enumerate().skip(1) {
433 let coeff = c.scale(i as f64);
434 result = result.add(coeff.mul(zpow));
435 zpow = zpow.mul(z);
436 }
437 result
438 }
439
440 pub fn sample(&self, z0: Complex) -> (Option<usize>, u32, Complex) {
442 let mut z = z0;
445 for i in 0..self.max_iter {
446 let fz = self.eval(z);
447 let dfz = self.eval_deriv(z);
448 if dfz.norm_sq() < 1e-300 { break; }
449 let step = fz.div(dfz);
450 z = z.sub(self.relax.mul(step));
451 if step.norm() < self.tol {
452 return (None, i, z); }
454 }
455 (None, self.max_iter, z)
456 }
457}
458
459pub struct LyapunovFractal {
464 pub sequence: Vec<bool>, pub warmup: u32,
466 pub iterations: u32,
467}
468
469impl Default for LyapunovFractal {
470 fn default() -> Self {
471 Self { sequence: vec![false, true], warmup: 200, iterations: 1000 }
473 }
474}
475
476impl LyapunovFractal {
477 pub fn new(pattern: &str) -> Self {
478 let sequence = pattern.chars()
479 .filter(|c| *c == 'A' || *c == 'B')
480 .map(|c| c == 'B')
481 .collect();
482 Self { sequence, warmup: 200, iterations: 1000 }
483 }
484
485 pub fn sample(&self, a: f64, b: f64) -> f64 {
488 if self.sequence.is_empty() { return 0.0; }
489 let mut x = 0.5_f64;
490 let seq_len = self.sequence.len();
491
492 for i in 0..(self.warmup as usize) {
494 let r = if self.sequence[i % seq_len] { b } else { a };
495 x = r * x * (1.0 - x);
496 }
497
498 let mut lyap = 0.0_f64;
500 for i in 0..(self.iterations as usize) {
501 let r = if self.sequence[i % seq_len] { b } else { a };
502 x = r * x * (1.0 - x);
503 let d = (r * (1.0 - 2.0 * x)).abs();
504 lyap += d.max(1e-300).ln();
505 }
506 lyap / self.iterations as f64
507 }
508}
509
510#[derive(Clone, Copy, Debug)]
514pub enum FractalPalette {
515 Grayscale,
517 Smooth { offset: f32, freq: f32 },
519 OrbitTrap,
521 AngleBased,
523 UltraFractal,
525 Fire,
527 Electric,
529}
530
531impl FractalPalette {
532 pub fn color(&self, result: &EscapeResult, max_iter: u32) -> Vec4 {
534 if !result.escaped {
535 return Vec4::ZERO; }
537 let t = (result.smooth / max_iter as f64).clamp(0.0, 1.0) as f32;
538
539 match self {
540 FractalPalette::Grayscale => {
541 let v = t;
542 Vec4::new(v, v, v, 1.0)
543 }
544 FractalPalette::Smooth { offset, freq } => {
545 let r = (std::f32::consts::TAU * (t * freq + offset + 0.0)).cos() * 0.5 + 0.5;
546 let g = (std::f32::consts::TAU * (t * freq + offset + 0.333)).cos() * 0.5 + 0.5;
547 let b = (std::f32::consts::TAU * (t * freq + offset + 0.667)).cos() * 0.5 + 0.5;
548 Vec4::new(r, g, b, 1.0)
549 }
550 FractalPalette::OrbitTrap => {
551 let n = result.final_norm as f32;
553 let v = 1.0 - (n / 2.0).min(1.0);
554 Vec4::new(v * 0.9, v * 0.4, v * 1.0, 1.0)
555 }
556 FractalPalette::AngleBased => {
557 let angle = result.final_z.arg() as f32;
558 let hue = (angle / std::f32::consts::TAU + 0.5).fract();
559 Self::hsv_to_rgb(hue, 0.8 + t * 0.2, 0.7 + t * 0.3)
560 }
561 FractalPalette::UltraFractal => {
562 let t4 = (t * 4.0).fract();
564 let stop = (t * 4.0) as u32 % 4;
565 let (a, b) = match stop {
566 0 => (Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.8, 0.8)),
567 1 => (Vec3::new(0.0, 0.8, 0.8), Vec3::new(1.0, 0.85, 0.0)),
568 2 => (Vec3::new(1.0, 0.85, 0.0), Vec3::new(1.0, 1.0, 1.0)),
569 _ => (Vec3::new(1.0, 1.0, 1.0), Vec3::new(0.0, 0.0, 0.0)),
570 };
571 let c = a.lerp(b, t4);
572 Vec4::new(c.x, c.y, c.z, 1.0)
573 }
574 FractalPalette::Fire => {
575 if t < 0.25 {
576 let tt = t / 0.25;
577 Vec4::new(tt, 0.0, 0.0, 1.0)
578 } else if t < 0.5 {
579 let tt = (t - 0.25) / 0.25;
580 Vec4::new(1.0, tt * 0.5, 0.0, 1.0)
581 } else if t < 0.75 {
582 let tt = (t - 0.5) / 0.25;
583 Vec4::new(1.0, 0.5 + tt * 0.5, 0.0, 1.0)
584 } else {
585 let tt = (t - 0.75) / 0.25;
586 Vec4::new(1.0, 1.0, tt, 1.0)
587 }
588 }
589 FractalPalette::Electric => {
590 if t < 0.33 {
591 let tt = t / 0.33;
592 Vec4::new(0.0, 0.0, tt * 0.6, 1.0)
593 } else if t < 0.66 {
594 let tt = (t - 0.33) / 0.33;
595 Vec4::new(0.0, tt, 0.6 + tt * 0.4, 1.0)
596 } else {
597 let tt = (t - 0.66) / 0.34;
598 Vec4::new(tt, 1.0, 1.0, 1.0)
599 }
600 }
601 }
602 }
603
604 fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Vec4 {
605 let i = (h * 6.0) as u32;
606 let f = h * 6.0 - i as f32;
607 let p = v * (1.0 - s);
608 let q = v * (1.0 - f * s);
609 let t = v * (1.0 - (1.0 - f) * s);
610 let (r, g, b) = match i % 6 {
611 0 => (v, t, p),
612 1 => (q, v, p),
613 2 => (p, v, t),
614 3 => (p, q, v),
615 4 => (t, p, v),
616 _ => (v, p, q),
617 };
618 Vec4::new(r, g, b, 1.0)
619 }
620}
621
622#[derive(Clone, Copy, Debug, PartialEq)]
626pub struct Quaternion {
627 pub w: f32,
628 pub x: f32,
629 pub y: f32,
630 pub z: f32,
631}
632
633impl Quaternion {
634 pub const IDENTITY: Self = Self { w: 1.0, x: 0.0, y: 0.0, z: 0.0 };
635
636 pub fn new(w: f32, x: f32, y: f32, z: f32) -> Self { Self { w, x, y, z } }
637
638 pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
640 let half = angle * 0.5;
641 let s = half.sin();
642 Self::new(half.cos(), axis.x * s, axis.y * s, axis.z * s)
643 }
644
645 pub fn from_euler(roll: f32, pitch: f32, yaw: f32) -> Self {
647 let cr = (roll * 0.5).cos(); let sr = (roll * 0.5).sin();
648 let cp = (pitch * 0.5).cos(); let sp = (pitch * 0.5).sin();
649 let cy = (yaw * 0.5).cos(); let sy = (yaw * 0.5).sin();
650 Self::new(
651 cr * cp * cy + sr * sp * sy,
652 sr * cp * cy - cr * sp * sy,
653 cr * sp * cy + sr * cp * sy,
654 cr * cp * sy - sr * sp * cy,
655 )
656 }
657
658 pub fn norm(self) -> f32 {
659 (self.w*self.w + self.x*self.x + self.y*self.y + self.z*self.z).sqrt()
660 }
661
662 pub fn normalize(self) -> Self {
663 let n = self.norm();
664 Self::new(self.w/n, self.x/n, self.y/n, self.z/n)
665 }
666
667 pub fn conjugate(self) -> Self {
668 Self::new(self.w, -self.x, -self.y, -self.z)
669 }
670
671 pub fn inverse(self) -> Self {
672 let n2 = self.w*self.w + self.x*self.x + self.y*self.y + self.z*self.z;
673 let c = self.conjugate();
674 Self::new(c.w/n2, c.x/n2, c.y/n2, c.z/n2)
675 }
676
677 pub fn dot(self, rhs: Self) -> f32 {
678 self.w*rhs.w + self.x*rhs.x + self.y*rhs.y + self.z*rhs.z
679 }
680
681 pub fn mul(self, rhs: Self) -> Self {
683 Self::new(
684 self.w*rhs.w - self.x*rhs.x - self.y*rhs.y - self.z*rhs.z,
685 self.w*rhs.x + self.x*rhs.w + self.y*rhs.z - self.z*rhs.y,
686 self.w*rhs.y - self.x*rhs.z + self.y*rhs.w + self.z*rhs.x,
687 self.w*rhs.z + self.x*rhs.y - self.y*rhs.x + self.z*rhs.w,
688 )
689 }
690
691 pub fn rotate(self, v: Vec3) -> Vec3 {
693 let q = self;
694 let qv = Vec3::new(q.x, q.y, q.z);
695 let uv = qv.cross(v);
696 let uuv = qv.cross(uv);
697 v + (uv * q.w + uuv) * 2.0
698 }
699
700 pub fn slerp(self, other: Self, t: f32) -> Self {
702 let mut d = self.dot(other);
703 let other = if d < 0.0 {
704 d = -d;
705 Self::new(-other.w, -other.x, -other.y, -other.z)
706 } else {
707 other
708 };
709
710 if d > 0.9995 {
711 return Self::new(
713 self.w + t*(other.w - self.w),
714 self.x + t*(other.x - self.x),
715 self.y + t*(other.y - self.y),
716 self.z + t*(other.z - self.z),
717 ).normalize();
718 }
719
720 let theta_0 = d.acos();
721 let theta = theta_0 * t;
722 let sin_t0 = theta_0.sin();
723 let sin_t = theta.sin();
724 let s1 = (theta_0 - theta).sin() / sin_t0;
725 let s2 = sin_t / sin_t0;
726
727 Self::new(
728 s1 * self.w + s2 * other.w,
729 s1 * self.x + s2 * other.x,
730 s1 * self.y + s2 * other.y,
731 s1 * self.z + s2 * other.z,
732 )
733 }
734
735 pub fn angle(self) -> f32 {
737 2.0 * self.w.clamp(-1.0, 1.0).acos()
738 }
739
740 pub fn axis(self) -> Vec3 {
742 let s_sq = 1.0 - self.w * self.w;
743 if s_sq < 1e-10 { return Vec3::Y; }
744 let inv_s = 1.0 / s_sq.sqrt();
745 Vec3::new(self.x * inv_s, self.y * inv_s, self.z * inv_s)
746 }
747
748 pub fn exp_map(v: Vec3) -> Self {
750 let theta = v.length();
751 if theta < 1e-8 { return Self::IDENTITY; }
752 let half = theta * 0.5;
753 let s = half.sin() / theta;
754 Self::new(half.cos(), v.x * s, v.y * s, v.z * s)
755 }
756
757 pub fn log_map(self) -> Vec3 {
759 let v_norm = Vec3::new(self.x, self.y, self.z).length();
760 if v_norm < 1e-8 { return Vec3::ZERO; }
761 let theta = v_norm.atan2(self.w);
762 Vec3::new(self.x, self.y, self.z) * (theta / v_norm)
763 }
764
765 pub fn to_mat4(self) -> glam::Mat4 {
767 glam::Mat4::from_quat(glam::Quat::from_xyzw(self.x, self.y, self.z, self.w))
768 }
769}
770
771impl Default for Quaternion {
772 fn default() -> Self { Self::IDENTITY }
773}
774
775impl std::ops::Mul for Quaternion {
776 type Output = Self;
777 fn mul(self, rhs: Self) -> Self { Quaternion::mul(self, rhs) }
778}
779
780#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[test]
787 fn complex_mul() {
788 let a = Complex::new(1.0, 2.0);
789 let b = Complex::new(3.0, 4.0);
790 let c = a * b;
791 assert!((c.re - (-5.0)).abs() < 1e-10);
792 assert!((c.im - 10.0).abs() < 1e-10);
793 }
794
795 #[test]
796 fn complex_euler_identity() {
797 let z = Complex::new(0.0, std::f64::consts::PI).exp();
799 assert!((z.re + 1.0).abs() < 1e-10);
800 assert!(z.im.abs() < 1e-10);
801 }
802
803 #[test]
804 fn complex_sqrt() {
805 let z = Complex::new(-1.0, 0.0).sqrt();
806 assert!(z.re.abs() < 1e-10);
807 assert!((z.im - 1.0).abs() < 1e-10);
808 }
809
810 #[test]
811 fn mandelbrot_origin_escapes_not() {
812 let params = MandelbrotParams::default();
813 let result = params.sample(Complex::ZERO);
814 assert!(!result.escaped);
815 }
816
817 #[test]
818 fn mandelbrot_far_escapes() {
819 let params = MandelbrotParams::default();
820 let result = params.sample(Complex::new(3.0, 3.0));
821 assert!(result.escaped);
822 assert!(result.iterations < 5);
823 }
824
825 #[test]
826 fn julia_samples() {
827 let j = JuliaParams::preset(0);
828 let r = j.sample(Complex::new(0.0, 0.0));
829 let _ = r;
832 }
833
834 #[test]
835 fn quat_identity_rotates_nothing() {
836 let q = Quaternion::IDENTITY;
837 let v = Vec3::new(1.0, 2.0, 3.0);
838 let w = q.rotate(v);
839 assert!((w - v).length() < 1e-5);
840 }
841
842 #[test]
843 fn quat_axis_angle_roundtrip() {
844 let axis = Vec3::new(0.0, 1.0, 0.0);
845 let angle = std::f32::consts::FRAC_PI_4;
846 let q = Quaternion::from_axis_angle(axis, angle);
847 let v = Vec3::new(1.0, 0.0, 0.0);
848 let w = q.rotate(v);
849 assert!((w.x - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
851 assert!((w.z + std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
852 }
853
854 #[test]
855 fn quat_slerp_halfway() {
856 let a = Quaternion::IDENTITY;
857 let b = Quaternion::from_axis_angle(Vec3::Y, std::f32::consts::PI);
858 let m = a.slerp(b, 0.5);
859 let expected_angle = std::f32::consts::FRAC_PI_2;
860 assert!((m.angle() - expected_angle).abs() < 1e-4);
861 }
862
863 #[test]
864 fn lyapunov_stable() {
865 let l = LyapunovFractal::default();
866 let e = l.sample(2.0, 2.0);
868 assert!(e < 0.0, "Expected negative Lyapunov exponent, got {e}");
869 }
870
871 #[test]
872 fn lyapunov_chaotic() {
873 let l = LyapunovFractal::default();
874 let e = l.sample(3.9, 3.9);
876 assert!(e > 0.0, "Expected positive Lyapunov exponent at r=3.9, got {e}");
877 }
878
879 #[test]
880 fn palette_maps_escaped() {
881 let result = EscapeResult {
882 escaped: true, iterations: 50, smooth: 51.3,
883 final_norm: 2.1, final_z: Complex::new(2.0, 0.5),
884 };
885 let color = FractalPalette::Fire.color(&result, 256);
886 assert!(color.w > 0.0);
887 }
888}