1use std::cell::RefCell;
12use std::rc::Rc;
13
14use deno_core::OpState;
15
16#[derive(Debug, Clone)]
18struct Particle {
19 x: f32,
20 y: f32,
21 vx: f32,
22 vy: f32,
23 angle: f32,
24 angular_vel: f32,
25 scale: f32,
26 alpha: f32,
27 lifetime: f32,
28 max_lifetime: f32,
29 texture_id: u32,
30}
31
32#[derive(Debug, Clone)]
34struct EmitterConfig {
35 spawn_rate: f32,
36 lifetime_min: f32,
37 lifetime_max: f32,
38 speed_min: f32,
39 speed_max: f32,
40 direction: f32,
41 spread: f32,
42 scale_min: f32,
43 scale_max: f32,
44 alpha_start: f32,
45 alpha_end: f32,
46 gravity_x: f32,
47 gravity_y: f32,
48 texture_id: u32,
49}
50
51impl Default for EmitterConfig {
52 fn default() -> Self {
53 Self {
54 spawn_rate: 10.0,
55 lifetime_min: 0.5,
56 lifetime_max: 1.5,
57 speed_min: 20.0,
58 speed_max: 80.0,
59 direction: -std::f32::consts::FRAC_PI_2, spread: std::f32::consts::PI,
61 scale_min: 1.0,
62 scale_max: 1.0,
63 alpha_start: 1.0,
64 alpha_end: 0.0,
65 gravity_x: 0.0,
66 gravity_y: 0.0,
67 texture_id: 0,
68 }
69 }
70}
71
72#[derive(Debug)]
74pub struct ParticleEmitter {
75 pub id: u32,
76 config: EmitterConfig,
77 particles: Vec<Particle>,
78 time_accumulator: f32,
79 rng_state: u32,
81}
82
83impl ParticleEmitter {
84 fn new(id: u32, config: EmitterConfig) -> Self {
85 let rng_state = id.wrapping_mul(2654435761).max(1);
87 Self {
88 id,
89 config,
90 particles: Vec::new(),
91 time_accumulator: 0.0,
92 rng_state,
93 }
94 }
95
96 fn rand(&mut self) -> f32 {
98 let mut s = self.rng_state;
99 s ^= s << 13;
100 s ^= s >> 17;
101 s ^= s << 5;
102 self.rng_state = s;
103 (s as f32) / (u32::MAX as f32)
104 }
105
106 fn rand_range(&mut self, min: f32, max: f32) -> f32 {
107 min + self.rand() * (max - min)
108 }
109
110 fn spawn_particle(&mut self, cx: f32, cy: f32) {
111 let lifetime_min = self.config.lifetime_min;
113 let lifetime_max = self.config.lifetime_max;
114 let speed_min = self.config.speed_min;
115 let speed_max = self.config.speed_max;
116 let direction = self.config.direction;
117 let half_spread = self.config.spread * 0.5;
118 let scale_min = self.config.scale_min;
119 let scale_max = self.config.scale_max;
120 let alpha_start = self.config.alpha_start;
121 let texture_id = self.config.texture_id;
122
123 let lifetime = self.rand_range(lifetime_min, lifetime_max);
124 let speed = self.rand_range(speed_min, speed_max);
125 let angle = direction + self.rand_range(-half_spread, half_spread);
126 let scale = self.rand_range(scale_min, scale_max);
127
128 self.particles.push(Particle {
129 x: cx,
130 y: cy,
131 vx: angle.cos() * speed,
132 vy: angle.sin() * speed,
133 angle: 0.0,
134 angular_vel: 0.0,
135 scale,
136 alpha: alpha_start,
137 lifetime: 0.0,
138 max_lifetime: lifetime,
139 texture_id,
140 });
141 }
142
143 fn update(&mut self, dt: f32, cx: f32, cy: f32) {
144 let spawn_rate = self.config.spawn_rate;
146 let gx = self.config.gravity_x;
147 let gy = self.config.gravity_y;
148 let alpha_start = self.config.alpha_start;
149 let alpha_end = self.config.alpha_end;
150
151 self.time_accumulator += dt * spawn_rate;
153 while self.time_accumulator >= 1.0 {
154 self.spawn_particle(cx, cy);
155 self.time_accumulator -= 1.0;
156 }
157
158 self.particles.retain_mut(|p| {
159 p.lifetime += dt;
160 if p.lifetime >= p.max_lifetime {
161 return false;
162 }
163
164 p.vx += gx * dt;
165 p.vy += gy * dt;
166 p.x += p.vx * dt;
167 p.y += p.vy * dt;
168 p.angle += p.angular_vel * dt;
169
170 let t = p.lifetime / p.max_lifetime;
172 p.alpha = alpha_start + (alpha_end - alpha_start) * t;
173
174 true
175 });
176 }
177}
178
179pub struct ParticleState {
181 pub emitters: Vec<ParticleEmitter>,
182 pub next_id: u32,
183}
184
185impl ParticleState {
186 pub fn new() -> Self {
187 Self {
188 emitters: Vec::new(),
189 next_id: 1,
190 }
191 }
192
193 fn find(&self, id: u32) -> Option<usize> {
194 self.emitters.iter().position(|e| e.id == id)
195 }
196}
197
198#[deno_core::op2(fast)]
206fn op_create_emitter(state: &mut OpState, #[string] config_json: &str) -> u32 {
207 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
208 let mut ps = ps.borrow_mut();
209
210 let mut cfg = EmitterConfig::default();
211
212 fn extract_f32(json: &str, key: &str) -> Option<f32> {
215 let needle = format!("\"{}\"", key);
216 let start = json.find(&needle)?;
217 let after_key = &json[start + needle.len()..];
218 let colon = after_key.find(':')?;
219 let val_start = &after_key[colon + 1..];
220 let val_start = val_start.trim_start();
222 let end = val_start.find(|c: char| c == ',' || c == '}').unwrap_or(val_start.len());
224 val_start[..end].trim().parse().ok()
225 }
226
227 fn extract_u32(json: &str, key: &str) -> Option<u32> {
228 extract_f32(json, key).map(|f| f as u32)
229 }
230
231 if let Some(v) = extract_f32(config_json, "spawnRate") { cfg.spawn_rate = v; }
232 if let Some(v) = extract_f32(config_json, "lifetimeMin") { cfg.lifetime_min = v; }
233 if let Some(v) = extract_f32(config_json, "lifetimeMax") { cfg.lifetime_max = v; }
234 if let Some(v) = extract_f32(config_json, "speedMin") { cfg.speed_min = v; }
235 if let Some(v) = extract_f32(config_json, "speedMax") { cfg.speed_max = v; }
236 if let Some(v) = extract_f32(config_json, "direction") { cfg.direction = v; }
237 if let Some(v) = extract_f32(config_json, "spread") { cfg.spread = v; }
238 if let Some(v) = extract_f32(config_json, "scaleMin") { cfg.scale_min = v; }
239 if let Some(v) = extract_f32(config_json, "scaleMax") { cfg.scale_max = v; }
240 if let Some(v) = extract_f32(config_json, "alphaStart") { cfg.alpha_start = v; }
241 if let Some(v) = extract_f32(config_json, "alphaEnd") { cfg.alpha_end = v; }
242 if let Some(v) = extract_f32(config_json, "gravityX") { cfg.gravity_x = v; }
243 if let Some(v) = extract_f32(config_json, "gravityY") { cfg.gravity_y = v; }
244 if let Some(v) = extract_u32(config_json, "textureId") { cfg.texture_id = v; }
245
246 let id = ps.next_id;
247 ps.next_id += 1;
248 ps.emitters.push(ParticleEmitter::new(id, cfg));
249 id
250}
251
252#[deno_core::op2(fast)]
255fn op_update_emitter(state: &mut OpState, id: u32, dt: f64, cx: f64, cy: f64) {
256 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
257 let mut ps = ps.borrow_mut();
258 if let Some(idx) = ps.find(id) {
259 ps.emitters[idx].update(dt as f32, cx as f32, cy as f32);
260 }
261}
262
263#[deno_core::op2(fast)]
265fn op_destroy_emitter(state: &mut OpState, id: u32) {
266 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
267 let mut ps = ps.borrow_mut();
268 if let Some(idx) = ps.find(id) {
269 ps.emitters.swap_remove(idx);
270 }
271}
272
273#[deno_core::op2(fast)]
275fn op_get_emitter_particle_count(state: &mut OpState, id: u32) -> u32 {
276 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
277 let ps = ps.borrow();
278 match ps.find(id) {
279 Some(idx) => ps.emitters[idx].particles.len() as u32,
280 None => 0,
281 }
282}
283
284#[deno_core::op2]
288#[buffer]
289fn op_get_emitter_sprite_data(state: &mut OpState, id: u32) -> Vec<u8> {
290 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
291 let ps = ps.borrow();
292 let idx = match ps.find(id) {
293 Some(i) => i,
294 None => return Vec::new(),
295 };
296
297 let emitter = &ps.emitters[idx];
298 let count = emitter.particles.len();
299 let mut floats = Vec::with_capacity(count * 6);
300
301 for p in &emitter.particles {
302 floats.push(p.x);
303 floats.push(p.y);
304 floats.push(p.angle);
305 floats.push(p.scale);
306 floats.push(p.alpha);
307 floats.push(f32::from_bits(p.texture_id));
308 }
309
310 bytemuck::cast_slice(&floats).to_vec()
311}
312
313deno_core::extension!(
314 particle_ext,
315 ops = [
316 op_create_emitter,
317 op_update_emitter,
318 op_destroy_emitter,
319 op_get_emitter_particle_count,
320 op_get_emitter_sprite_data,
321 ],
322);