Skip to main content

proof_engine/fractal/
flame.rs

1//! Flame fractals (Scott Draves algorithm) — nonlinear IFS with color mapping.
2
3use glam::Vec2;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum FlameVariation {
7    Linear, Sinusoidal, Spherical, Swirl, Horseshoe, Polar, Handkerchief,
8    Heart, Disc, Spiral, Hyperbolic, Diamond, Ex, Julia, Bent, Fisheye,
9}
10
11impl FlameVariation {
12    pub fn apply(&self, p: Vec2) -> Vec2 {
13        let (x, y) = (p.x, p.y);
14        let r = p.length().max(1e-10);
15        let theta = y.atan2(x);
16        match self {
17            Self::Linear => p,
18            Self::Sinusoidal => Vec2::new(x.sin(), y.sin()),
19            Self::Spherical => p / (r * r),
20            Self::Swirl => { let r2 = r * r; Vec2::new(x * r2.sin() - y * r2.cos(), x * r2.cos() + y * r2.sin()) }
21            Self::Horseshoe => Vec2::new((x - y) * (x + y) / r, 2.0 * x * y / r),
22            Self::Polar => Vec2::new(theta / std::f32::consts::PI, r - 1.0),
23            Self::Handkerchief => Vec2::new(r * (theta + r).sin(), r * (theta - r).cos()),
24            Self::Heart => Vec2::new(r * (theta * r).sin(), -r * (theta * r).cos()),
25            Self::Disc => { let tr = theta / std::f32::consts::PI; Vec2::new(tr * (std::f32::consts::PI * r).sin(), tr * (std::f32::consts::PI * r).cos()) }
26            Self::Spiral => Vec2::new((theta.cos() + r.sin()) / r, (theta.sin() - r.cos()) / r),
27            Self::Hyperbolic => Vec2::new(theta.sin() / r, r * theta.cos()),
28            Self::Diamond => Vec2::new(theta.sin() * r.cos(), theta.cos() * r.sin()),
29            Self::Ex => { let p0 = (theta + r).sin().powi(3); let p1 = (theta - r).cos().powi(3); Vec2::new(r * (p0 + p1), r * (p0 - p1)) }
30            Self::Julia => { let sr = r.sqrt(); let omega = if (theta * 1000.0) as i32 % 2 == 0 { 0.0 } else { std::f32::consts::PI }; Vec2::new(sr * (theta / 2.0 + omega).cos(), sr * (theta / 2.0 + omega).sin()) }
31            Self::Bent => Vec2::new(if x >= 0.0 { x } else { 2.0 * x }, if y >= 0.0 { y } else { y / 2.0 }),
32            Self::Fisheye => { let rr = 2.0 / (r + 1.0); Vec2::new(rr * y, rr * x) }
33        }
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct FlameParams {
39    pub variations: Vec<(FlameVariation, f32)>,
40    pub color_speed: f32,
41    pub iterations: u32,
42    pub supersample: u32,
43}
44
45impl Default for FlameParams {
46    fn default() -> Self {
47        Self { variations: vec![(FlameVariation::Linear, 0.5), (FlameVariation::Sinusoidal, 0.5)], color_speed: 0.5, iterations: 100000, supersample: 1 }
48    }
49}
50
51/// Render a flame fractal. Returns (x, y, color_index) tuples.
52pub fn render_flame(params: &FlameParams, seed: u64) -> Vec<(Vec2, f32)> {
53    let mut points = Vec::with_capacity(params.iterations as usize);
54    let mut p = Vec2::new(0.1, 0.1);
55    let mut color = 0.5f32;
56    let mut rng = seed;
57
58    for i in 0..params.iterations + 20 {
59        // Pick a variation weighted randomly
60        rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
61        let total_w: f32 = params.variations.iter().map(|(_, w)| w).sum();
62        let r = (rng >> 33) as f32 / (u32::MAX >> 1) as f32 * total_w;
63        let mut cum = 0.0;
64        let mut selected = &params.variations[0];
65        for v in &params.variations {
66            cum += v.1;
67            if r <= cum { selected = v; break; }
68        }
69
70        p = selected.0.apply(p);
71        color = color * (1.0 - params.color_speed) + params.color_speed * (selected.0 as u8 as f32 / 16.0);
72
73        if i >= 20 { points.push((p, color)); }
74    }
75    points
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    #[test]
82    fn flame_renders() {
83        let params = FlameParams { iterations: 1000, ..Default::default() };
84        let result = render_flame(&params, 42);
85        assert_eq!(result.len(), 1000);
86    }
87    #[test]
88    fn variations_dont_panic() {
89        let p = Vec2::new(0.5, 0.3);
90        for v in &[FlameVariation::Linear, FlameVariation::Spherical, FlameVariation::Swirl, FlameVariation::Heart, FlameVariation::Julia] {
91            let _r = v.apply(p);
92        }
93    }
94}