Skip to main content

pfx_live/flat/effects/
shards.rs

1use super::super::{Draw, Fill, Shadow, Shape, Stroke, place};
2use super::between;
3use pfx_core::sim::Rng;
4use pfx_physics::rigid::{
5    BodyDesc, BodyId, ColliderDesc, Combine, Friction, Layers, Locks, Pose, World,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Shatter {
10    pub fill: Fill,
11    pub stroke: Option<Stroke>,
12    pub count: u32,
13    pub half: [[f32; 2]; 2],
14    pub radius: f32,
15    pub depth: f32,
16    pub speed: [f32; 2],
17    pub direction: f32,
18    pub spread: f32,
19    pub spin: [f32; 2],
20    pub life: [f32; 2],
21    pub fade: f32,
22    pub density: f32,
23    pub restitution: f32,
24    pub friction: f32,
25    pub layers: Layers,
26    pub elevation: f32,
27    pub shadow: Shadow,
28}
29
30impl Shatter {
31    pub fn new(fill: Fill, half: [[f32; 2]; 2]) -> Self {
32        Self {
33            fill,
34            stroke: None,
35            count: 8,
36            half,
37            radius: 2.0,
38            depth: 4.0,
39            speed: [200.0, 500.0],
40            direction: -std::f32::consts::FRAC_PI_2,
41            spread: std::f32::consts::PI,
42            spin: [0.0, 6.0],
43            life: [1.5, 2.5],
44            fade: 0.4,
45            density: 1.0,
46            restitution: 0.3,
47            friction: 0.6,
48            layers: Layers::ALL,
49            elevation: 1.0,
50            shadow: Shadow::Cast,
51        }
52    }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
56struct Shard {
57    body: BodyId,
58    half: [f32; 2],
59    radius: f32,
60    age: f32,
61    life: f32,
62    fade: f32,
63    fill: Fill,
64    stroke: Option<Stroke>,
65    elevation: f32,
66    shadow: Shadow,
67}
68
69pub struct Shards {
70    items: Vec<Shard>,
71    cap: usize,
72    rng: Rng,
73}
74
75pub fn angle_of(rotation: [f32; 4]) -> f32 {
76    2.0 * rotation[2].atan2(rotation[3])
77}
78
79impl Shards {
80    pub fn new(cap: usize, seed: u64) -> Self {
81        Self {
82            items: Vec::with_capacity(cap),
83            cap,
84            rng: Rng::new(seed),
85        }
86    }
87
88    pub fn live(&self) -> usize {
89        self.items.len()
90    }
91
92    pub fn bodies(&self) -> impl Iterator<Item = BodyId> + '_ {
93        self.items.iter().map(|shard| shard.body)
94    }
95
96    pub fn spawn(&mut self, world: &mut World, shatter: &Shatter, at: [f32; 2]) -> u32 {
97        let mut spawned = 0;
98        for _ in 0..shatter.count {
99            if self.items.len() >= self.cap {
100                break;
101            }
102            let rng = &mut self.rng;
103            let half = [
104                between(rng, [shatter.half[0][0], shatter.half[1][0]]),
105                between(rng, [shatter.half[0][1], shatter.half[1][1]]),
106            ];
107            let angle = shatter.direction + (rng.next_f32() - 0.5) * shatter.spread;
108            let speed = between(rng, shatter.speed);
109            let spin = between(rng, shatter.spin) * if rng.next_bool() { 1.0 } else { -1.0 };
110            let life = between(rng, shatter.life).max(1e-3);
111            let turn = rng.next_f32() * std::f32::consts::TAU;
112            let pose = Pose::at([at[0], at[1], 0.0]).turned([0.0, 0.0, 1.0], turn);
113            let body = world.add_body(
114                BodyDesc::dynamic(pose)
115                    .with_velocity([angle.cos() * speed, angle.sin() * speed, 0.0])
116                    .with_spin([0.0, 0.0, spin])
117                    .with_locks(Locks::PLANE_XY),
118            );
119            let radius = shatter.radius.clamp(0.0, half[0].min(half[1]));
120            let collider =
121                ColliderDesc::round_box([half[0], half[1], shatter.depth.max(0.5)], radius)
122                    .with_density(shatter.density)
123                    .with_friction(Friction::uniform(shatter.friction))
124                    .with_restitution(shatter.restitution, Combine::Max)
125                    .with_layers(shatter.layers);
126            if world.add_collider(body, collider).is_none() {
127                world.remove_body(body);
128                continue;
129            }
130            self.items.push(Shard {
131                body,
132                half,
133                radius,
134                age: 0.0,
135                life,
136                fade: shatter.fade.max(0.0),
137                fill: shatter.fill,
138                stroke: shatter.stroke,
139                elevation: shatter.elevation,
140                shadow: shatter.shadow,
141            });
142            spawned += 1;
143        }
144        spawned
145    }
146
147    pub fn step(&mut self, world: &mut World, dt: f32) {
148        if dt.is_nan() || dt <= 0.0 {
149            return;
150        }
151        let mut index = 0;
152        while index < self.items.len() {
153            let shard = &mut self.items[index];
154            shard.age += dt;
155            if shard.age >= shard.life {
156                world.remove_body(shard.body);
157                self.items.swap_remove(index);
158            } else {
159                index += 1;
160            }
161        }
162    }
163
164    pub fn clear(&mut self, world: &mut World) {
165        for shard in self.items.drain(..) {
166            world.remove_body(shard.body);
167        }
168    }
169
170    pub fn draws(&self, world: &World, out: &mut Vec<Draw>) {
171        for shard in &self.items {
172            let Some(pose) = world.pose(shard.body) else {
173                continue;
174            };
175            let left = shard.life - shard.age;
176            let alpha = if shard.fade > 0.0 {
177                (left / shard.fade).clamp(0.0, 1.0)
178            } else {
179                1.0
180            };
181            let mut draw = Draw::new(Shape::Rect {
182                half: shard.half,
183                radii: [shard.radius; 4],
184            })
185            .transform(place(
186                [pose.position[0], pose.position[1]],
187                angle_of(pose.rotation),
188            ))
189            .elevation(shard.elevation)
190            .shadow(shard.shadow)
191            .opacity(alpha)
192            .unpicked();
193            draw.fill = shard.fill;
194            draw.stroke = shard.stroke;
195            out.push(draw);
196        }
197    }
198}