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, 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}