1use super::*;
4use motion_core::{Point, Rect};
5
6#[derive(Clone)]
8pub enum ForceFalloff {
9 Constant,
11 Linear,
13 InverseSquare,
15 Curve(ParticleCurve),
17}
18
19#[derive(Clone)]
21pub struct RadialForce {
22 pub center: Point,
24 pub strength: f32,
26 pub radius: f32,
28 pub falloff: ForceFalloff,
30}
31
32impl ParticleModifier for RadialForce {
33 fn apply(
34 &self,
35 particle: &mut Particle,
36 step: f32,
37 _: Time,
38 _: &mut Random,
39 ) -> Result<(), ValidationError> {
40 if !self.radius.is_finite()
41 || self.radius <= 0.0
42 || !self.strength.is_finite()
43 || !self.center.x.is_finite()
44 || !self.center.y.is_finite()
45 {
46 return Err(invalid(
47 "force",
48 "Require finite force center/strength and positive radius",
49 ));
50 }
51 let delta = [
52 self.center.x as f32 - particle.position[0],
53 self.center.y as f32 - particle.position[1],
54 ];
55 let distance = libm::hypotf(delta[0], delta[1]);
56 if distance >= self.radius || distance == 0.0 {
57 return Ok(());
58 }
59 let amount = match &self.falloff {
60 ForceFalloff::Constant => 1.0,
61 ForceFalloff::Linear => 1.0 - distance / self.radius,
62 ForceFalloff::InverseSquare => 1.0 / distance.max(1.0).powi(2),
63 ForceFalloff::Curve(curve) => curve.sample(distance / self.radius)?,
64 };
65 for (velocity, delta) in particle.velocity.iter_mut().zip(delta) {
66 *velocity += delta / distance * self.strength * amount * step;
67 }
68 particle.validate()
69 }
70}
71
72#[derive(Clone, Copy, Debug)]
74pub struct CurlNoise {
75 pub frequency: f64,
77 pub strength: f64,
79 pub seed: u32,
81 pub phase_speed: f64,
83}
84
85fn shared_curl(
86 position: [f32; 2],
87 frequency: f64,
88 phase: f64,
89 seed: u32,
90) -> Result<[f64; 2], ValidationError> {
91 if !frequency.is_finite() || frequency <= 0.0 || !phase.is_finite() {
92 return Err(invalid(
93 "curl",
94 "Require positive finite frequency and finite phase",
95 ));
96 }
97 let point = position.map(|value| f64::from(value) * frequency + phase);
98 if !motion_core::noise::lattice_supported(point) {
99 return Err(invalid(
100 "curl",
101 "Curl coordinates exceed signed 32-bit lattice support",
102 ));
103 }
104 Ok(motion_core::noise::curl2(point, seed))
105}
106
107impl ParticleModifier for CurlNoise {
108 fn apply(
109 &self,
110 particle: &mut Particle,
111 step: f32,
112 time: Time,
113 _: &mut Random,
114 ) -> Result<(), ValidationError> {
115 if !self.strength.is_finite()
116 || !self.phase_speed.is_finite()
117 || !step.is_finite()
118 || step < 0.0
119 {
120 return Err(invalid(
121 "curl",
122 "Require finite strength, phase speed and nonnegative step",
123 ));
124 }
125 let direction = shared_curl(
126 particle.position,
127 self.frequency,
128 time.as_seconds() * self.phase_speed,
129 self.seed,
130 )?;
131 let velocity: [f32; 2] = std::array::from_fn(|axis| {
132 (f64::from(particle.velocity[axis]) + direction[axis] * self.strength * f64::from(step))
133 as f32
134 });
135 if velocity.into_iter().any(|value| !value.is_finite()) {
136 return Err(invalid(
137 "curl",
138 "Curl velocity exceeds finite particle support",
139 ));
140 }
141 particle.velocity = velocity;
142 particle.validate()
143 }
144
145 fn work(&self) -> u64 {
146 96
147 }
148}
149
150#[derive(Clone)]
152pub enum FlowSource {
153 Curl {
155 seed: u32,
157 frequency: f64,
159 },
160 Image {
162 image: Arc<crate::ImageAsset>,
164 bounds: Rect,
166 },
167}
168
169#[derive(Clone)]
171pub struct FlowField {
172 pub source: FlowSource,
174 pub strength: f32,
176 pub phase_speed: f64,
178}
179
180impl FlowField {
181 pub fn curl(seed: u32, frequency: f64, strength: f32) -> Self {
183 Self {
184 source: FlowSource::Curl { seed, frequency },
185 strength,
186 phase_speed: 1.0,
187 }
188 }
189}
190
191impl ParticleModifier for FlowField {
192 fn apply(
193 &self,
194 particle: &mut Particle,
195 step: f32,
196 time: Time,
197 _: &mut Random,
198 ) -> Result<(), ValidationError> {
199 if !self.strength.is_finite() || !self.phase_speed.is_finite() {
200 return Err(invalid("flow", "Require finite strength and phase speed"));
201 }
202 let phase = time.as_seconds() * self.phase_speed;
203 let direction = match &self.source {
204 FlowSource::Curl { seed, frequency } => {
205 shared_curl(particle.position, *frequency, phase, *seed)?.map(|value| value as f32)
206 }
207 FlowSource::Image { image, bounds } => {
208 motion_core::drawing::validate_rect(*bounds, "particles.flow", "bounds")?;
209 if bounds.width() <= 0.0 || bounds.height() <= 0.0 {
210 return Err(invalid("flow", "Require positive flow image bounds"));
211 }
212 let horizontal = ((f64::from(particle.position[0]) - bounds.left) / bounds.width()
213 + phase)
214 .rem_euclid(1.0);
215 let vertical = ((f64::from(particle.position[1]) - bounds.top) / bounds.height())
216 .clamp(0.0, 1.0);
217 let sample = image.levels()[0]
218 .pixel(
219 (horizontal * f64::from(image.width())) as u32,
220 ((vertical * f64::from(image.height())) as u32).min(image.height() - 1),
221 )
222 .ok_or_else(|| invalid("flow", "Missing image vector"))?;
223 if sample[3] == 0.0 {
224 [0.0; 2]
225 } else {
226 [
227 sample[0] / sample[3] * 2.0 - 1.0,
228 sample[1] / sample[3] * 2.0 - 1.0,
229 ]
230 }
231 }
232 };
233 for (velocity, direction) in particle.velocity.iter_mut().zip(direction) {
234 *velocity += direction * self.strength * step;
235 }
236 particle.validate()
237 }
238
239 fn work(&self) -> u64 {
240 if matches!(self.source, FlowSource::Curl { .. }) {
241 96
242 } else {
243 1
244 }
245 }
246}
247
248#[derive(Clone, Copy, Debug)]
250pub struct Boids {
251 pub radius: f64,
253 pub separation: f32,
255 pub alignment: f32,
257 pub cohesion: f32,
259 pub max_particles: usize,
261}
262
263impl Default for Boids {
264 fn default() -> Self {
265 Self {
266 radius: 32.0,
267 separation: 1.0,
268 alignment: 0.5,
269 cohesion: 0.1,
270 max_particles: 4096,
271 }
272 }
273}
274
275impl ParticleGroupModifier for Boids {
276 fn apply(
277 &self,
278 particles: &mut [Particle],
279 step: f32,
280 _: Time,
281 max_work: u64,
282 ) -> Result<u64, ValidationError> {
283 if self.max_particles == 0
284 || particles.len() > self.max_particles
285 || [self.separation, self.alignment, self.cohesion]
286 .iter()
287 .any(|value| !value.is_finite() || *value < 0.0)
288 {
289 return Err(limit("Invalid flock parameters or population allowance"));
290 }
291 let pairs = super::neighbors::pairs(particles, self.radius, max_work / 16)?;
292 let work = particles.len() as u64 * 4 + pairs.len() as u64 * 32;
293 if work > max_work {
294 return Err(limit("Flock work exhausted"));
295 }
296 let mut forces = vec![[0.0_f32; 6]; particles.len()];
297 let mut counts = vec![0u32; particles.len()];
298 for (first, second) in pairs {
299 for (owner, other) in [(first, second), (second, first)] {
300 let difference = [
301 particles[owner].position[0] - particles[other].position[0],
302 particles[owner].position[1] - particles[other].position[1],
303 ];
304 let square =
305 (difference[0] * difference[0] + difference[1] * difference[1]).max(1.0);
306 for axis in 0..2 {
307 forces[owner][axis] += difference[axis] / square;
308 forces[owner][axis + 2] += particles[other].velocity[axis];
309 forces[owner][axis + 4] += particles[other].position[axis];
310 }
311 counts[owner] += 1;
312 }
313 }
314 for (index, particle) in particles.iter_mut().enumerate() {
315 if counts[index] == 0 {
316 continue;
317 }
318 let count = counts[index] as f32;
319 for axis in 0..2 {
320 particle.velocity[axis] += step
321 * (forces[index][axis] * self.separation
322 + (forces[index][axis + 2] / count - particle.velocity[axis])
323 * self.alignment
324 + (forces[index][axis + 4] / count - particle.position[axis])
325 * self.cohesion);
326 }
327 particle.validate()?;
328 }
329 Ok(work)
330 }
331}
332
333#[derive(Clone, Copy, Debug)]
335pub struct Spring {
336 pub rest_length: f32,
338 pub stiffness: f32,
340 pub damping: f32,
342 pub closed: bool,
344}
345
346impl ParticleGroupModifier for Spring {
347 fn apply(
348 &self,
349 particles: &mut [Particle],
350 step: f32,
351 _: Time,
352 max_work: u64,
353 ) -> Result<u64, ValidationError> {
354 if [self.rest_length, self.stiffness, self.damping]
355 .iter()
356 .any(|value| !value.is_finite() || *value < 0.0)
357 {
358 return Err(invalid(
359 "spring",
360 "Require finite nonnegative spring parameters",
361 ));
362 }
363 let work = particles.len() as u64 * 32;
364 if work > max_work {
365 return Err(limit("Spring work exhausted"));
366 }
367 let mut order = (0..particles.len()).collect::<Vec<_>>();
368 order.sort_by_key(|index| particles[*index].index);
369 let mut forces = vec![[0.0_f32; 2]; particles.len()];
370 let count = order.len().saturating_sub(1) + usize::from(self.closed && order.len() > 2);
371 for index in 0..count {
372 let first = order[index];
373 let second = order[(index + 1) % order.len()];
374 let delta = [
375 particles[second].position[0] - particles[first].position[0],
376 particles[second].position[1] - particles[first].position[1],
377 ];
378 let distance = libm::hypotf(delta[0], delta[1]);
379 if distance == 0.0 {
380 continue;
381 }
382 let normal = delta.map(|value| value / distance);
383 let speed = (particles[second].velocity[0] - particles[first].velocity[0]) * normal[0]
384 + (particles[second].velocity[1] - particles[first].velocity[1]) * normal[1];
385 let amount = (distance - self.rest_length) * self.stiffness + speed * self.damping;
386 for axis in 0..2 {
387 forces[first][axis] += normal[axis] * amount;
388 forces[second][axis] -= normal[axis] * amount;
389 }
390 }
391 for (particle, force) in particles.iter_mut().zip(forces) {
392 for (velocity, force) in particle.velocity.iter_mut().zip(force) {
393 *velocity += force * step;
394 }
395 particle.validate()?;
396 }
397 Ok(work)
398 }
399}
400
401#[derive(Clone, Copy, Debug)]
403pub struct ParticleCollisions {
404 pub response: CollisionResponse,
406}
407
408impl ParticleGroupModifier for ParticleCollisions {
409 fn apply(
410 &self,
411 particles: &mut [Particle],
412 _: f32,
413 _: Time,
414 max_work: u64,
415 ) -> Result<u64, ValidationError> {
416 self.response.validate()?;
417 let radius = particles
418 .iter()
419 .fold(0.0_f32, |value, particle| value.max(particle.size))
420 * 2.0;
421 if radius == 0.0 {
422 return Ok(particles.len() as u64);
423 }
424 let pairs = super::neighbors::pairs(particles, f64::from(radius), max_work / 16)?;
425 let work = particles.len() as u64 + pairs.len() as u64 * 32;
426 if work > max_work {
427 return Err(limit("Particle contact work exhausted"));
428 }
429 for (first, second) in pairs {
430 let delta = [
431 particles[second].position[0] - particles[first].position[0],
432 particles[second].position[1] - particles[first].position[1],
433 ];
434 let distance = libm::hypotf(delta[0], delta[1]);
435 let minimum = particles[first].size + particles[second].size;
436 if distance >= minimum {
437 continue;
438 }
439 let normal = if distance == 0.0 {
440 [1.0, 0.0]
441 } else {
442 delta.map(|value| value / distance)
443 };
444 let relative = [
445 particles[second].velocity[0] - particles[first].velocity[0],
446 particles[second].velocity[1] - particles[first].velocity[1],
447 ];
448 let speed = relative[0] * normal[0] + relative[1] * normal[1];
449 for axis in 0..2 {
450 let correction = normal[axis] * (minimum - distance + 1e-5) * 0.5;
451 particles[first].position[axis] -= correction;
452 particles[second].position[axis] += correction;
453 if speed < 0.0 {
454 let impulse = normal[axis] * speed * (1.0 + self.response.restitution) * 0.5;
455 let friction =
456 (relative[axis] - speed * normal[axis]) * self.response.friction * 0.5;
457 particles[first].velocity[axis] += impulse + friction;
458 particles[second].velocity[axis] -= impulse + friction;
459 }
460 }
461 if self.response.kill_on_hit {
462 for index in [first, second] {
463 particles[index].lifetime = particles[index].age.max(f32::MIN_POSITIVE);
464 }
465 }
466 }
467 for particle in particles {
468 particle.validate()?;
469 }
470 Ok(work)
471 }
472
473 fn collisions(&self) -> bool {
474 true
475 }
476}