motion_content/particles/style.rs
1//! Built-in and custom particle presentation styles.
2
3use super::*;
4use motion_core::{Node, SceneContext, ValidationError};
5
6type CustomStyle =
7 Arc<dyn Fn(&[Particle], &SceneContext<'_>) -> Result<Node, ValidationError> + Send + Sync>;
8
9/// particle presentation. Built-ins share an instance protocol; custom
10/// callbacks run on CPU and return ordinary content or a validation error, never
11/// custom WGSL.
12#[derive(Clone)]
13pub enum ParticleStyle {
14 /// Smooth analytic circles; radius multiplies particle.size.
15 Dots {
16 /// Base local radius.
17 radius: f64,
18 },
19 /// Compact Gaussian halos with linear Plus compositing.
20 GlowDots {
21 /// Gaussian scale (support extends to three radii).
22 radius: f64,
23 },
24 /// Bilinear sprite instances, tinted by premultiplied particle color.
25 Sprites {
26 /// Lazy immutable texture.
27 image: motion_core::Handle<crate::ImageAsset>,
28 /// Full local sprite side.
29 size: f64,
30 /// Adds velocity direction to particle.rotation.
31 align_to_velocity: bool,
32 },
33 /// Eight actual historical segments per particle, not a velocity extrapolation.
34 Trails {
35 /// Historical interval.
36 length: Duration,
37 /// Local line width.
38 width: f64,
39 },
40 /// Analytic dots and bounded pairwise neighbor connections.
41 Plexus {
42 /// Dot radius.
43 radius: f64,
44 /// Maximum local neighbor separation.
45 max_distance: f64,
46 },
47 /// Texture-free analytic geometry.
48 Shape {
49 /// Base radius or half height.
50 radius: f64,
51 /// Coverage geometry.
52 shape: ParticleShape,
53 },
54 /// Depth-aware, energy-compensated aperture. Depth is `particle.custom[1]`.
55 Bokeh {
56 /// Base in-focus radius.
57 radius: f64,
58 /// Circle-of-confusion scale.
59 aperture: f32,
60 /// In-focus depth.
61 focus: f32,
62 /// Aperture silhouette.
63 shape: BokehShape,
64 /// Rim intensity in 0..=1.
65 ring: f32,
66 /// Edge hardness in 0..=1.
67 hardness: f32,
68 /// Finite nonnegative HDR multiplier.
69 highlight: f32,
70 },
71 /// A built-in or custom style with explicit batch options; see [`StyleOptions`].
72 Configured {
73 /// Underlying built-in or custom style.
74 style: Box<ParticleStyle>,
75 /// Batch-level presentation policy.
76 options: StyleOptions,
77 },
78 /// CPU-only presentation callback.
79 Custom(CustomStyle),
80}
81
82/// Explicit batch compositing, independent of primitive kind.
83pub type ParticleBlend = motion_core::instances::InstanceBlend;
84
85/// Built-in analytic particle silhouettes.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub enum ParticleShape {
88 /// Regular polygon with 3..=32 sides.
89 Polygon {
90 /// Number of regular sides, in 3..=32.
91 sides: u8,
92 },
93 /// Annulus with relative thickness in (0,1].
94 Ring {
95 /// Relative annulus width in (0,1].
96 thickness: f32,
97 },
98 /// Rounded velocity-oriented capsule, aspect in 1..=64.
99 Capsule {
100 /// Length-to-radius ratio in 1..=64.
101 aspect: f32,
102 },
103 /// Soft-edged circle, hardness in 0..=1.
104 Soft {
105 /// Edge hardness in 0..=1.
106 hardness: f32,
107 },
108 /// Rotated rectangle, aspect in (0,64].
109 Quad {
110 /// Horizontal-to-vertical half-extent ratio in (0,64].
111 aspect: f32,
112 },
113}
114
115/// Aperture geometry for depth-aware particles.
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub enum BokehShape {
118 /// Circular aperture.
119 Disc,
120 /// Regular aperture with 3..=32 blades.
121 Polygon(u8),
122}
123
124/// Deterministic velocity-aligned elongation.
125#[derive(Clone, Copy, Debug, PartialEq)]
126pub struct SpeedStretch {
127 /// Additional aspect per unit speed.
128 pub scale: f32,
129 /// Aspect ceiling in 1..=64.
130 pub maximum: f32,
131}
132
133/// Historical sampling shared by all particles at each requested boundary.
134#[derive(Clone)]
135pub struct TrailOptions {
136 /// Number of historical intervals, in 1..=64; the default is eight.
137 pub segments: u8,
138 /// Width along the historical interval; None preserves constant width.
139 pub taper: Option<ParticleCurve>,
140 /// Premultiplied fade along the interval.
141 pub fade: Option<ParticleCurve>,
142 /// Catmull-Rom interpolation with bounded adaptive subdivision.
143 pub smooth: bool,
144 /// Flat-ended continuous strip instead of rounded segments; with `smooth`, bends
145 /// are subdivided up to eight times per interval to close join gaps.
146 pub ribbon: bool,
147}
148
149impl Default for TrailOptions {
150 fn default() -> Self {
151 Self {
152 segments: 8,
153 taper: None,
154 fade: None,
155 smooth: false,
156 ribbon: false,
157 }
158 }
159}
160
161/// Optional policies grouped separately from each style's basic geometry.
162#[derive(Clone, Default)]
163pub struct StyleOptions {
164 /// Batch compositing; None selects the style's natural rule: additive Plus for
165 /// `GlowDots` and source-over otherwise.
166 pub blend: Option<ParticleBlend>,
167 /// Optional flipbook or atlas cell selection.
168 pub sheet: Option<SpriteSheet>,
169 /// Historical geometry policy.
170 pub trails: TrailOptions,
171 /// Optional velocity stretch.
172 pub stretch: Option<SpeedStretch>,
173 /// Local displacement per unit depth relative to focus.
174 pub parallax: [f32; 2],
175 /// Enables stable far-to-near sorting by (depth,index).
176 pub sort_depth: bool,
177 /// Animates Quad aspect and brightness using particle age and phase `custom[2]`.
178 pub flip: bool,
179}
180
181impl ParticleStyle {
182 fn configure(self, update: impl FnOnce(&mut StyleOptions)) -> Self {
183 let (style, mut options) = match self {
184 Self::Configured { style, options } => (style, options),
185 style => (Box::new(style), StyleOptions::default()),
186 };
187 update(&mut options);
188 Self::Configured { style, options }
189 }
190
191 /// Selects compositing for the entire batch, not individual GPU invocations.
192 pub fn blend(self, blend: ParticleBlend) -> Self {
193 self.configure(|options| options.blend = Some(blend))
194 }
195
196 /// Adds animation or atlas UVs to Sprites.
197 pub fn sheet(self, sheet: SpriteSheet) -> Self {
198 self.configure(|options| options.sheet = Some(sheet))
199 }
200
201 /// Configures segment count, endpoint width/color and optional smoothing/ribbon.
202 pub fn trails(self, trails: TrailOptions) -> Self {
203 self.configure(|options| options.trails = trails)
204 }
205
206 /// Aligns and stretches geometry along the velocity, with a finite ceiling.
207 pub fn stretch_by_speed(self, scale: f32, maximum: f32) -> Self {
208 self.configure(|options| options.stretch = Some(SpeedStretch { scale, maximum }))
209 }
210
211 /// Adds deterministic depth parallax and stable depth ordering.
212 pub fn parallax(self, offset: [f32; 2]) -> Self {
213 self.configure(|options| {
214 options.parallax = offset;
215 options.sort_depth = true;
216 })
217 }
218
219 /// Enables the analytic single-axis confetti flip.
220 pub fn flip(self) -> Self {
221 self.configure(|options| options.flip = true)
222 }
223}
224
225impl Default for ParticleStyle {
226 fn default() -> Self {
227 Self::Dots { radius: 2.0 }
228 }
229}