Skip to main content

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}