Skip to main content

proof_engine/fractal/
ifs.rs

1//! Iterated Function Systems (IFS) — Sierpinski, Barnsley fern, custom IFS.
2
3use glam::Vec2;
4
5/// 2D affine transformation: p' = A*p + b.
6#[derive(Debug, Clone)]
7pub struct AffineTx {
8    pub a: f32, pub b: f32, pub c: f32, pub d: f32, pub e: f32, pub f: f32,
9    pub probability: f32,
10    pub color_index: u8,
11}
12
13impl AffineTx {
14    pub fn apply(&self, p: Vec2) -> Vec2 {
15        Vec2::new(self.a * p.x + self.b * p.y + self.e, self.c * p.x + self.d * p.y + self.f)
16    }
17}
18
19/// An Iterated Function System.
20#[derive(Debug, Clone)]
21pub struct IfsSystem {
22    pub transforms: Vec<AffineTx>,
23    pub name: String,
24}
25
26/// Rendered IFS fractal.
27pub struct IfsFractal {
28    pub points: Vec<Vec2>,
29    pub colors: Vec<u8>,
30}
31
32impl IfsSystem {
33    /// Run the chaos game for `iterations` steps.
34    pub fn render(&self, iterations: u32, seed: u64) -> IfsFractal {
35        let mut points = Vec::with_capacity(iterations as usize);
36        let mut colors = Vec::with_capacity(iterations as usize);
37        let mut p = Vec2::ZERO;
38        let mut rng = seed;
39        let total_prob: f32 = self.transforms.iter().map(|t| t.probability).sum();
40
41        for _ in 0..iterations {
42            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
43            let r = (rng >> 33) as f32 / (u32::MAX >> 1) as f32 * total_prob;
44            let mut cumulative = 0.0;
45            let mut tx_idx = 0;
46            for (i, tx) in self.transforms.iter().enumerate() {
47                cumulative += tx.probability;
48                if r <= cumulative { tx_idx = i; break; }
49            }
50            p = self.transforms[tx_idx].apply(p);
51            points.push(p);
52            colors.push(self.transforms[tx_idx].color_index);
53        }
54        IfsFractal { points, colors }
55    }
56
57    /// Barnsley fern preset.
58    pub fn barnsley_fern() -> Self {
59        Self {
60            name: "Barnsley Fern".to_string(),
61            transforms: vec![
62                AffineTx { a: 0.0, b: 0.0, c: 0.0, d: 0.16, e: 0.0, f: 0.0, probability: 0.01, color_index: 0 },
63                AffineTx { a: 0.85, b: 0.04, c: -0.04, d: 0.85, e: 0.0, f: 1.6, probability: 0.85, color_index: 1 },
64                AffineTx { a: 0.2, b: -0.26, c: 0.23, d: 0.22, e: 0.0, f: 1.6, probability: 0.07, color_index: 2 },
65                AffineTx { a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0.0, f: 0.44, probability: 0.07, color_index: 3 },
66            ],
67        }
68    }
69
70    /// Sierpinski triangle preset.
71    pub fn sierpinski() -> Self {
72        Self {
73            name: "Sierpinski Triangle".to_string(),
74            transforms: vec![
75                AffineTx { a: 0.5, b: 0.0, c: 0.0, d: 0.5, e: 0.0, f: 0.0, probability: 1.0, color_index: 0 },
76                AffineTx { a: 0.5, b: 0.0, c: 0.0, d: 0.5, e: 0.5, f: 0.0, probability: 1.0, color_index: 1 },
77                AffineTx { a: 0.5, b: 0.0, c: 0.0, d: 0.5, e: 0.25, f: 0.5, probability: 1.0, color_index: 2 },
78            ],
79        }
80    }
81
82    /// Sierpinski carpet.
83    pub fn sierpinski_carpet() -> Self {
84        let mut transforms = Vec::new();
85        for i in 0..3 { for j in 0..3 {
86            if i == 1 && j == 1 { continue; }
87            transforms.push(AffineTx {
88                a: 1.0/3.0, b: 0.0, c: 0.0, d: 1.0/3.0,
89                e: i as f32 / 3.0, f: j as f32 / 3.0,
90                probability: 1.0, color_index: (i * 3 + j) as u8,
91            });
92        }}
93        Self { name: "Sierpinski Carpet".to_string(), transforms }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    #[test]
101    fn barnsley_fern_renders() {
102        let ifs = IfsSystem::barnsley_fern();
103        let result = ifs.render(10000, 42);
104        assert_eq!(result.points.len(), 10000);
105        // Fern should be roughly bounded
106        let max_y = result.points.iter().map(|p| p.y).fold(f32::MIN, f32::max);
107        assert!(max_y > 5.0 && max_y < 15.0, "Fern y range: {max_y}");
108    }
109    #[test]
110    fn sierpinski_bounded() {
111        let ifs = IfsSystem::sierpinski();
112        let result = ifs.render(5000, 123);
113        for p in &result.points {
114            assert!(p.x >= -0.1 && p.x <= 1.1 && p.y >= -0.1 && p.y <= 1.1);
115        }
116    }
117}