1use glam::{Vec2, Vec4};
6use std::f32::consts::PI;
7
8pub struct DielectricBreakdown {
12 pub grid: Vec<f32>, pub width: usize,
14 pub height: usize,
15 pub threshold: f32, pub bolt_mask: Vec<bool>, }
18
19impl DielectricBreakdown {
20 pub fn new(width: usize, height: usize, threshold: f32) -> Self {
21 Self {
22 grid: vec![0.0; width * height],
23 width,
24 height,
25 threshold,
26 bolt_mask: vec![false; width * height],
27 }
28 }
29
30 fn idx(&self, x: usize, y: usize) -> usize {
31 x.min(self.width - 1) + y.min(self.height - 1) * self.width
32 }
33
34 pub fn set_boundary_conditions(&mut self, start_y: usize) {
36 for x in 0..self.width {
38 let i = self.idx(x, start_y);
39 self.grid[i] = 1.0;
40 }
41 for x in 0..self.width {
43 let i = self.idx(x, self.height - 1);
44 self.grid[i] = 0.0;
45 }
46 for y in 0..self.height {
48 for x in 0..self.width {
49 let i = self.idx(x, y);
50 if self.bolt_mask[i] {
51 self.grid[i] = 1.0;
52 }
53 }
54 }
55 }
56}
57
58#[derive(Clone, Debug)]
62pub struct LightningBolt {
63 pub segments: Vec<(Vec2, Vec2)>,
64 pub brightness: Vec<f32>,
65}
66
67impl LightningBolt {
68 pub fn new() -> Self {
69 Self {
70 segments: Vec::new(),
71 brightness: Vec::new(),
72 }
73 }
74
75 pub fn add_segment(&mut self, start: Vec2, end: Vec2, brightness: f32) {
76 self.segments.push((start, end));
77 self.brightness.push(brightness);
78 }
79
80 pub fn total_length(&self) -> f32 {
82 self.segments.iter().map(|(a, b)| (*b - *a).length()).sum()
83 }
84
85 pub fn segment_count(&self) -> usize {
87 self.segments.len()
88 }
89
90 pub fn endpoints(&self) -> Option<(Vec2, Vec2)> {
92 if self.segments.is_empty() {
93 return None;
94 }
95 Some((self.segments[0].0, self.segments.last().unwrap().1))
96 }
97}
98
99impl Default for LightningBolt {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105pub fn laplace_solve(
110 grid: &mut Vec<f32>,
111 boundary_mask: &[bool],
112 width: usize,
113 height: usize,
114 iterations: usize,
115) {
116 let omega = 1.6; let mut scratch = grid.clone();
118
119 for _ in 0..iterations {
120 for y in 1..height - 1 {
121 for x in 1..width - 1 {
122 let i = x + y * width;
123 if boundary_mask[i] {
124 continue; }
126 let avg = 0.25 * (
127 scratch[(x - 1) + y * width]
128 + scratch[(x + 1) + y * width]
129 + scratch[x + (y - 1) * width]
130 + scratch[x + (y + 1) * width]
131 );
132 scratch[i] = (1.0 - omega) * scratch[i] + omega * avg;
133 }
134 }
135 grid.copy_from_slice(&scratch);
136 }
137}
138
139pub fn generate_bolt(
151 start: Vec2,
152 end: Vec2,
153 grid_size: usize,
154 branch_prob: f32,
155) -> LightningBolt {
156 let width = grid_size;
157 let height = grid_size;
158
159 let mut bolt = LightningBolt::new();
160 let mut breakdown = DielectricBreakdown::new(width, height, 0.5);
161
162 let sx = (start.x * (width - 1) as f32).clamp(0.0, (width - 1) as f32) as usize;
164 let sy = (start.y * (height - 1) as f32).clamp(0.0, (height - 1) as f32) as usize;
165 let ex = (end.x * (width - 1) as f32).clamp(0.0, (width - 1) as f32) as usize;
166 let ey = (end.y * (height - 1) as f32).clamp(0.0, (height - 1) as f32) as usize;
167
168 breakdown.bolt_mask[sx + sy * width] = true;
170 let mut bolt_points: Vec<(usize, usize)> = vec![(sx, sy)];
171
172 let mut boundary_mask = vec![false; width * height];
174 for x in 0..width {
176 boundary_mask[x] = true; breakdown.grid[x] = 0.0;
178 boundary_mask[x + (height - 1) * width] = true; breakdown.grid[x + (height - 1) * width] = 0.0;
180 }
181 for y in 0..height {
183 boundary_mask[y * width] = true;
184 boundary_mask[(width - 1) + y * width] = true;
185 }
186
187 breakdown.grid[ex + ey * width] = 0.0;
189 boundary_mask[ex + ey * width] = true;
190
191 let mut hash_seed = 42u64;
193 let hash_next = |seed: &mut u64| -> f32 {
194 *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
195 ((*seed >> 33) as f32) / (u32::MAX as f32 / 2.0)
196 };
197
198 let max_steps = width * height / 2;
199 let mut reached_target = false;
200
201 for _step in 0..max_steps {
202 for &(bx, by) in &bolt_points {
204 let i = bx + by * width;
205 breakdown.grid[i] = 1.0;
206 boundary_mask[i] = true;
207 }
208
209 laplace_solve(&mut breakdown.grid, &boundary_mask, width, height, 50);
211
212 let mut candidates: Vec<(usize, usize, f32)> = Vec::new();
214 for &(bx, by) in &bolt_points {
215 let neighbors = [
216 (bx.wrapping_sub(1), by),
217 (bx + 1, by),
218 (bx, by.wrapping_sub(1)),
219 (bx, by + 1),
220 ];
221 for (nx, ny) in neighbors {
222 if nx >= width || ny >= height {
223 continue;
224 }
225 let ni = nx + ny * width;
226 if breakdown.bolt_mask[ni] {
227 continue;
228 }
229 let gradient = (1.0 - breakdown.grid[ni]).max(0.0);
231 let eta = 2.0; let weight = gradient.powf(eta);
233 if weight > 1e-10 {
234 candidates.push((nx, ny, weight));
235 }
236 }
237 }
238
239 if candidates.is_empty() {
240 break;
241 }
242
243 let total_weight: f32 = candidates.iter().map(|c| c.2).sum();
245 if total_weight < 1e-10 {
246 break;
247 }
248 let r = hash_next(&mut hash_seed).abs() * total_weight;
249 let mut cumulative = 0.0f32;
250 let mut chosen = candidates[0];
251 for c in &candidates {
252 cumulative += c.2;
253 if cumulative >= r {
254 chosen = *c;
255 break;
256 }
257 }
258
259 let (nx, ny, _) = chosen;
260 let ni = nx + ny * width;
261 breakdown.bolt_mask[ni] = true;
262
263 let mut best_dist = f32::MAX;
265 let mut from = (sx, sy);
266 for &(bx, by) in &bolt_points {
267 let dx = nx as f32 - bx as f32;
268 let dy = ny as f32 - by as f32;
269 let dist = dx * dx + dy * dy;
270 if dist < best_dist {
271 best_dist = dist;
272 from = (bx, by);
273 }
274 }
275
276 let seg_start = Vec2::new(from.0 as f32 / (width - 1) as f32, from.1 as f32 / (height - 1) as f32);
277 let seg_end = Vec2::new(nx as f32 / (width - 1) as f32, ny as f32 / (height - 1) as f32);
278 bolt.add_segment(seg_start, seg_end, 1.0);
279 bolt_points.push((nx, ny));
280
281 let dx = nx as i32 - ex as i32;
283 let dy = ny as i32 - ey as i32;
284 if dx * dx + dy * dy <= 2 {
285 reached_target = true;
286 let final_end = Vec2::new(ex as f32 / (width - 1) as f32, ey as f32 / (height - 1) as f32);
288 bolt.add_segment(seg_end, final_end, 1.0);
289 break;
290 }
291 }
292
293 add_branches(&mut bolt, branch_prob, &mut hash_seed);
295
296 bolt
297}
298
299pub fn add_branches(bolt: &mut LightningBolt, probability: f32, seed: &mut u64) {
302 let hash_next = |s: &mut u64| -> f32 {
303 *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
304 ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
305 };
306
307 let original_count = bolt.segments.len();
308 let mut new_segments = Vec::new();
309 let mut new_brightness = Vec::new();
310
311 for i in 0..original_count {
312 let r = hash_next(seed).abs();
313 if r < probability {
314 let (start, end) = bolt.segments[i];
315 let mid = (start + end) * 0.5;
316 let dir = end - start;
317 let len = dir.length() * 0.7;
318
319 let angle_offset = hash_next(seed) * PI * 0.5;
321 let base_angle = dir.y.atan2(dir.x);
322 let branch_angle = base_angle + angle_offset;
323 let branch_end = mid + Vec2::new(branch_angle.cos(), branch_angle.sin()) * len;
324
325 new_segments.push((mid, branch_end));
326 new_brightness.push(bolt.brightness[i] * 0.5);
327
328 if hash_next(seed).abs() < probability * 0.5 {
330 let sub_len = len * 0.5;
331 let sub_angle = branch_angle + hash_next(seed) * PI * 0.3;
332 let sub_end = branch_end + Vec2::new(sub_angle.cos(), sub_angle.sin()) * sub_len;
333 new_segments.push((branch_end, sub_end));
334 new_brightness.push(bolt.brightness[i] * 0.25);
335 }
336 }
337 }
338
339 bolt.segments.extend(new_segments);
340 bolt.brightness.extend(new_brightness);
341}
342
343pub fn bolt_with_return_stroke(mut bolt: LightningBolt) -> LightningBolt {
345 let main_channel_len = bolt.segments.len();
348 for i in 0..main_channel_len {
349 bolt.brightness[i] = (bolt.brightness[i] * 3.0).min(1.0);
350 }
351 bolt
352}
353
354pub fn animate_bolt(bolt: &LightningBolt, time: f32) -> Vec<(Vec2, Vec2, f32)> {
357 let total_segments = bolt.segments.len() as f32;
358 if total_segments < 1.0 {
359 return Vec::new();
360 }
361
362 let leader_duration = 1.0; let return_duration = 0.1; let decay_duration = 0.5; let mut result = Vec::new();
367
368 for (i, &(start, end)) in bolt.segments.iter().enumerate() {
369 let segment_time = (i as f32 / total_segments) * leader_duration;
370 let base_brightness = bolt.brightness[i];
371
372 if time < segment_time {
373 continue;
375 }
376
377 let brightness;
378 if time < leader_duration {
379 brightness = base_brightness * 0.3;
381 } else if time < leader_duration + return_duration {
382 brightness = base_brightness;
384 } else {
385 let decay_t = (time - leader_duration - return_duration) / decay_duration;
387 brightness = base_brightness * (1.0 - decay_t).max(0.0);
388 }
389
390 if brightness > 0.01 {
391 result.push((start, end, brightness));
392 }
393 }
394
395 result
396}
397
398pub fn stepped_leader(
400 start: Vec2,
401 direction: Vec2,
402 steps: usize,
403 seed: &mut u64,
404) -> Vec<Vec2> {
405 let hash_next = |s: &mut u64| -> f32 {
406 *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
407 ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
408 };
409
410 let step_length = direction.length() / steps as f32;
411 let dir_norm = direction.normalize();
412
413 let mut points = Vec::with_capacity(steps + 1);
414 let mut pos = start;
415 points.push(pos);
416
417 for _ in 0..steps {
418 let jitter_angle = hash_next(seed) * PI * 0.4 - PI * 0.2;
420 let angle = dir_norm.y.atan2(dir_norm.x) + jitter_angle;
421 let step = Vec2::new(angle.cos(), angle.sin()) * step_length;
422 pos += step;
423 points.push(pos);
424 }
425
426 points
427}
428
429pub struct LightningRenderer {
433 pub core_color: Vec4,
434 pub glow_color: Vec4,
435 pub glow_radius: f32,
436}
437
438impl LightningRenderer {
439 pub fn new() -> Self {
440 Self {
441 core_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
442 glow_color: Vec4::new(0.6, 0.7, 1.0, 0.5),
443 glow_radius: 3.0,
444 }
445 }
446
447 pub fn color_for_brightness(&self, brightness: f32) -> Vec4 {
449 let t = brightness.clamp(0.0, 1.0);
450 let core = self.core_color * t;
451 Vec4::new(
452 core.x.max(self.glow_color.x * t * 0.5),
453 core.y.max(self.glow_color.y * t * 0.5),
454 core.z.max(self.glow_color.z * t * 0.5),
455 t,
456 )
457 }
458
459 pub fn bolt_glyph(direction: Vec2) -> char {
461 let angle = direction.y.atan2(direction.x);
462 let octant = ((angle / (PI / 4.0)).round() as i32).rem_euclid(8);
463 match octant {
464 0 | 4 => '─',
465 1 | 5 => '╲',
466 2 | 6 => '│',
467 3 | 7 => '╱',
468 _ => '⚡',
469 }
470 }
471
472 pub fn render_bolt(&self, bolt: &LightningBolt) -> Vec<(Vec2, char, Vec4)> {
474 let mut result = Vec::new();
475 for (i, &(start, end)) in bolt.segments.iter().enumerate() {
476 let brightness = bolt.brightness[i];
477 let color = self.color_for_brightness(brightness);
478 let dir = end - start;
479 let ch = Self::bolt_glyph(dir);
480 let mid = (start + end) * 0.5;
481 result.push((mid, ch, color));
482 }
483 result
484 }
485}
486
487impl Default for LightningRenderer {
488 fn default() -> Self {
489 Self::new()
490 }
491}
492
493#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_laplace_solver() {
501 let width = 10;
502 let height = 10;
503 let mut grid = vec![0.0; width * height];
504 let mut boundary = vec![false; width * height];
505
506 for x in 0..width {
508 grid[x] = 1.0;
509 boundary[x] = true;
510 grid[x + (height - 1) * width] = 0.0;
511 boundary[x + (height - 1) * width] = true;
512 }
513 for y in 0..height {
515 grid[y * width] = 1.0 - y as f32 / (height - 1) as f32;
516 boundary[y * width] = true;
517 grid[(width - 1) + y * width] = 1.0 - y as f32 / (height - 1) as f32;
518 boundary[(width - 1) + y * width] = true;
519 }
520
521 laplace_solve(&mut grid, &boundary, width, height, 200);
522
523 let mid_x = width / 2;
525 for y in 1..height - 1 {
526 let val = grid[mid_x + y * width];
527 assert!(val >= 0.0 && val <= 1.0, "Value at ({},{}) = {} out of range", mid_x, y, val);
528 }
529 let mid_val = grid[mid_x + (height / 2) * width];
531 assert!((mid_val - 0.5).abs() < 0.15, "Mid value should be ~0.5: {}", mid_val);
532 }
533
534 #[test]
535 fn test_bolt_connects_start_to_end() {
536 let bolt = generate_bolt(
537 Vec2::new(0.5, 0.1),
538 Vec2::new(0.5, 0.9),
539 20,
540 0.0, );
542
543 assert!(bolt.segments.len() > 0, "Bolt should have segments");
544
545 if let Some((first_start, _)) = bolt.segments.first() {
547 let dist_to_start = (*first_start - Vec2::new(0.5, 0.1)).length();
548 assert!(dist_to_start < 0.3, "Bolt should start near start point: {}", dist_to_start);
549 }
550 }
551
552 #[test]
553 fn test_branching_creates_tree() {
554 let bolt_no_branch = generate_bolt(
555 Vec2::new(0.5, 0.1),
556 Vec2::new(0.5, 0.9),
557 20,
558 0.0,
559 );
560 let bolt_with_branch = generate_bolt(
561 Vec2::new(0.5, 0.1),
562 Vec2::new(0.5, 0.9),
563 20,
564 0.8, );
566
567 assert!(
569 bolt_with_branch.segments.len() >= bolt_no_branch.segments.len(),
570 "Branching should add segments: {} vs {}",
571 bolt_with_branch.segments.len(),
572 bolt_no_branch.segments.len()
573 );
574 }
575
576 #[test]
577 fn test_return_stroke() {
578 let mut bolt = LightningBolt::new();
579 bolt.add_segment(Vec2::ZERO, Vec2::new(0.0, 0.5), 0.3);
580 bolt.add_segment(Vec2::new(0.0, 0.5), Vec2::new(0.0, 1.0), 0.3);
581
582 let bright = bolt_with_return_stroke(bolt);
583 assert!(bright.brightness[0] > 0.3, "Return stroke should brighten");
584 }
585
586 #[test]
587 fn test_animate_bolt() {
588 let mut bolt = LightningBolt::new();
589 for i in 0..10 {
590 let y0 = i as f32 * 0.1;
591 let y1 = (i + 1) as f32 * 0.1;
592 bolt.add_segment(Vec2::new(0.5, y0), Vec2::new(0.5, y1), 1.0);
593 }
594
595 let anim_early = animate_bolt(&bolt, 0.01);
597 assert!(!anim_early.is_empty(), "Some segments should be visible early");
598
599 let anim_return = animate_bolt(&bolt, 1.05);
601 assert_eq!(anim_return.len(), 10, "All segments visible during return stroke");
602 }
603
604 #[test]
605 fn test_stepped_leader() {
606 let mut seed = 99u64;
607 let points = stepped_leader(
608 Vec2::new(0.5, 0.0),
609 Vec2::new(0.0, 1.0),
610 20,
611 &mut seed,
612 );
613 assert_eq!(points.len(), 21);
614 assert!((points[0] - Vec2::new(0.5, 0.0)).length() < 1e-6);
615 let last = points.last().unwrap();
617 assert!(last.y > 0.0, "Leader should progress in direction");
618 }
619
620 #[test]
621 fn test_renderer_glyph() {
622 assert_eq!(LightningRenderer::bolt_glyph(Vec2::new(0.0, 1.0)), '│');
623 assert_eq!(LightningRenderer::bolt_glyph(Vec2::new(1.0, 0.0)), '─');
624 }
625
626 #[test]
627 fn test_bolt_total_length() {
628 let mut bolt = LightningBolt::new();
629 bolt.add_segment(Vec2::ZERO, Vec2::new(1.0, 0.0), 1.0);
630 bolt.add_segment(Vec2::new(1.0, 0.0), Vec2::new(1.0, 1.0), 1.0);
631 assert!((bolt.total_length() - 2.0).abs() < 1e-6);
632 }
633}