1use noise::{NoiseFn, Perlin};
27use rand::Rng;
28use sdl3::pixels::Color;
29use sdl3::render::{Canvas, FPoint};
30use sdl3::video::Window;
31
32#[derive(Debug, Clone, Copy)]
34pub struct FireParams {
35 pub flame_height: f32,
37 pub intensity: f32,
39 pub flicker_speed: f32,
41 pub particle_density: f32,
43 pub flame_width: f32,
45}
46
47impl Default for FireParams {
48 fn default() -> Self {
49 Self {
50 flame_height: 50.0,
51 intensity: 1.0,
52 flicker_speed: 2.0,
53 particle_density: 0.5,
54 flame_width: 20.0,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy)]
61pub struct FireLine {
62 pub start: FPoint,
63 pub end: FPoint,
64 pub params: FireParams,
65}
66
67impl FireLine {
68 pub fn new(x1: f32, y1: f32, x2: f32, y2: f32, params: FireParams) -> Self {
70 Self {
71 start: FPoint::new(x1, y1),
72 end: FPoint::new(x2, y2),
73 params,
74 }
75 }
76
77 pub fn length(&self) -> f32 {
79 let dx = self.end.x - self.start.x;
80 let dy = self.end.y - self.start.y;
81 (dx * dx + dy * dy).sqrt()
82 }
83}
84
85pub struct FireRenderer {
87 canvas: Canvas<Window>,
88 noise: Perlin,
89}
90
91impl FireRenderer {
92 pub fn new(canvas: Canvas<Window>) -> Self {
94 Self {
95 canvas,
96 noise: Perlin::new(rand::rng().random()),
97 }
98 }
99
100 pub fn canvas_mut(&mut self) -> &mut Canvas<Window> {
102 &mut self.canvas
103 }
104
105 pub fn present(&mut self) {
107 self.canvas.present();
108 }
109
110 pub fn render(&mut self, line: &FireLine, time: f32) -> Result<(), String> {
112 let length = line.length();
113 let num_samples = (length * line.params.particle_density) as usize;
114
115 if num_samples == 0 {
116 return Ok(());
117 }
118
119 for i in 0..num_samples {
120 let t = i as f32 / num_samples as f32;
121
122 let x = line.start.x + t * (line.end.x - line.start.x);
124 let base_y = line.start.y + t * (line.end.y - line.start.y);
125
126 let particles_per_sample = (line.params.flame_height * 0.5) as usize;
128
129 for p in 0..particles_per_sample {
130 let y_offset = p as f32;
131 let y = base_y - y_offset;
132
133 let x_offset = (rand::rng().random::<f32>() - 0.5) * line.params.flame_width;
135 let px = x + x_offset;
136
137 let noise_val = self.noise.get([
139 px as f64 * 0.05,
140 y as f64 * 0.05,
141 (time * line.params.flicker_speed) as f64,
142 ]);
143
144 let noise_normalized = ((noise_val + 1.0) * 0.5).clamp(0.0, 1.0) as f32;
146
147 let height_factor = 1.0 - (y_offset / line.params.flame_height);
149
150 let intensity = noise_normalized * height_factor * line.params.intensity;
152
153 if intensity < 0.1 {
155 continue;
156 }
157
158 let color = fire_color(intensity);
160
161 self.canvas.set_draw_color(color);
162 self.canvas.draw_point(FPoint::new(px, y)).map_err(|e| e.to_string())?;
163 }
164 }
165
166 Ok(())
167 }
168}
169
170fn fire_color(intensity: f32) -> Color {
173 let intensity = intensity.clamp(0.0, 1.0);
174
175 if intensity < 0.25 {
176 let t = intensity / 0.25;
178 Color::RGB((t * 139.0) as u8, 0, 0)
179 } else if intensity < 0.5 {
180 let t = (intensity - 0.25) / 0.25;
182 Color::RGB((139.0 + t * 116.0) as u8, 0, 0)
183 } else if intensity < 0.75 {
184 let t = (intensity - 0.5) / 0.25;
186 Color::RGB(255, (t * 165.0) as u8, 0)
187 } else if intensity < 0.9 {
188 let t = (intensity - 0.75) / 0.15;
190 Color::RGB(255, (165.0 + t * 90.0) as u8, (t * 255.0) as u8)
191 } else {
192 let t = (intensity - 0.9) / 0.1;
194 Color::RGB(255, 255, (255.0 + t * 0.0) as u8)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_fire_line_creation() {
204 let line = FireLine::new(0.0, 0.0, 100.0, 0.0, FireParams::default());
205 assert_eq!(line.start.x(), 0.0);
206 assert_eq!(line.start.y(), 0.0);
207 assert_eq!(line.end.x(), 100.0);
208 assert_eq!(line.end.y(), 0.0);
209 }
210
211 #[test]
212 fn test_fire_line_length() {
213 let line = FireLine::new(0.0, 0.0, 3.0, 4.0, FireParams::default());
214 assert!((line.length() - 5.0).abs() < 0.001);
215 }
216
217 #[test]
218 fn test_fire_color_gradient() {
219 let black = fire_color(0.0);
220 let red = fire_color(0.5);
221 let orange = fire_color(0.7);
222 let yellow = fire_color(0.85);
223 let white = fire_color(1.0);
224
225 assert!(black.r < red.r);
227 assert!(red.g < orange.g);
228 assert!(orange.b < yellow.b);
229 }
230}