motion_content/particles/
trajectory.rs1use super::*;
4use motion_core::animation::value_noise;
5use motion_core::{Point, Vector2};
6use std::ops::Range;
7
8pub trait TrajectoryModel: Send + Sync {
10 fn duration(&self) -> Option<Duration> {
12 None
13 }
14
15 fn spawned(&self, from: Time, to: Time) -> Range<u64>;
17
18 fn birth_time(&self, index: u64) -> Time;
20
21 fn state(&self, index: u64, age_seconds: f32) -> Result<Option<Particle>, ValidationError>;
23}
24
25#[derive(Clone, Debug, PartialEq)]
26struct TrajectoryDefinition {
27 pub(crate) kind: u32,
28 pub(crate) parameters: [f32; 8],
29 pub(crate) count: u64,
30 pub(crate) lifetime: f32,
31 pub(crate) seed: u64,
32 pub(crate) initial: Arc<[[f32; 4]]>,
33}
34
35impl TrajectoryDefinition {
36 pub(crate) fn initial(&self, index: u64) -> Result<Particle, ValidationError> {
37 if self.count > MAX_INSTANCE_COUNT as u64
38 || !self.lifetime.is_finite()
39 || self.lifetime <= 0.0
40 || self.lifetime > 3600.0
41 || self.parameters.iter().any(|value| !value.is_finite())
42 {
43 return Err(invalid(
44 "trajectory",
45 "Invalid builtin trajectory parameters/count/lifetime",
46 ));
47 }
48 if (self.kind == 3 && self.parameters[2] < 0.0)
49 || (self.kind == 4 && (self.parameters[4] < 0.0 || self.parameters[5] < 0.0))
50 {
51 return Err(invalid(
52 "trajectory",
53 "Radius, noise amplitude and frequency must be nonnegative",
54 ));
55 }
56 let mut random = Random::from_key((self.seed, index));
57 let mut particle = Particle {
58 index,
59 lifetime: self.lifetime,
60 ..Default::default()
61 };
62 match self.kind {
63 1 => {
64 particle.position = [self.parameters[0], self.parameters[1]];
65 let angle = random.next_float() * std::f64::consts::TAU;
66 let speed = f64::from(self.parameters[6])
67 + (f64::from(self.parameters[7]) - f64::from(self.parameters[6]))
68 * random.next_float();
69 particle.velocity = [
70 (libm::cos(angle) * speed) as f32,
71 (libm::sin(angle) * speed) as f32,
72 ];
73 }
74 2 => {
75 let points = self
76 .initial
77 .get(index as usize)
78 .ok_or_else(|| invalid("points", "Converge point index missing"))?;
79 particle.position = [points[0], points[1]];
80 particle.custom = [points[2], points[3], 0.0, 0.0];
81 }
82 3 => {
83 particle.custom[0] = (random.next_float() * std::f64::consts::TAU) as f32;
84 }
85 4 => {
86 particle.position = [self.parameters[0], self.parameters[1]];
87 particle.velocity = [self.parameters[2], self.parameters[3]];
88 particle.custom[0] = (random.next_float() * 32.0) as f32;
89 particle.custom[1] = (random.next_float() * 32.0) as f32;
90 }
91 _ => return Err(invalid("model", "Unknown builtin trajectory")),
92 }
93 particle.validate()?;
94 Ok(particle)
95 }
96
97 pub(crate) fn evaluate(
98 &self,
99 mut particle: Particle,
100 age: f32,
101 ) -> Result<Particle, ValidationError> {
102 let time = f64::from(age);
103 let values = self.parameters.map(f64::from);
104 match self.kind {
105 1 => {
106 let drag = values[4];
107 if drag < 0.0 || values[6] < 0.0 || values[7] < values[6] {
108 return Err(invalid(
109 "ballistic",
110 "Require nonnegative drag and ordered speeds",
111 ));
112 }
113 let product = drag * time;
114 let (decay, advance, gravity) = if product.abs() < 0.01 {
115 let square = product * product;
116 let cube = square * product;
117 let fourth = cube * product;
118 (
119 libm::exp(-product),
120 time * (1.0 - product * 0.5 + square / 6.0 - cube / 24.0 + fourth / 120.0),
121 time * time
122 * (0.5 - product / 6.0 + square / 24.0 - cube / 120.0 + fourth / 720.0),
123 )
124 } else {
125 let advance = -libm::expm1(-product) / drag;
126 (libm::exp(-product), advance, (time - advance) / drag)
127 };
128 for axis in 0..2 {
129 let velocity = f64::from(particle.velocity[axis]);
130 particle.position[axis] = (f64::from(particle.position[axis])
131 + velocity * advance
132 + values[2 + axis] * gravity)
133 as f32;
134 particle.velocity[axis] =
135 (velocity * decay + values[2 + axis] * advance) as f32;
136 }
137 }
138 2 => {
139 let progress = (time / f64::from(self.lifetime)).clamp(0.0, 1.0);
140 let amount = progress * progress * (3.0 - 2.0 * progress);
141 let derivative = 6.0 * progress * (1.0 - progress) / f64::from(self.lifetime);
142 for axis in 0..2 {
143 let start = f64::from(particle.position[axis]);
144 let delta = f64::from(particle.custom[axis]) - start;
145 particle.position[axis] = (start + delta * amount) as f32;
146 particle.velocity[axis] = (delta * derivative) as f32;
147 }
148 }
149 3 => {
150 let angle = f64::from(particle.custom[0]) + values[3] * time;
151 particle.position = [
152 (values[0] + values[2] * libm::cos(angle)) as f32,
153 (values[1] + values[2] * libm::sin(angle)) as f32,
154 ];
155 particle.velocity = [
156 (-values[2] * values[3] * libm::sin(angle)) as f32,
157 (values[2] * values[3] * libm::cos(angle)) as f32,
158 ];
159 }
160 4 => {
161 for axis in 0..2 {
162 let phase = f64::from(particle.custom[axis]);
163 let (value, derivative) = value_noise(
164 phase + time * values[5],
165 particle.index as u32 + axis as u32,
166 );
167 let (origin, _) = value_noise(phase, particle.index as u32 + axis as u32);
168 particle.position[axis] = (f64::from(particle.position[axis])
169 + f64::from(particle.velocity[axis]) * time
170 + values[4] * (value - origin))
171 as f32;
172 particle.velocity[axis] = (f64::from(particle.velocity[axis])
173 + values[4] * values[5] * derivative)
174 as f32;
175 }
176 }
177 _ => unreachable!(),
178 }
179 particle.age = age;
180 particle.validate()?;
181 Ok(particle)
182 }
183}
184
185#[derive(Clone)]
187pub struct ParticleTrajectory {
188 pub(crate) model: Arc<dyn TrajectoryModel>,
189 pub(crate) style: Arc<ParticleStyle>,
190}
191
192impl ParticleTrajectory {
193 pub fn new(model: impl TrajectoryModel + 'static) -> Self {
195 Self {
196 model: Arc::new(model),
197 style: Arc::new(ParticleStyle::default()),
198 }
199 }
200
201 pub fn sample(&self, time: Time) -> Result<Arc<[Particle]>, ValidationError> {
203 if time < Time::ZERO {
204 return Ok(Arc::from([]));
205 }
206 let end = time
207 .ticks()
208 .checked_add(1)
209 .map(Time::from_ticks)
210 .ok_or_else(|| invalid("time", "Trajectory query overflow"))?;
211 let range = self.model.spawned(Time::ZERO, end);
212 if range.end < range.start || range.end - range.start > MAX_INSTANCE_COUNT as u64 {
213 return Err(limit(
214 "Trajectory birth range exceeds the instance allowance",
215 ));
216 }
217 let mut particles = Vec::new();
218 particles
219 .try_reserve_exact((range.end - range.start) as usize)
220 .map_err(|_| limit("Trajectory state allocation failed"))?;
221 for index in range {
222 let birth = self.model.birth_time(index);
223 if birth < Time::ZERO || birth > time {
224 return Err(invalid(
225 "birth",
226 "Custom model returned an invalid birth time",
227 ));
228 }
229 if let Some(mut particle) = self
230 .model
231 .state(index, (time - birth).as_seconds() as f32)?
232 {
233 particle.index = index;
234 particle.validate()?;
235 if particle.age < particle.lifetime {
236 particles.push(particle);
237 }
238 }
239 }
240 Ok(particles.into())
241 }
242}
243
244fn model(kind: u32, center: Point, count: u64) -> TrajectoryDefinition {
245 TrajectoryDefinition {
246 kind,
247 parameters: [
248 center.x as f32,
249 center.y as f32,
250 0.0,
251 0.0,
252 0.0,
253 0.0,
254 80.0,
255 160.0,
256 ],
257 count,
258 lifetime: 3.0,
259 seed: 0,
260 initial: Arc::from([]),
261 }
262}
263
264macro_rules! common {
265 ($name:ident) => {
266 impl $name {
267 pub fn seed(mut self, seed: u64) -> Self {
269 self.0.seed = seed;
270 self
271 }
272 pub fn lifetime(mut self, lifetime: Duration) -> Self {
274 self.0.lifetime = lifetime.as_seconds() as f32;
275 self
276 }
277 }
278 impl TrajectoryModel for $name {
279 fn spawned(&self, from: Time, to: Time) -> Range<u64> {
280 if from <= Time::ZERO && to > Time::ZERO {
281 0..self.0.count
282 } else {
283 0..0
284 }
285 }
286 fn birth_time(&self, _: u64) -> Time {
287 Time::ZERO
288 }
289 fn state(&self, index: u64, age: f32) -> Result<Option<Particle>, ValidationError> {
290 let initial = self.0.initial(index)?;
291 if index >= self.0.count || !age.is_finite() {
292 return Err(invalid("age", "Invalid trajectory ID/age"));
293 }
294 if age < 0.0 || age >= self.0.lifetime {
295 return Ok(None);
296 }
297 Ok(Some(self.0.evaluate(initial, age)?))
298 }
299 fn duration(&self) -> Option<Duration> {
300 if self.0.lifetime.is_finite() && self.0.lifetime > 0.0 && self.0.lifetime <= 3600.0
301 {
302 Some(Time::from_seconds(f64::from(self.0.lifetime)))
303 } else {
304 None
305 }
306 }
307 }
308 };
309}
310
311#[derive(Clone, Debug)]
313pub struct Ballistic(TrajectoryDefinition);
314
315impl Ballistic {
316 pub fn burst(center: Point, count: u64) -> Self {
318 Self(model(1, center, count))
319 }
320
321 pub fn gravity(mut self, gravity: Vector2) -> Self {
323 self.0.parameters[2] = gravity.x as f32;
324 self.0.parameters[3] = gravity.y as f32;
325 self
326 }
327
328 pub fn drag(mut self, drag: f64) -> Self {
330 self.0.parameters[4] = drag as f32;
331 self
332 }
333
334 pub fn speed(mut self, minimum: f64, maximum: f64) -> Self {
336 self.0.parameters[6] = minimum as f32;
337 self.0.parameters[7] = maximum as f32;
338 self
339 }
340}
341common!(Ballistic);
342
343#[derive(Clone, Debug)]
345pub struct Converge(TrajectoryDefinition);
346
347impl Converge {
348 pub fn new(from: &[Point], to: &[Point], duration: Duration) -> Result<Self, ValidationError> {
350 if from.is_empty() || to.is_empty() || from.len().max(to.len()) > MAX_INSTANCE_COUNT {
351 return Err(invalid(
352 "points",
353 "Converge requires nonempty point sets within the instance allowance",
354 ));
355 }
356 let mut definition = model(2, Point::ORIGIN, from.len().max(to.len()) as u64);
357 definition.lifetime = duration.as_seconds() as f32;
358 definition.initial = (0..definition.count as usize)
359 .map(|index| {
360 [
361 from[index % from.len()].x as f32,
362 from[index % from.len()].y as f32,
363 to[index % to.len()].x as f32,
364 to[index % to.len()].y as f32,
365 ]
366 })
367 .collect::<Vec<_>>()
368 .into();
369 Ok(Self(definition))
370 }
371}
372common!(Converge);
373
374#[derive(Clone, Debug)]
376pub struct Orbit(TrajectoryDefinition);
377
378impl Orbit {
379 pub fn new(center: Point, radius: f64, count: u64) -> Self {
381 let mut definition = model(3, center, count);
382 definition.parameters[2] = radius as f32;
383 definition.parameters[3] = 1.0;
384 Self(definition)
385 }
386
387 pub fn angular_velocity(mut self, velocity: f64) -> Self {
389 self.0.parameters[3] = velocity as f32;
390 self
391 }
392}
393common!(Orbit);
394
395#[derive(Clone, Debug)]
397pub struct NoiseDrift(TrajectoryDefinition);
398
399impl NoiseDrift {
400 pub fn new(origin: Point, count: u64) -> Self {
402 let mut definition = model(4, origin, count);
403 definition.parameters[4] = 20.0;
404 definition.parameters[5] = 1.0;
405 Self(definition)
406 }
407
408 pub fn velocity(mut self, velocity: Vector2) -> Self {
410 self.0.parameters[2] = velocity.x as f32;
411 self.0.parameters[3] = velocity.y as f32;
412 self
413 }
414
415 pub fn noise(mut self, amplitude: f64, frequency: f64) -> Self {
417 self.0.parameters[4] = amplitude as f32;
418 self.0.parameters[5] = frequency as f32;
419 self
420 }
421}
422common!(NoiseDrift);
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use motion_core::TimeLiteral;
428
429 #[test]
430 fn ballistic_solves_gravity_and_drag_together_with_stable_zero_limit() -> motion_core::Result<()>
431 {
432 for drag in [0.0, 1e-9, 0.8] {
433 let model = Ballistic::burst(Point::new(2.0, 3.0), 1)
434 .seed(42)
435 .speed(5.0, 5.0)
436 .gravity(Vector2::new(0.0, 10.0))
437 .drag(drag);
438 let start = model.state(0, 0.0)?.unwrap();
439 let actual = model.state(0, 1.0)?.unwrap();
440 let mut position = start.position.map(f64::from);
441 let mut velocity = start.velocity.map(f64::from);
442 let step = 1.0 / 20000.0;
443 for _ in 0..20000 {
444 for axis in 0..2 {
445 let force = if axis == 1 { 10.0 } else { 0.0 };
446 let acceleration = force - drag * velocity[axis];
447 position[axis] += velocity[axis] * step + 0.5 * acceleration * step * step;
448 velocity[axis] += acceleration * step;
449 }
450 }
451 for axis in 0..2 {
452 assert!((position[axis] - f64::from(actual.position[axis])).abs() < 0.001);
453 assert!((velocity[axis] - f64::from(actual.velocity[axis])).abs() < 0.001);
454 }
455 }
456 let converge = Converge::new(&[Point::ORIGIN], &[Point::new(10.0, 20.0)], 2.seconds())?;
457 assert_eq!(converge.state(0, 1.0)?.unwrap().position, [5.0, 10.0]);
458 let orbit = Orbit::new(Point::ORIGIN, 5.0, 1);
459 let state = orbit.state(0, 1.0)?.unwrap();
460 assert!(
461 (state.position[0] * state.position[0] + state.position[1] * state.position[1] - 25.0)
462 .abs()
463 < 0.0001
464 );
465 let noise = NoiseDrift::new(Point::ORIGIN, 1).velocity(Vector2::new(1.0, 0.0));
466 let before = noise.state(0, 0.999)?.unwrap();
467 let after = noise.state(0, 1.001)?.unwrap();
468 let exact = noise.state(0, 1.0)?.unwrap();
469 for axis in 0..2 {
470 assert!(
471 ((after.position[axis] - before.position[axis]) / 0.002 - exact.velocity[axis])
472 .abs()
473 < 0.02
474 );
475 }
476 let trajectory = ParticleTrajectory::new(orbit);
477 assert_eq!(
478 trajectory.sample(1.seconds())?,
479 trajectory.sample(1.seconds())?
480 );
481 Ok(())
482 }
483}