motion_content/particles/
render.rs1#[cfg(test)]
4use super::batch::SpatialNode;
5use super::*;
6use motion_core::{Node, Rect, SceneContext};
7
8#[cfg(test)]
9mod instance_protocol_tests;
10
11fn render(
12 style: &ParticleStyle,
13 context: &SceneContext<'_>,
14 states: Arc<[Particle]>,
15 mut sample: impl FnMut(Time) -> Result<Arc<[Particle]>, ValidationError>,
16) -> Result<Node, ValidationError> {
17 match style {
18 ParticleStyle::Configured { style, options } => {
19 if matches!(style.as_ref(), ParticleStyle::Trails { .. })
20 && options.blend.is_none()
21 && options.sheet.is_none()
22 && options.stretch.is_none()
23 && options.parallax == [0.0; 2]
24 && !options.sort_depth
25 && !options.flip
26 && options.trails.segments == 8
27 && options.trails.taper.is_none()
28 && options.trails.fade.is_none()
29 && !options.trails.smooth
30 && !options.trails.ribbon
31 {
32 return render(style, context, states, sample);
33 }
34 return super::extended::render(style, options, context, states, sample);
35 }
36 ParticleStyle::Shape { .. } | ParticleStyle::Bokeh { .. } => {
37 return super::extended::render(
38 style,
39 &StyleOptions::default(),
40 context,
41 states,
42 sample,
43 );
44 }
45 _ => {}
46 }
47 if let ParticleStyle::Custom(callback) = style {
48 return Ok(callback(&states, context));
49 }
50 let extent = match style {
51 ParticleStyle::Dots { radius }
52 | ParticleStyle::GlowDots { radius }
53 | ParticleStyle::Plexus { radius, .. } => *radius,
54 ParticleStyle::Sprites { size, .. } => *size,
55 ParticleStyle::Trails { width, .. } => *width,
56 _ => unreachable!(),
57 };
58 if !extent.is_finite() || !(0.0..=4096.0).contains(&extent) {
59 return Err(invalid(
60 "style",
61 "Particle style size must be finite in 0..=4096",
62 ));
63 }
64 if let ParticleStyle::Plexus { max_distance, .. } = style
65 && (!max_distance.is_finite() || *max_distance <= 0.0)
66 {
67 return Err(invalid(
68 "distance",
69 "Plexus distance must be finite and positive",
70 ));
71 }
72 if states.is_empty() && !matches!(style, ParticleStyle::Trails { .. }) {
73 return Ok(Node::empty());
74 }
75 let (kind, radius, width, align, sprite) = match style {
76 ParticleStyle::Dots { radius } => (0, *radius, 1.0, false, None),
77 ParticleStyle::GlowDots { radius } => (1, *radius, 1.0, false, None),
78 ParticleStyle::Sprites {
79 image,
80 size,
81 align_to_velocity,
82 } => match image.get() {
83 Ok(image) => (2, *size * 0.5, 1.0, *align_to_velocity, Some(image)),
84 Err(_) => {
85 return Ok(Node::from(
86 crate::Image::new(image.clone()).size(*size, *size),
87 ));
88 }
89 },
90 ParticleStyle::Trails { width, .. } => (3, 1.0, *width, false, None),
91 ParticleStyle::Plexus { radius, .. } => (4, *radius, 1.0, false, None),
92 _ => unreachable!(),
93 };
94 if !radius.is_finite() || radius < 0.0 || !width.is_finite() || width < 0.0 {
95 return Err(invalid("style", "Invalid particle style geometry"));
96 }
97 let mut instances = Vec::new();
98 let mut append = |particle: Particle, previous: [f32; 2], line: bool| {
99 if instances.len() >= MAX_INSTANCE_COUNT {
100 return Err(limit("Particle instance allowance exhausted"));
101 }
102 instances
103 .try_reserve(1)
104 .map_err(|_| limit("Particle instance allocation failed"))?;
105 instances.push(Instance::new(particle, previous, line));
106 Ok(())
107 };
108 if let ParticleStyle::Trails { length, .. } = style {
109 if *length <= Time::ZERO || length.as_seconds() > 60.0 {
110 return Err(invalid("trail", "Trail duration must be in (0,60] seconds"));
111 }
112 let mut previous = sample(context.time() - *length)?;
113 for segment in 1..=8 {
114 let time = context.time() - *length + Time::from_ticks(length.ticks() * segment / 8);
115 let current = sample(time)?;
116 let lookup = previous
117 .iter()
118 .map(|particle| (particle.index, particle))
119 .collect::<std::collections::BTreeMap<_, _>>();
120 for particle in current.iter() {
121 if let Some(before) = lookup.get(&particle.index) {
122 append(*particle, before.position, true)?;
123 }
124 }
125 previous = current;
126 }
127 } else {
128 for particle in states.iter() {
129 append(*particle, particle.position, false)?;
130 }
131 }
132 if let ParticleStyle::Plexus { max_distance, .. } = style {
133 for (first, second) in super::neighbors::pairs(&states, *max_distance, 100_000_000)? {
134 append(states[first], states[second].position, true)?;
135 }
136 }
137 let mut bounds = Rect::default();
138 for (index, instance) in instances.iter().enumerate() {
139 let particle = instance.particle;
140 let reach =
141 (radius * f64::from(particle.size) * if kind == 1 { 3.0 } else { 2.0 }).max(width);
142 let left = f64::from(particle.position[0].min(instance.previous[0])) - reach;
143 let top = f64::from(particle.position[1].min(instance.previous[1])) - reach;
144 let right = f64::from(particle.position[0].max(instance.previous[0])) + reach;
145 let bottom = f64::from(particle.position[1].max(instance.previous[1])) + reach;
146 let extent = Rect::new(left, top, right, bottom);
147 bounds = if index == 0 {
148 extent
149 } else {
150 bounds.union(extent)
151 };
152 }
153 let mut batch = ParticleBatch {
154 instances: instances.into(),
155 kind,
156 radius: radius as f32,
157 width: width as f32,
158 align,
159 sprite,
160 bounds,
161 index: Arc::from([]),
162 };
163 batch.validate()?;
164 batch.index = batch.build_spatial_index();
165 Ok(Node::from(batch))
166}
167
168impl ParticleSimulation {
169 pub fn style(mut self, style: ParticleStyle) -> Self {
171 self.style = Arc::new(style);
172 self
173 }
174
175 pub fn try_render(&self, context: &SceneContext<'_>) -> Result<Node, ValidationError> {
177 let duration = context.clip_duration();
178 let mut cursor = super::simulation::SampleCursor::default();
181 let states = self.sample_with(&mut cursor, context.time(), duration)?;
182 render(&self.style, context, states, |time| {
183 self.sample_with(&mut cursor, time, duration)
184 })
185 }
186}
187
188impl ParticleTrajectory {
189 pub fn style(mut self, style: ParticleStyle) -> Self {
191 self.style = Arc::new(style);
192 self
193 }
194
195 pub fn try_render(&self, context: &SceneContext<'_>) -> Result<Node, ValidationError> {
197 render(&self.style, context, self.sample(context.time())?, |time| {
198 self.sample(time)
199 })
200 }
201}
202
203impl motion_core::authoring::Scene for ParticleSimulation {
204 fn render(&self, context: &SceneContext<'_>) -> Node {
205 self.try_render(context)
206 .unwrap_or_else(motion_core::authoring::transition::diagnostic)
207 }
208
209 fn duration(&self) -> Option<Duration> {
210 self.natural_duration()
211 }
212}
213
214impl motion_core::authoring::Scene for ParticleTrajectory {
215 fn render(&self, context: &SceneContext<'_>) -> Node {
216 self.try_render(context)
217 .unwrap_or_else(motion_core::authoring::transition::diagnostic)
218 }
219
220 fn duration(&self) -> Option<Duration> {
221 self.model.duration()
222 }
223}
224
225#[cfg(test)]
226mod tests;