1use motion_core::{Duration, Random, Time, ValidationError};
42use std::sync::{Arc, OnceLock};
43
44mod batch;
45mod collision;
46mod components;
47mod curves;
48mod events;
49mod extended;
50mod forces;
51mod neighbors;
52pub mod presets;
53mod render;
54mod sprites;
55mod style;
56#[cfg(test)]
57mod tests;
58
59use batch::Instance;
60
61mod simulation;
62mod trajectory;
63
64pub use batch::ParticleBatch;
65pub use collision::{
66 CircleCollider, CollisionResponse, PolygonCollider, SdfCollider, SegmentCollider,
67 SignedDistanceGrid,
68};
69pub use components::{
70 Attractor, BounceBox, BurstEmitter, ColorOverLifetime, DiskEmitter, Drag, Gravity,
71 ImageDensity, ImageEmitter, PathEmitter, PointEmitter, RectEmitter, Seek, SizeOverLifetime,
72 SurfaceEmitter, Vortex, Wind,
73};
74pub use curves::{
75 ColorCurve, ColorField, FieldCoordinate, IntensityOverLifetime, ParticleCurve, Temperature,
76};
77pub use events::{Inherit, ParticleEvent, ParticleEventKind, SubEmitter};
78pub use forces::{
79 Boids, CurlNoise, FlowField, FlowSource, ForceFalloff, ParticleCollisions, RadialForce, Spring,
80};
81pub use simulation::{ParticleSimulation, SimulationLimits, SimulationStats};
82pub use sprites::{ProceduralSprite, SpriteAtlas, SpriteSheet, SpriteSheetMode};
83pub use style::{
84 BokehShape, ParticleBlend, ParticleShape, ParticleStyle, SpeedStretch, StyleOptions,
85 TrailOptions,
86};
87pub use trajectory::{Ballistic, Converge, NoiseDrift, Orbit, ParticleTrajectory, TrajectoryModel};
88
89use motion_core::instances::MAX_INSTANCE_COUNT;
90
91const MAX_PARTICLE_SIZE: f32 = 4096.0;
93
94const MAX_LIFETIME_SECONDS: f32 = 3600.0;
96
97#[repr(C)]
99#[derive(Clone, Copy, Debug, PartialEq)]
100#[cfg_attr(feature = "gpu", derive(bytemuck::Pod, bytemuck::Zeroable))]
101pub struct Particle {
102 pub position: [f32; 2],
104 pub velocity: [f32; 2],
106 pub color: [f32; 4],
108 pub size: f32,
110 pub rotation: f32,
112 pub age: f32,
114 pub lifetime: f32,
116 pub index: u64,
118 pub custom: [f32; 4],
120}
121
122impl Default for Particle {
123 fn default() -> Self {
124 Self {
125 position: [0.0; 2],
126 velocity: [0.0; 2],
127 color: [1.0; 4],
128 size: 1.0,
129 rotation: 0.0,
130 age: 0.0,
131 lifetime: 1.0,
132 index: 0,
133 custom: [0.0; 4],
134 }
135 }
136}
137
138impl Particle {
139 pub(crate) fn validate(&self) -> Result<(), ValidationError> {
140 let finite = |values: &[f32]| values.iter().all(|value| value.is_finite());
143 if !(finite(&self.position)
144 && finite(&self.velocity)
145 && finite(&self.color)
146 && finite(&self.custom)
147 && finite(&[self.size, self.rotation, self.age, self.lifetime]))
148 || self.size < 0.0
149 || self.size > MAX_PARTICLE_SIZE
150 || self.age < 0.0
151 || self.lifetime <= 0.0
152 || self.lifetime > MAX_LIFETIME_SECONDS
153 || !(0.0..=1.0).contains(&self.color[3])
154 || (self.color[3] == 0.0 && self.color[..3].iter().any(|value| *value != 0.0))
155 {
156 return Err(invalid(
157 "state",
158 "Invalid finite particle geometry, lifetime or premultiplied color",
159 ));
160 }
161 Ok(())
162 }
163}
164
165pub trait ParticleEmitter: Send + Sync {
169 fn accepts_event(&self, _: &ParticleEvent) -> bool {
171 true
172 }
173
174 fn emit(
176 &self,
177 step: u64,
178 time: Time,
179 random: &mut Random,
180 output: &mut Vec<Particle>,
181 ) -> Result<(), ValidationError>;
182}
183
184pub trait ParticleModifier: Send + Sync {
186 fn apply(
188 &self,
189 particle: &mut Particle,
190 step_seconds: f32,
191 time: Time,
192 random: &mut Random,
193 ) -> Result<(), ValidationError>;
194
195 fn resets_color(&self) -> bool {
198 false
199 }
200
201 fn work(&self) -> u64 {
203 1
204 }
205
206 fn apply_event(
208 &self,
209 particle: &mut Particle,
210 step_seconds: f32,
211 time: Time,
212 random: &mut Random,
213 ) -> Result<bool, ValidationError> {
214 self.apply(particle, step_seconds, time, random)?;
215 Ok(false)
216 }
217}
218
219pub trait ParticleGroupModifier: Send + Sync {
221 fn apply(
223 &self,
224 particles: &mut [Particle],
225 step: f32,
226 time: Time,
227 max_work: u64,
228 ) -> Result<u64, ValidationError>;
229
230 fn collisions(&self) -> bool {
232 false
233 }
234}
235
236fn invalid(field: &str, message: &str) -> ValidationError {
237 ValidationError::invalid("particles", field, message)
238}
239
240fn limit(message: &str) -> ValidationError {
241 ValidationError::limit("particles", "budget", message)
242}