use motion_core::{Duration, Random, Time, ValidationError};
use std::sync::{Arc, OnceLock};
mod batch;
mod collision;
mod components;
mod curves;
mod events;
mod extended;
mod forces;
mod neighbors;
pub mod presets;
mod render;
mod sprites;
mod style;
#[cfg(test)]
mod tests;
use batch::Instance;
mod simulation;
mod trajectory;
pub use batch::ParticleBatch;
pub use collision::{
CircleCollider, CollisionResponse, PolygonCollider, SdfCollider, SegmentCollider,
SignedDistanceGrid,
};
pub use components::{
Attractor, BounceBox, BurstEmitter, ColorOverLifetime, DiskEmitter, Drag, Gravity,
ImageDensity, ImageEmitter, PathEmitter, PointEmitter, RectEmitter, Seek, SizeOverLifetime,
SurfaceEmitter, Vortex, Wind,
};
pub use curves::{
ColorCurve, ColorField, FieldCoordinate, IntensityOverLifetime, ParticleCurve, Temperature,
};
pub use events::{Inherit, ParticleEvent, ParticleEventKind, SubEmitter};
pub use forces::{
Boids, CurlNoise, FlowField, FlowSource, ForceFalloff, ParticleCollisions, RadialForce, Spring,
};
pub use simulation::{ParticleSimulation, SimulationLimits, SimulationStats};
pub use sprites::{ProceduralSprite, SpriteAtlas, SpriteSheet, SpriteSheetMode};
pub use style::{
BokehShape, ParticleBlend, ParticleShape, ParticleStyle, SpeedStretch, StyleOptions,
TrailOptions,
};
pub use trajectory::{Ballistic, Converge, NoiseDrift, Orbit, ParticleTrajectory, TrajectoryModel};
use motion_core::instances::MAX_INSTANCE_COUNT;
const MAX_PARTICLE_SIZE: f32 = 4096.0;
const MAX_LIFETIME_SECONDS: f32 = 3600.0;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "gpu", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Particle {
pub position: [f32; 2],
pub velocity: [f32; 2],
pub color: [f32; 4],
pub size: f32,
pub rotation: f32,
pub age: f32,
pub lifetime: f32,
pub index: u64,
pub custom: [f32; 4],
}
impl Default for Particle {
fn default() -> Self {
Self {
position: [0.0; 2],
velocity: [0.0; 2],
color: [1.0; 4],
size: 1.0,
rotation: 0.0,
age: 0.0,
lifetime: 1.0,
index: 0,
custom: [0.0; 4],
}
}
}
impl Particle {
pub(crate) fn validate(&self) -> Result<(), ValidationError> {
let finite = |values: &[f32]| values.iter().all(|value| value.is_finite());
if !(finite(&self.position)
&& finite(&self.velocity)
&& finite(&self.color)
&& finite(&self.custom)
&& finite(&[self.size, self.rotation, self.age, self.lifetime]))
|| self.size < 0.0
|| self.size > MAX_PARTICLE_SIZE
|| self.age < 0.0
|| self.lifetime <= 0.0
|| self.lifetime > MAX_LIFETIME_SECONDS
|| !(0.0..=1.0).contains(&self.color[3])
|| (self.color[3] == 0.0 && self.color[..3].iter().any(|value| *value != 0.0))
{
return Err(invalid(
"state",
"Invalid finite particle geometry, lifetime or premultiplied color",
));
}
Ok(())
}
}
pub trait ParticleEmitter: Send + Sync {
fn accepts_event(&self, _: &ParticleEvent) -> bool {
true
}
fn emit(
&self,
step: u64,
time: Time,
random: &mut Random,
output: &mut Vec<Particle>,
) -> Result<(), ValidationError>;
}
pub trait ParticleModifier: Send + Sync {
fn apply(
&self,
particle: &mut Particle,
step_seconds: f32,
time: Time,
random: &mut Random,
) -> Result<(), ValidationError>;
fn resets_color(&self) -> bool {
false
}
fn work(&self) -> u64 {
1
}
fn apply_event(
&self,
particle: &mut Particle,
step_seconds: f32,
time: Time,
random: &mut Random,
) -> Result<bool, ValidationError> {
self.apply(particle, step_seconds, time, random)?;
Ok(false)
}
}
pub trait ParticleGroupModifier: Send + Sync {
fn apply(
&self,
particles: &mut [Particle],
step: f32,
time: Time,
max_work: u64,
) -> Result<u64, ValidationError>;
fn collisions(&self) -> bool {
false
}
}
fn invalid(field: &str, message: &str) -> ValidationError {
ValidationError::invalid("particles", field, message)
}
fn limit(message: &str) -> ValidationError {
ValidationError::limit("particles", "budget", message)
}