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