Skip to main content

primase_core/
particles.rs

1//! `core.particles`: a particle effect as a source states it, in game-neutral terms.
2//!
3//! One component on an entity placed by its [`Transform`]: a tree of [`Emitter`]s that follow
4//! the entity's frame (or their parents'), emit particles on a schedule from a shape, move them by a law and draw each as one
5//! of a few forms ([`Draw`]). Everything a renderer needs is here and in game-neutral words: the curves are constants,
6//! steps, lines or cubic Beziers of a stated [`Argument`] (the particle's age, the emitter's active time, the time the particle
7//! was emitted), their random variation is seeded and per-stream, blends are the standard few, orientations are named by what
8//! they face. A source's own dialect (an engine's curve flavours, its units, its enumerations) is lowered into these by the
9//! source's pass, which counts what it could not carry ([`ParticleSystem::lost`]); no renderer needs to know the source.
10//!
11//! **Determinism.** A system's picture at a time is a function of its description, its seed and its [`Input`]s: random draws
12//! are keyed by `(seed, emitter, scope index, stream)` through [`unit()`], never by the order particles are
13//! processed in, so a renderer and a test that step the same description agree.
14//!
15//! **Units.** Metres, seconds, degrees (angles that a source gives in other units are converted by its pass), linear colour.
16//! Angles are degrees because that is what the sources store; the renderer converts.
17
18use serde::Serialize;
19use serde_json::Value as Json;
20
21use crate::{Component, Entity, JsonError, Transform, World};
22
23mod motion;
24pub use motion::{MotionState, Mover};
25
26/// What a [`Value`] is a function of.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
28pub enum Argument {
29    /// Nothing: the curve is read at 0.
30    Constant,
31    /// Seconds since the particle was emitted.
32    Age,
33    /// Seconds since the emitter became active (after its delay, or when its state was entered).
34    ActiveTime,
35    /// The emitter's active time at which the particle was emitted; constant for the particle's life.
36    EmissionTime,
37}
38
39/// How a [`Curve`] runs between its keys.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
41pub enum Interpolation {
42    /// Holds each key's value until the next key.
43    Step,
44    /// Joins the keys by straight lines.
45    Linear,
46    /// Joins the keys by cubic Beziers whose time handles lie a third of the way along the segment, so a segment from key
47    /// `a` to key `b` of duration `d` is, for `u = (x - a.at) / d`, the cubic through `a.value`, `a.value + a.out`,
48    /// `b.value - b.in` and `b.value` in `u` (handles are offsets in value, per component).
49    Bezier,
50}
51
52/// One key of a [`Curve`].
53#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
54pub struct Key {
55    /// The argument value of the key, seconds, ascending along the curve.
56    pub at: f32,
57    /// The value at the key, in the curve's components (the rest 0).
58    pub value: [f32; 4],
59    /// For [`Interpolation::Bezier`]: the value offset of the incoming handle (subtracted from this key's value to give the
60    /// segment's second control point); 0 otherwise.
61    pub handle_in: [f32; 4],
62    /// For [`Interpolation::Bezier`]: the value offset of the outgoing handle.
63    pub handle_out: [f32; 4],
64}
65
66/// A function of one argument with one to four components.
67#[derive(Clone, Debug, PartialEq, Serialize)]
68pub struct Curve {
69    /// The number of components (1 to 4).
70    pub width: u8,
71    /// How it runs between keys.
72    pub interpolation: Interpolation,
73    /// Whether the argument wraps at the last key's `at` (the first key then follows it). A loop of no duration is its first
74    /// key's value.
75    pub looped: bool,
76    /// At least one key; a one-key curve is a constant.
77    pub keys: Vec<Key>,
78}
79
80impl Curve {
81    /// A constant of one to four components.
82    pub fn constant(values: &[f32]) -> Curve {
83        let mut value = [0.0; 4];
84        for (slot, v) in value.iter_mut().zip(values) {
85            *slot = *v;
86        }
87        Curve {
88            width: values.len().clamp(1, 4) as u8,
89            interpolation: Interpolation::Linear,
90            looped: false,
91            keys: vec![Key {
92                at: 0.0,
93                value,
94                handle_in: [0.0; 4],
95                handle_out: [0.0; 4],
96            }],
97        }
98    }
99
100    /// The curve at argument `x`: before the first key the first value, after the last key the last value (or wrapped, for a
101    /// loop). Components past [`Curve::width`] are 0.
102    pub fn value_at(&self, x: f32) -> [f32; 4] {
103        let keys = &self.keys;
104        let (Some(first), Some(last)) = (keys.first(), keys.last()) else {
105            return [0.0; 4];
106        };
107        let x = if self.looped {
108            if last.at <= 0.0 {
109                return first.value;
110            }
111            x.rem_euclid(last.at)
112        } else {
113            x
114        };
115        if keys.len() == 1 || x <= first.at {
116            return first.value;
117        }
118        if x >= last.at {
119            return last.value;
120        }
121        // the segment [a, b] with a.at <= x < b.at
122        let b = keys.partition_point(|k| k.at <= x);
123        let (a, b) = (&keys[b - 1], &keys[b]);
124        let d = b.at - a.at;
125        if d <= 0.0 {
126            return b.value;
127        }
128        let u = (x - a.at) / d;
129        let mut out = [0.0; 4];
130        for (c, slot) in out
131            .iter_mut()
132            .enumerate()
133            .take(usize::from(self.width.clamp(1, 4)))
134        {
135            *slot = match self.interpolation {
136                Interpolation::Step => a.value[c],
137                Interpolation::Linear => a.value[c] + (b.value[c] - a.value[c]) * u,
138                Interpolation::Bezier => {
139                    let (p0, p1, p2, p3) = (
140                        a.value[c],
141                        a.value[c] + a.handle_out[c],
142                        b.value[c] - b.handle_in[c],
143                        b.value[c],
144                    );
145                    let k = 1.0 - u;
146                    k * (k * k * p0 + 3.0 * u * (k * p1 + u * p2)) + u * u * u * p3
147                }
148            };
149        }
150        out
151    }
152
153    fn check(&self, what: &str) -> Result<(), String> {
154        if !(1..=4).contains(&self.width) {
155            return Err(format!("{what}: {} components", self.width));
156        }
157        if self.keys.is_empty() {
158            return Err(format!("{what}: no keys"));
159        }
160        let mut previous = f32::NEG_INFINITY;
161        for (i, k) in self.keys.iter().enumerate() {
162            if !(k.at.is_finite()
163                && k.value
164                    .iter()
165                    .chain(&k.handle_in)
166                    .chain(&k.handle_out)
167                    .all(|v| v.is_finite()))
168            {
169                return Err(format!("{what}: key {i} is not finite"));
170            }
171            if k.at < previous {
172                return Err(format!("{what}: key {i} at {} goes back from {previous}", k.at));
173            }
174            previous = k.at;
175        }
176        Ok(())
177    }
178}
179
180/// Random variation of a [`Value`], drawn from the stream named in each case (a draw is a uniform number in `[0, 1)` per
181/// component, keyed as the module says).
182#[derive(Clone, Debug, PartialEq, Serialize)]
183pub enum Variation {
184    /// `base + (2u - 1) * max`: the base varies by up to `max` either way.
185    Delta { max: [f32; 4], stream: u32 },
186    /// `base + (2u - 1) * max * base`: the base varies by up to the fraction `max` of itself.
187    Fraction { max: [f32; 4], stream: u32 },
188    /// `base + min + u * (max - min)`: a random addition in a range.
189    Range {
190        min: [f32; 4],
191        max: [f32; 4],
192        stream: u32,
193    },
194    /// `base * factor(input)`: the base scaled by a curve of an [`Input`]'s current value.
195    Scaled { input: u32, factor: Curve },
196}
197
198/// A scalar or vector quantity that may vary with time and at random: what every parameter of an emitter is.
199#[derive(Clone, Debug, PartialEq, Serialize)]
200pub struct Value {
201    /// What the curve is a function of.
202    pub argument: Argument,
203    /// The base value.
204    pub curve: Curve,
205    /// Variations applied to it, in order.
206    pub variation: Vec<Variation>,
207}
208
209impl Value {
210    /// A constant that does not vary.
211    pub fn constant(values: &[f32]) -> Value {
212        Value {
213            argument: Argument::Constant,
214            curve: Curve::constant(values),
215            variation: Vec::new(),
216        }
217    }
218
219    /// Whether the value is a constant that does not vary.
220    pub fn is_fixed(&self) -> bool {
221        self.variation.is_empty() && (self.argument == Argument::Constant || self.curve.keys.len() == 1)
222    }
223
224    fn check(&self, what: &str, inputs: usize) -> Result<(), String> {
225        self.curve.check(what)?;
226        for v in &self.variation {
227            match v {
228                Variation::Delta { max, .. } | Variation::Fraction { max, .. } => {
229                    if !max.iter().all(|m| m.is_finite()) {
230                        return Err(format!("{what}: a variation that is not finite"));
231                    }
232                }
233                Variation::Range { min, max, .. } => {
234                    if !min.iter().chain(max).all(|m| m.is_finite()) {
235                        return Err(format!("{what}: a variation that is not finite"));
236                    }
237                }
238                Variation::Scaled { input, factor } => {
239                    if usize::try_from(*input).is_ok_and(|i| i >= inputs) {
240                        return Err(format!("{what}: input {input} of {inputs}"));
241                    }
242                    factor.check(what)?;
243                }
244            }
245        }
246        Ok(())
247    }
248}
249
250/// The random draw of the module docs: a uniform number in `[0, 1)` keyed by `(seed, emitter, scope, stream, component)`.
251/// `scope` is the index that makes the draw belong to one thing: a particle's number in its emitter for a value of the
252/// particle's age or emission time, the emitter's activation count for a value of its active time.
253pub fn unit(seed: u64, emitter: u32, scope: u64, stream: u32, component: u32) -> f32 {
254    fn mix(z: u64) -> u64 {
255        let z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
256        let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
257        let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
258        z ^ (z >> 31)
259    }
260    let mut x = mix(seed);
261    for v in [u64::from(emitter), scope, u64::from(stream), u64::from(component)] {
262        x = mix(x ^ v);
263    }
264    // the top 24 bits make an exactly representable float in [0, 1)
265    (x >> 40) as f32 / 16_777_216.0
266}
267
268impl Value {
269    /// The value at argument `x` for the draw `scope` of `emitter` in a system of `seed`, with the system's inputs read
270    /// through `input` (by index): the curve, then the variations in order.
271    pub fn eval(&self, x: f32, seed: u64, emitter: u32, scope: u64, input: &dyn Fn(u32) -> f32) -> [f32; 4] {
272        let at = if self.argument == Argument::Constant {
273            0.0
274        } else {
275            x
276        };
277        let mut v = self.curve.value_at(at);
278        let width = usize::from(self.curve.width.clamp(1, 4));
279        for var in &self.variation {
280            match var {
281                Variation::Delta { max, stream } => {
282                    for (c, slot) in v.iter_mut().enumerate().take(width) {
283                        let u = unit(seed, emitter, scope, *stream, c as u32);
284                        *slot += (2.0 * u - 1.0) * max[c];
285                    }
286                }
287                Variation::Fraction { max, stream } => {
288                    for (c, slot) in v.iter_mut().enumerate().take(width) {
289                        let u = unit(seed, emitter, scope, *stream, c as u32);
290                        *slot += (2.0 * u - 1.0) * max[c] * *slot;
291                    }
292                }
293                Variation::Range { min, max, stream } => {
294                    for (c, slot) in v.iter_mut().enumerate().take(width) {
295                        let u = unit(seed, emitter, scope, *stream, c as u32);
296                        *slot += min[c] + u * (max[c] - min[c]);
297                    }
298                }
299                Variation::Scaled { input: i, factor } => {
300                    let f = factor.value_at(input(*i));
301                    for (c, slot) in v.iter_mut().enumerate().take(width) {
302                        *slot *= f[c.min(usize::from(factor.width.clamp(1, 4)) - 1)];
303                    }
304                }
305            }
306        }
307        v
308    }
309}
310
311/// A named quantity a program may drive from outside (a source's external values: the weather, a switch). `default` is
312/// what a renderer uses when nothing drives it.
313#[derive(Clone, Debug, PartialEq, Serialize)]
314pub struct Input {
315    /// The source's name for it.
316    pub name: String,
317    /// The value when nothing sets it.
318    pub default: f32,
319}
320
321/// How a thing follows the frame it hangs from.
322#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
323pub enum Follow {
324    /// Not at all: it keeps its place in the world once made.
325    None,
326    /// With position and orientation.
327    Frame,
328    /// With position only.
329    Position,
330}
331
332/// Which frame a thing hangs from.
333#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
334pub enum Anchor {
335    /// Its parent emitter's frame (or the system's, for a root).
336    Parent,
337    /// The system's own frame: the entity's placement, which a source may make an attachment point of another entity.
338    System,
339}
340
341/// A thing's attachment: which frame, and how it follows.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
343pub struct Attachment {
344    pub anchor: Anchor,
345    pub follow: Follow,
346}
347
348/// How an emitter makes particles.
349#[derive(Clone, Debug, PartialEq, Serialize)]
350pub enum Spawn {
351    /// One particle, once, when the emitter becomes active.
352    Once,
353    /// `count` particles every `interval` seconds, `total` times at most, never more than `max_alive` alive.
354    Periodic {
355        interval: Value,
356        count: Value,
357        total: Option<u32>,
358        max_alive: Option<u32>,
359    },
360    /// `count` particles each time the emitter has moved `step` metres from where it last emitted.
361    Distance {
362        step: Value,
363        count: Value,
364        total: Option<u32>,
365        max_alive: Option<u32>,
366    },
367}
368
369/// Which axis a cylinder runs along.
370#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
371pub enum Axis {
372    X,
373    Y,
374    Z,
375}
376
377/// Where in its frame an emitter makes a particle.
378#[derive(Clone, Debug, PartialEq, Serialize)]
379pub enum Shape {
380    /// The frame's origin.
381    Point,
382    /// A disc in the XY plane; `concentration` is 0 for uniform over the area, 1 for all at the centre and -1 for all on the
383    /// rim.
384    Disk { radius: Value, concentration: Value },
385    /// A rectangle in the XY plane, with the same `concentration` toward its centre.
386    Rectangle { size: [Value; 2], concentration: Value },
387    /// A sphere's surface, or its volume.
388    Sphere { radius: Value, volume: bool },
389    /// A box's surface, or its volume.
390    Box { size: [Value; 3], volume: bool },
391    /// A cylinder's surface, or its volume.
392    Cylinder {
393        radius: Value,
394        height: Value,
395        volume: bool,
396        axis: Axis,
397    },
398}
399
400/// Which way an emitted particle first heads, before the spread.
401#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
402pub enum Heading {
403    /// Out of the shape (along the frame's axis the shape faces for a point, disc or rectangle; away from the centre for a
404    /// sphere, cylinder or box; for a box volume along a random local axis). A source states the axis in `Shape` terms:
405    /// the source's pass puts a point's, disc's and rectangle's facing in `facing`.
406    Shape { facing: [f32; 3] },
407    /// A fixed direction in the world (a unit vector).
408    World([f32; 3]),
409    /// A fixed direction in the emitter's frame (a unit vector).
410    Frame([f32; 3]),
411}
412
413/// How a particle's first heading is scattered.
414#[derive(Clone, Debug, PartialEq, Serialize)]
415pub enum Spread {
416    /// Not at all.
417    None,
418    /// Inside a cone of half-angle `angle` (degrees); `concentration` 0 spreads uniformly over the cone's area, 1 puts every
419    /// particle on the axis and -1 on the cone's edge.
420    Cone { angle: Value, concentration: Value },
421    /// Inside an elliptical cone of half-angles `x` and `y`.
422    Ellipse {
423        x: Value,
424        y: Value,
425        concentration: Value,
426    },
427    /// Inside a rectangular cone of half-angles `x` and `y`.
428    Rectangle {
429        x: Value,
430        y: Value,
431        concentration: Value,
432    },
433}
434
435/// What drives a particle along its heading.
436#[derive(Clone, Debug, PartialEq, Serialize)]
437pub enum Drive {
438    /// Nothing along the heading (only gravity and forces move it).
439    None,
440    /// A speed (metres per second) along the current heading: `speed` times `multiplier`, each a function of the particle's
441    /// age.
442    Speed { speed: Value, multiplier: Value },
443    /// An acceleration (metres per second squared) along the current heading, integrated into the speed: `acceleration`
444    /// times `multiplier`.
445    Acceleration { acceleration: Value, multiplier: Value },
446}
447
448/// Random turning of a particle's heading.
449#[derive(Clone, Debug, PartialEq, Serialize)]
450pub struct Turns {
451    /// Seconds between turns.
452    pub interval: f32,
453    /// The largest turn (degrees).
454    pub max_angle: Value,
455}
456
457/// Following the emitter's movement though not attached to it.
458#[derive(Clone, Debug, PartialEq, Serialize)]
459pub struct Drag {
460    /// 0 not at all, 1 as if attached; negative opposes, above 1 exaggerates.
461    pub factor: Value,
462    /// Whether rotation is followed as well as movement.
463    pub rotation: bool,
464}
465
466/// How a particle moves.
467#[derive(Clone, Debug, PartialEq, Serialize)]
468pub struct Movement {
469    /// The drive along the heading.
470    pub drive: Drive,
471    /// Acceleration along world -Y (metres per second squared; positive falls); it bends the heading.
472    pub gravity: Value,
473    /// Random turns.
474    pub turns: Option<Turns>,
475    /// Following the emitter.
476    pub drag: Option<Drag>,
477    /// How force fields move the particle; `None` when the particle ignores them.
478    pub fields: Option<FieldResponse>,
479}
480
481/// A particle's response to the force fields of the system: the speed it takes along a field's direction (`speed` times
482/// `speed_multiplier`) and the acceleration (`acceleration` times `acceleration_multiplier`), functions of the emitter's
483/// active time; the part a source does not state is a constant 0 (and its multiplier 1).
484#[derive(Clone, Debug, PartialEq, Serialize)]
485pub struct FieldResponse {
486    pub speed: Value,
487    pub speed_multiplier: Value,
488    pub acceleration: Value,
489    pub acceleration_multiplier: Value,
490}
491
492/// The law every particle of an emitter follows.
493#[derive(Clone, Debug, PartialEq, Serialize)]
494pub struct Law {
495    /// Seconds a particle lives; `None` for ever (until the emitter ends).
496    pub lifetime: Option<Value>,
497    /// How a particle hangs from its emitter's frame.
498    pub attachment: Attachment,
499    /// The movement; `None` for a particle that stays where it is made.
500    pub movement: Option<Movement>,
501    /// The speed (metres per second, along the heading) given to a particle when it is emitted, for a particle whose drive is
502    /// an acceleration; a function of the emitter's active time.
503    pub initial_speed: Value,
504    /// Multipliers of every particle's size along its three axes, functions of the time it was emitted; a uniform one has the
505    /// first in all three.
506    pub size_scale: [Value; 3],
507    /// A multiplier of every particle's colour (linear RGBA, unrestricted), a function of the emitter's active time.
508    pub tint: Value,
509}
510
511/// How a draw's colour is blended with what lies behind.
512#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
513pub enum Blend {
514    /// Replaces what is behind, ignoring alpha.
515    Replace,
516    /// `src * a + dst * (1 - a)`.
517    Alpha,
518    /// `dst + src * a`.
519    Additive,
520    /// `dst - src * a`.
521    Subtractive,
522    /// `dst * src`.
523    Multiply,
524}
525
526/// What a flat particle faces.
527#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
528pub enum Orientation {
529    /// The camera's image plane.
530    CameraPlane,
531    /// The camera's position (turning with the view direction).
532    Camera,
533    /// Toward the camera about the world's vertical axis only.
534    CameraYaw,
535    /// Toward the camera about the frame's +Y axis only.
536    FrameYaw,
537    /// A fixed direction in the world (its normal, a unit vector).
538    World([f32; 3]),
539    /// A fixed direction in the emitter's frame.
540    Frame([f32; 3]),
541    /// Along its heading.
542    Heading,
543}
544
545/// A texture applied to a draw: the texture entity and what an atlas is cut into.
546#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
547pub struct Map {
548    /// A `core.texture` or `core.texture_file` entity, or a `core.texture_absent` one: a texture the source names that the
549    /// read could not give ([`crate::TextureAbsent`]), which a viewer shows as missing.
550    pub texture: Entity,
551}
552
553/// A texture atlas cut into cells.
554#[derive(Clone, Debug, PartialEq, Serialize)]
555pub struct Atlas {
556    /// Cells per row.
557    pub columns: u32,
558    /// Cells in use.
559    pub cells: u32,
560    /// The cell shown (a fractional value blends two when `blend_cells`), a function of the particle's age.
561    pub cell: Value,
562    /// Whether neighbouring cells are cross-faded.
563    pub blend_cells: bool,
564}
565
566/// One factor of a draw's colour: a value (linear RGBA unless it says one channel) multiplied in, optionally clamped to 0..1
567/// first.
568#[derive(Clone, Debug, PartialEq, Serialize)]
569pub struct Term {
570    pub value: Value,
571    pub clamp: bool,
572    pub channels: Channels,
573}
574
575/// Which channels a [`Term`] multiplies.
576#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
577pub enum Channels {
578    Rgba,
579    Rgb,
580    Alpha,
581}
582
583/// Limiting a draw by its distance from the camera. `near` and `far` are each a pair of distances in metres: for `near`
584/// `[hidden_at, full_at]` (invisible at or nearer than the first, fully visible at or beyond the second, linear between); for
585/// `far` `[full_at, hidden_at]`. `cut_near` and `cut_far` are hard limits (hidden nearer, hidden farther). `None` is no limit.
586#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
587pub struct DistanceFade {
588    pub near: Option<[f32; 2]>,
589    pub far: Option<[f32; 2]>,
590    pub cut_near: Option<f32>,
591    pub cut_far: Option<f32>,
592}
593
594impl DistanceFade {
595    /// Whether it limits nothing.
596    pub fn is_none(&self) -> bool {
597        *self == DistanceFade::default()
598    }
599}
600
601/// Fading a draw out where it meets geometry (a soft particle): over `distance` metres of depth behind its surface.
602#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
603pub struct SoftEdge {
604    pub distance: f32,
605}
606
607/// A camera-facing or fixed flat particle, one textured quad.
608#[derive(Clone, Debug, PartialEq, Serialize)]
609pub struct Quad {
610    /// The colour texture; `None` is white.
611    pub map: Option<Map>,
612    pub blend: Blend,
613    pub orientation: Orientation,
614    /// Width and height in metres, functions of the particle's age.
615    pub size: [Value; 2],
616    /// Whether the height follows the width.
617    pub uniform: bool,
618    /// Each particle's size is multiplied by a random factor between this and 1, per axis (1 for none).
619    pub size_variance: [f32; 2],
620    /// Offset from the particle's position in its own axes, metres.
621    pub offset: [Value; 3],
622    /// Rotation about the three axes (degrees), then spin about them (degrees per second, a function of the age).
623    pub rotation: [Value; 3],
624    pub spin: [Value; 3],
625    /// Whether size applies before rotation.
626    pub scale_before_rotation: bool,
627    pub atlas: Option<Atlas>,
628    /// The factors of the colour, multiplied.
629    pub tint: Vec<Term>,
630    /// Alpha below this is dropped (0 to 1), and alpha above it stretched to fill the range.
631    pub alpha_fade_threshold: Value,
632    /// Alpha below this is dropped outright.
633    pub alpha_cutoff: Value,
634    pub soft: Option<SoftEdge>,
635    pub distance: Option<DistanceFade>,
636    /// An octagonal quad (cut corners) instead of a rectangle.
637    pub octagonal: bool,
638    /// A lit surface: a normal map and how it is lit; `None` is unlit.
639    pub lit: Option<Lit>,
640    /// A bloom tint added to what is drawn, or `None` for none.
641    pub bloom: Option<[f32; 4]>,
642    /// Whether the texture's colour is multiplied by its alpha first.
643    pub premultiply_alpha: bool,
644    /// Pushes the draw toward (negative) or away from the camera in depth, metres.
645    pub depth_bias: Value,
646}
647
648/// How a lit particle is shaded.
649#[derive(Clone, Debug, PartialEq, Serialize)]
650pub struct Lit {
651    pub normal_map: Option<Map>,
652    pub specular_map: Option<Map>,
653    pub glossiness: f32,
654    pub specularity: f32,
655    /// Whether the particle takes shadows (0 none to 1 full darkness).
656    pub shadow: f32,
657}
658
659/// Several textured layers on one quad (masks, noise, scrolling detail), composed into one colour before the quad's own
660/// tint applies: the base layer gives a colour and an opacity, and each laid layer then changes one of the two by its
661/// operation, weighted by its own opacity ([`compose_layers`]).
662#[derive(Clone, Debug, PartialEq, Serialize)]
663pub struct LayeredQuad {
664    /// The quad's shape, blend, tint and limits. Its `map` is `None`: the layers are its texture. Its `premultiply_alpha`
665    /// multiplies each layer's colour by that layer's own opacity before the layers are composed.
666    pub quad: Quad,
667    /// The first layer: the colour and opacity the others are laid onto.
668    pub base: Layer,
669    /// The layers laid onto the base, in order.
670    pub layers: Vec<LaidLayer>,
671}
672
673/// One textured layer of a [`LayeredQuad`]: its sample is its texture's texel (white without one) times its tint.
674#[derive(Clone, Debug, PartialEq, Serialize)]
675pub struct Layer {
676    pub map: Option<Map>,
677    pub tint: Value,
678    /// Texture-space scroll speed per second, offset and scale: functions of the age (offset of nothing).
679    pub scroll: [Value; 2],
680    pub offset: [Value; 2],
681    pub scale: [Value; 2],
682}
683
684/// A layer laid onto those below it in a [`LayeredQuad`].
685#[derive(Clone, Debug, PartialEq, Serialize)]
686pub struct LaidLayer {
687    pub layer: Layer,
688    /// What the layer changes.
689    pub target: LayerTarget,
690    /// How it combines with what it changes.
691    pub operation: LayerOperation,
692}
693
694/// What a [`LaidLayer`] changes.
695#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
696pub enum LayerTarget {
697    /// The colour, by the layer's colour; the opacity is left as it is.
698    Colour,
699    /// The opacity, by the layer's brightness (the mean of its red, green and blue, clamped to 0..1); the colour is left
700    /// as it is.
701    Opacity,
702}
703
704/// How a [`LaidLayer`]'s value `l` combines with the value `b` it changes (per channel), weighted by the layer's own
705/// opacity `w`.
706#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
707pub enum LayerOperation {
708    /// `b * (1 + (l - 1) * w)`: `b * l` where the layer is opaque, `b` where it is clear.
709    Multiply,
710    /// `b + l * w`.
711    Add,
712    /// `b + (overlay(b, l) - b) * w`, where `overlay(b, l)` is `2 b l` for `b` below 0.5 and `1 - 2 (1 - b) (1 - l)`
713    /// otherwise.
714    Overlay,
715}
716
717impl LayerOperation {
718    /// `b` changed by `l` at weight `w`.
719    pub fn apply(self, b: f32, l: f32, w: f32) -> f32 {
720        match self {
721            LayerOperation::Multiply => b * (1.0 + (l - 1.0) * w),
722            LayerOperation::Add => b + l * w,
723            LayerOperation::Overlay => {
724                let overlay = if b < 0.5 {
725                    2.0 * b * l
726                } else {
727                    1.0 - 2.0 * (1.0 - b) * (1.0 - l)
728                };
729                b + (overlay - b) * w
730            }
731        }
732    }
733}
734
735/// The colour (straight red, green, blue and opacity) of a [`LayeredQuad`]'s layers before the quad's tint: `base` is the
736/// base layer's sample and `laid` each laid layer's target, operation and sample, in order (a sample being the layer's
737/// texel times its tint, opacity in 0..1). With `premultiply_alpha` each sample's colour is first multiplied by its own
738/// opacity. The base gives the colour and the opacity; each laid layer then changes its target by its operation, with its
739/// colour or its brightness as `l` and its opacity as the weight.
740pub fn compose_layers(
741    premultiply_alpha: bool,
742    base: [f32; 4],
743    laid: &[(LayerTarget, LayerOperation, [f32; 4])],
744) -> [f32; 4] {
745    let colour = |s: [f32; 4]| {
746        let k = if premultiply_alpha { s[3] } else { 1.0 };
747        [s[0] * k, s[1] * k, s[2] * k]
748    };
749    let [r, g, b] = colour(base);
750    let mut out = [r, g, b, base[3]];
751    for &(target, operation, sample) in laid {
752        let c = colour(sample);
753        let w = sample[3];
754        match target {
755            LayerTarget::Colour => {
756                for (o, l) in out.iter_mut().zip(c) {
757                    *o = operation.apply(*o, l, w);
758                }
759            }
760            LayerTarget::Opacity => {
761                let brightness = ((c[0] + c[1] + c[2]) / 3.0).clamp(0.0, 1.0);
762                out[3] = operation.apply(out[3], brightness, w);
763            }
764        }
765    }
766    out
767}
768
769/// A point sprite: a screen-aligned textured square.
770#[derive(Clone, Debug, PartialEq, Serialize)]
771pub struct Point {
772    pub map: Option<Map>,
773    pub blend: Blend,
774    /// Edge in metres at the particle, a function of the age.
775    pub size: Value,
776    pub tint: Vec<Term>,
777    pub alpha_cutoff: Value,
778    pub soft: Option<SoftEdge>,
779    pub distance: Option<DistanceFade>,
780    pub bloom: Option<[f32; 4]>,
781}
782
783/// A mesh drawn per particle.
784#[derive(Clone, Debug, PartialEq, Serialize)]
785pub struct MeshParticle {
786    /// A prototype entity (a `core.mesh` with its materials).
787    pub mesh: Entity,
788    /// An animation clip entity to play on it, with its speed and whether it loops.
789    pub clip: Option<MeshClip>,
790    pub blend: Blend,
791    pub orientation: MeshOrientation,
792    /// Size along the three axes, functions of the age.
793    pub size: [Value; 3],
794    pub size_variance: [f32; 3],
795    pub uniform: bool,
796    pub rotation: [Value; 3],
797    pub spin: [Value; 3],
798    pub tint: Vec<Term>,
799    /// Texture-space scroll of the mesh's texture.
800    pub scroll: [Value; 2],
801    pub offset: [Value; 2],
802    pub atlas: Option<Atlas>,
803    pub soft: Option<SoftEdge>,
804    pub distance: Option<DistanceFade>,
805    pub lit: bool,
806}
807
808/// An animation played on a mesh particle.
809#[derive(Clone, Debug, PartialEq, Serialize)]
810pub struct MeshClip {
811    pub clip: Entity,
812    pub speed: f32,
813    pub looped: bool,
814}
815
816/// What a mesh particle faces.
817#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
818pub enum MeshOrientation {
819    /// Its own frame as emitted.
820    Frame,
821    /// World axes.
822    World,
823    /// Along the way it travels (the direction of its velocity), which is the frame's +Z: the mesh is first turned so its
824    /// own direction `forward` (a unit vector) points along +Z, then turned by its rotation and spin about the frame's
825    /// axes. The first turn is the shortest one; a `forward` of -Z takes the half turn about +X. The frame's other two axes
826    /// are any pair square to the travel.
827    Heading { forward: [f32; 3] },
828    /// Facing away from the camera's image plane (the model's front toward the camera).
829    CameraPlane,
830    /// Toward the camera about the vertical axis.
831    CameraYaw,
832}
833
834/// A strip left behind a moving particle.
835#[derive(Clone, Debug, PartialEq, Serialize)]
836pub struct Trail {
837    pub map: Option<Map>,
838    pub blend: Blend,
839    pub orientation: TrailOrientation,
840    /// Width in metres, a function of the particle's age, times `width_scale`, a function of the time it was emitted.
841    pub width: Value,
842    pub width_scale: Value,
843    /// Seconds between segment starts, how long a segment lives, how many are alive, and how many cuts a completed segment is
844    /// subdivided into.
845    pub segment_interval: f32,
846    pub segment_life: f32,
847    pub max_segments: u32,
848    pub subdivisions: u32,
849    /// The fraction of the texture's width used per segment (for the whole trail when `attached_uv`), the speed the texture
850    /// slides along (u per second), and the random offset of each segment's texture (u as a fraction, v a function of the age).
851    pub texture_fraction: Value,
852    pub scroll_u: Value,
853    pub variance_u: f32,
854    pub variance_v: Value,
855    /// Whether the texture is attached to the strip (sliding with its source) or stays where each segment was made.
856    pub attached_uv: bool,
857    /// Whether each segment's opacity follows the speed of the source when the segment was made.
858    pub dynamic_opacity: bool,
859    /// Where along the trail (a fraction of its length from the start, and from the end) opacity has fully faded in and out,
860    /// functions of the age: 0 is no fade.
861    pub fade_start: Value,
862    pub fade_end: Value,
863    pub alpha_fade_threshold: Value,
864    pub alpha_cutoff: Value,
865    pub atlas: Option<Atlas>,
866    pub tint: Vec<Term>,
867    /// Seconds over which the trail fades after its particle ends.
868    pub fade_out: f32,
869    /// A refraction of what is behind the strip, by a normal map and an intensity (a function of the emitter's active time).
870    pub distortion: Option<TrailDistortion>,
871    pub lit: Option<Lit>,
872    pub premultiply_alpha: bool,
873    pub soft: Option<SoftEdge>,
874    pub distance: DistanceFade,
875    pub bloom: Option<[f32; 4]>,
876}
877
878/// The refraction a trail makes of what is behind it.
879#[derive(Clone, Debug, PartialEq, Serialize)]
880pub struct TrailDistortion {
881    pub normal_map: Option<Map>,
882    pub intensity: Value,
883}
884
885/// How a trail's strip faces.
886#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
887pub enum TrailOrientation {
888    /// Toward the camera.
889    Camera,
890    /// Its width along its emitter frame's z axis: the segment that sweeps the strip lies along that axis (less what of it
891    /// runs along the trail).
892    Frame,
893    /// Flat to the world's up.
894    World,
895}
896
897/// A line or elongated quad along the particle's heading.
898#[derive(Clone, Debug, PartialEq, Serialize)]
899pub struct Streak {
900    pub map: Option<Map>,
901    pub blend: Blend,
902    /// Width in metres (a plain line has no width), a function of the time it was emitted, and its multiplier (age).
903    pub width: Option<Value>,
904    /// Length in metres, a function of the time it was emitted, and its multiplier (age).
905    pub length: Value,
906    /// Colour at the leading and trailing end, multiplied in with the tint (functions of the emitter's active time).
907    pub start_colour: Value,
908    pub end_colour: Value,
909    pub tint: Vec<Term>,
910    pub alpha_cutoff: Value,
911    pub soft: Option<SoftEdge>,
912    pub distance: DistanceFade,
913    pub bloom: Option<[f32; 4]>,
914}
915
916/// A sheet that bends the picture behind it.
917#[derive(Clone, Debug, PartialEq, Serialize)]
918pub struct Refraction {
919    pub mode: RefractionMode,
920    pub shape: RefractionShape,
921    pub orientation: Orientation,
922    /// The normal map that pushes (for [`RefractionMode::Push`]) and the mask that limits the sheet.
923    pub normal_map: Option<Map>,
924    pub mask: Option<Map>,
925    /// The sheet's offset from the particle and its size along three axes (metres, functions of the age), whether one size
926    /// serves all, and each particle's random size factor between this and 1 per axis.
927    pub offset: [Value; 3],
928    pub size: [Value; 3],
929    pub uniform: bool,
930    pub size_variance: [f32; 3],
931    /// How far the picture behind is moved at the sheet's centre, in hundredths of the view (of its width across, of its
932    /// height up and down), falling to nothing at `radius` along the sheet's own coordinates: a stir and a push move it so
933    /// far, a twist turns it about the sheet's centre by so many hundredths of a radian in the view's coordinates (0 to 1
934    /// across and down). A stir's speed of turning in degrees per second. How far out the effect reaches, as a share of
935    /// the ellipse the sheet inscribes (1 is that ellipse). Each a function of the age.
936    pub intensity: Value,
937    pub stir_speed: Value,
938    pub radius: Value,
939    /// The normal map's texture offset, scroll speed (per second) and scale along u and v: the map is read at the sheet's
940    /// coordinates plus the offset and the scroll so far, scaled about the sheet's centre.
941    pub normal_offset: [Value; 2],
942    pub normal_scroll: [Value; 2],
943    pub normal_scale: [Value; 2],
944    pub tint: Vec<Term>,
945    pub blend: Blend,
946    pub alpha_cutoff: Value,
947    pub distance: DistanceFade,
948}
949
950/// How a refraction sheet bends what is behind it.
951#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
952pub enum RefractionMode {
953    /// Moves the picture along a direction that turns at `stir_speed`.
954    Stir,
955    /// Moves it by the normal map: its x across the view and its y down it, each from -1 to 1.
956    Push,
957    /// Twists it about the sheet's centre while its edge stays.
958    Twist,
959}
960
961/// The sheet's silhouette.
962#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
963pub enum RefractionShape {
964    Rectangle,
965    /// Half an ellipsoid.
966    Hemiellipsoid,
967    Ellipsoid,
968}
969
970/// A blur of the picture away from a point.
971#[derive(Clone, Debug, PartialEq, Serialize)]
972pub struct RadialBlur {
973    /// A mask that limits where the blur applies.
974    pub mask: Option<Map>,
975    pub blend: Blend,
976    /// The area's offset from the particle and its size along two axes (metres, functions of the age), whether one size serves
977    /// both.
978    pub offset: [Value; 3],
979    pub size: [Value; 2],
980    pub uniform: bool,
981    /// The blur's strength (a function of the age) and how many times it is applied.
982    pub radius: Value,
983    pub iterations: u32,
984    pub tint: Vec<Term>,
985    pub alpha_cutoff: Value,
986    pub distance: DistanceFade,
987}
988
989/// A light that follows the particle or the emitter.
990#[derive(Clone, Debug, PartialEq, Serialize)]
991pub struct LightDraw {
992    pub kind: LightKind,
993    /// Linear RGB, a function of the age (unrestricted).
994    pub colour: Value,
995    /// Candela, a function of the age.
996    pub intensity: Value,
997    /// Metres at which it stops.
998    pub range: Value,
999    pub shadows: bool,
1000    /// Seconds over which it fades out when its emitter ends.
1001    pub fade_out: f32,
1002}
1003
1004/// What a [`LightDraw`] is.
1005#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1006pub enum LightKind {
1007    Point,
1008    /// A cone along the frame's +Z; angles in degrees from the axis.
1009    Spot {
1010        inner: f32,
1011        outer: f32,
1012    },
1013}
1014
1015/// A lens flare: up to four textured layers placed along the line between the source and the screen's centre.
1016#[derive(Clone, Debug, PartialEq, Serialize)]
1017pub struct Flare {
1018    pub layers: Vec<FlareLayer>,
1019    pub blend: Blend,
1020    /// The diameter of the source sphere, whose visible fraction sets the opacity, and the seconds the opacity takes to follow it.
1021    pub source_size: f32,
1022    pub opacity_transition: f32,
1023}
1024
1025/// How a flare layer's offset vector is found.
1026#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
1027pub enum Reflection {
1028    /// No offset: the flare sits at the source.
1029    None,
1030    /// The source's screen position reflected through the screen's centre.
1031    Radial,
1032    /// The source's screen position flipped horizontally.
1033    Horizontal,
1034}
1035
1036/// One layer of a [`Flare`]: `count` flares of one texture.
1037#[derive(Clone, Debug, PartialEq, Serialize)]
1038pub struct FlareLayer {
1039    pub map: Option<Map>,
1040    pub count: u32,
1041    /// Width and height (constants), whether the width serves both, and each flare's random factor between this and 1 per axis.
1042    pub width: Value,
1043    pub height: Value,
1044    pub uniform: bool,
1045    pub scale_variation: [f32; 2],
1046    /// Colour (clamped to 0..1) and a multiplier of it (unrestricted).
1047    pub colour: Value,
1048    pub colour_multiplier: [f32; 4],
1049    pub reflection: Reflection,
1050    /// A scalar multiplier of the offset vector, and each flare's random factor between this and 1 on it.
1051    pub offset: f32,
1052    pub offset_variation: f32,
1053    /// The radius of a sphere around the source within which the layer is attenuated (negative: none).
1054    pub attenuation_radius: f32,
1055    pub bloom: Option<[f32; 4]>,
1056}
1057
1058/// What a force field pushes with.
1059#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
1060pub enum ForceKind {
1061    /// A steady push along a direction.
1062    Wind,
1063    /// A pull toward a point or along a direction.
1064    Gravity,
1065    /// A swirling noise.
1066    Turbulence,
1067    /// Cancels other fields' effect.
1068    Cancel,
1069}
1070
1071/// The volume a force field acts in.
1072#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1073pub enum ForceVolume {
1074    /// Everywhere.
1075    Boundless,
1076    Box {
1077        half: [f32; 3],
1078    },
1079    Sphere {
1080        radius: f32,
1081    },
1082    Cylinder {
1083        radius: f32,
1084        half_height: f32,
1085    },
1086}
1087
1088/// A force field: pushes the particles whose [`Movement::fields`] listen, inside its volume.
1089#[derive(Clone, Debug, PartialEq, Serialize)]
1090pub struct ForceField {
1091    pub kind: ForceKind,
1092    pub volume: ForceVolume,
1093    /// For a wind: the direction in the emitter's frame (a unit vector); a gravity pulls toward the volume's centre.
1094    pub direction: [f32; 3],
1095    /// The strength, a function of the field's active time, times a constant multiplier and a random factor between the two
1096    /// bounds (a gravity takes their average).
1097    pub strength: Value,
1098    pub multiplier: f32,
1099    pub random_multiplier: [f32; 2],
1100    /// Seconds over which it fades out when its emitter ends.
1101    pub fade_out: f32,
1102}
1103
1104/// Ripples where the emitter's frame touches water: the water is pushed down to `depth` over `descent` seconds in the shape of
1105/// `map`, held for `duration` seconds and released.
1106#[derive(Clone, Debug, PartialEq, Serialize)]
1107pub struct Ripples {
1108    pub map: Option<Map>,
1109    pub depth: f32,
1110    pub size: f32,
1111    pub descent: f32,
1112    pub duration: f32,
1113}
1114
1115/// What an emitter draws for each particle (or, for a light, field or ripple, instead of particles).
1116#[derive(Clone, Debug, PartialEq, Serialize)]
1117pub enum Draw {
1118    Quad(Quad),
1119    LayeredQuad(Box<LayeredQuad>),
1120    Point(Point),
1121    Mesh(MeshParticle),
1122    Trail(Trail),
1123    Streak(Streak),
1124    Refraction(Refraction),
1125    RadialBlur(RadialBlur),
1126    Light(LightDraw),
1127    Flare(Flare),
1128    Field(ForceField),
1129    Ripples(Ripples),
1130}
1131
1132/// One emitter of a system.
1133#[derive(Clone, Debug, PartialEq, Serialize)]
1134pub struct Emitter {
1135    /// The source's name for it, if it has one.
1136    pub name: Option<String>,
1137    /// The emitter this one hangs from; always an earlier one. `None` hangs it from the system's frame.
1138    pub parent: Option<u32>,
1139    /// Placement in the parent's frame (metres), in the world's frame.
1140    pub placement: Transform,
1141    /// Random placement jitter: translation by up to this much either way (metres), rotation about each axis (degrees).
1142    pub jitter_translation: [f32; 3],
1143    pub jitter_rotation: [f32; 3],
1144    /// How the emitter follows its parent.
1145    pub attachment: Attachment,
1146    /// Seconds before it becomes active, and for how long (`None` for ever).
1147    pub delay: f32,
1148    pub duration: Option<f32>,
1149    /// How it moves in its parent's frame over its active time.
1150    pub motion: Option<NodeMotion>,
1151    pub spawn: Spawn,
1152    pub shape: Shape,
1153    pub heading: Heading,
1154    pub spread: Spread,
1155    pub law: Law,
1156    /// What it draws; `None` for an emitter that only holds children (or an effect-spawning one).
1157    pub draw: Option<Draw>,
1158    /// Other effects this emitter starts when it becomes active (a source's references to other effects): the entity of the
1159    /// system to start, attached to this emitter's frame.
1160    pub spawns: Vec<Entity>,
1161    /// Which `states` of the system it exists in; empty for all of them.
1162    pub states: Vec<u32>,
1163    /// Display order among the system's draws (lower draws in front, as a source's depth bias states).
1164    pub order: f32,
1165    /// The camera distances (metres, from and to) within which the emitter and its children exist; `None` for any distance (a
1166    /// source's level-of-detail nodes give each child a range).
1167    pub lod_range: Option<[f32; 2]>,
1168    /// For an emitter that makes copies of its children rather than particles: which children each emission starts. Its
1169    /// `spawn` schedules the emissions; each copy runs its children from its own start.
1170    pub copies: Option<Pick>,
1171}
1172
1173/// Which children an emitter that copies its children starts at each emission.
1174#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
1175pub enum Pick {
1176    /// All of them.
1177    All,
1178    /// One, at random.
1179    One,
1180}
1181
1182/// How an emitter moves in its parent's frame over its active time.
1183#[derive(Clone, Debug, PartialEq, Serialize)]
1184pub struct NodeMotion {
1185    /// The direction of `speed` and `acceleration` in the emitter's own frame (a unit vector).
1186    pub forward: [f32; 3],
1187    /// Speed along `forward` (metres per second) times its multiplier, and acceleration along it (metres per second squared)
1188    /// times its multiplier, functions of the active time.
1189    pub speed: Value,
1190    pub speed_multiplier: Value,
1191    pub acceleration: Value,
1192    pub acceleration_multiplier: Value,
1193    /// Acceleration along the emitter frame's +Y (metres per second squared), or along the world's +Y when `lift_in_world`.
1194    pub lift: Value,
1195    pub lift_in_world: bool,
1196    /// A displacement of the emitter in its parent's frame (metres), a function of the active time.
1197    pub displacement: Option<Value>,
1198    /// Spin about the emitter's three axes (degrees per second) times a multiplier, functions of the active time.
1199    pub spin: [Value; 3],
1200    pub spin_multiplier: [Value; 3],
1201    /// Whether the emitter turns to face its motion: its forward along its velocity.
1202    pub align: bool,
1203    pub turns: Option<Turns>,
1204    pub drag: Option<Drag>,
1205    /// The least and the most the speed along `forward` may be (metres per second): acceleration takes it no further,
1206    /// and a speed that starts outside them is held to them. `None` for no limit.
1207    pub min_speed: Option<f32>,
1208    pub max_speed: Option<f32>,
1209}
1210
1211/// What happens to a system when what spawned it goes away.
1212#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1213pub enum Termination {
1214    /// Emitters stop and the particles live out their lives.
1215    Natural,
1216    /// Everything fades out over this many seconds.
1217    Fade(f32),
1218    /// Everything vanishes at once.
1219    Instant,
1220}
1221
1222/// A particle effect.
1223#[derive(Clone, Debug, PartialEq, Serialize)]
1224pub struct ParticleSystem {
1225    /// The seed of every random draw of the system.
1226    pub seed: u64,
1227    /// The emitters, parents before children.
1228    pub emitters: Vec<Emitter>,
1229    /// Quantities a program may drive.
1230    pub inputs: Vec<Input>,
1231    /// The number of states the system has (a source's state machine; a renderer plays state 0 unless it is told another).
1232    pub states: u32,
1233    /// Seconds the system is run before it is first shown, so that a placed fire is already burning when its area loads.
1234    pub warmup: f32,
1235    /// The rate at which the system's time runs, a function of its active time (1 is real time); `None` for 1.
1236    pub time_scale: Option<Value>,
1237    /// Beyond this distance (metres) from the camera nothing of the system is drawn; `None` for any distance.
1238    pub max_view_distance: Option<f32>,
1239    /// What happens when the thing that spawned the system goes away (a source's termination).
1240    pub termination: Termination,
1241    /// Seconds after which a system with no endless emitter has no live particle: the longest an emitter is active plus its
1242    /// particles' longest life; `None` when something runs for ever. A renderer that loops a system restarts at this time.
1243    pub end: Option<f32>,
1244    /// What the source's pass could not carry, by reason, with a count (a placed and lost accounting).
1245    pub lost: Vec<(String, u32)>,
1246}
1247
1248impl Component for ParticleSystem {
1249    const NAME: &'static str = "core.particles";
1250    const GEOMETRY: crate::Geometry = crate::Geometry::None;
1251    fn to_json(&self) -> Result<Option<Json>, JsonError> {
1252        crate::json::to_value(self).map(Some)
1253    }
1254    /// The textures, meshes and clips the draws name, and the systems the emitters start.
1255    fn references(&self, out: &mut Vec<Entity>) {
1256        for e in &self.emitters {
1257            out.extend(e.spawns.iter().copied());
1258            if let Some(d) = &e.draw {
1259                d.references(out);
1260            }
1261        }
1262    }
1263    /// Emitters in order with parents before children, curves with ascending finite keys, variations naming an input the
1264    /// system has, an `end` that is finite, and finite numbers throughout the placement.
1265    fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
1266        for (i, e) in self.emitters.iter().enumerate() {
1267            let at = |what: &str| format!("emitter {i}: {what}");
1268            if let Some(p) = e.parent
1269                && usize::try_from(p).map_or(true, |p| p >= i)
1270            {
1271                return Err(at(&format!("parent {p} is not an earlier emitter")));
1272            }
1273            let finite = e
1274                .placement
1275                .cols
1276                .iter()
1277                .flatten()
1278                .chain(&e.jitter_translation)
1279                .chain(&e.jitter_rotation);
1280            if !finite.chain([&e.delay, &e.order]).all(|v| v.is_finite()) {
1281                return Err(at("a placement that is not finite"));
1282            }
1283            if e.states.iter().any(|&s| s >= self.states.max(1)) {
1284                return Err(at("a state the system does not have"));
1285            }
1286            e.check(self.inputs.len()).map_err(|m| at(&m))?;
1287        }
1288        if self.end.is_some_and(|t| !t.is_finite() || t < 0.0) {
1289            return Err("an end that is not a finite time".into());
1290        }
1291        if !(self.warmup.is_finite() && self.warmup >= 0.0) {
1292            return Err(format!("a warm-up of {} seconds", self.warmup));
1293        }
1294        if self
1295            .max_view_distance
1296            .is_some_and(|d| !(d.is_finite() && d > 0.0))
1297        {
1298            return Err("a view distance that is not a positive number".into());
1299        }
1300        if let Some(v) = &self.time_scale {
1301            v.check("time scale", self.inputs.len())?;
1302        }
1303        if let Termination::Fade(t) = self.termination
1304            && !(t.is_finite() && t >= 0.0)
1305        {
1306            return Err(format!("a fade of {t} seconds"));
1307        }
1308        Ok(())
1309    }
1310}
1311
1312impl Emitter {
1313    fn check(&self, inputs: usize) -> Result<(), String> {
1314        let mut values: Vec<(&str, &Value)> = vec![
1315            ("initial speed", &self.law.initial_speed),
1316            ("tint", &self.law.tint),
1317            ("size scale x", &self.law.size_scale[0]),
1318            ("size scale y", &self.law.size_scale[1]),
1319            ("size scale z", &self.law.size_scale[2]),
1320        ];
1321        if let Some(v) = &self.law.lifetime {
1322            values.push(("lifetime", v));
1323        }
1324        match &self.spawn {
1325            Spawn::Once => {}
1326            Spawn::Periodic { interval, count, .. } => {
1327                values.extend([("interval", interval), ("count", count)])
1328            }
1329            Spawn::Distance { step, count, .. } => values.extend([("step", step), ("count", count)]),
1330        }
1331        match &self.shape {
1332            Shape::Point => {}
1333            Shape::Disk {
1334                radius,
1335                concentration,
1336            } => values.extend([("radius", radius), ("concentration", concentration)]),
1337            Shape::Rectangle { size, concentration } => {
1338                values.extend(size.iter().map(|v| ("size", v)));
1339                values.push(("concentration", concentration));
1340            }
1341            Shape::Sphere { radius, .. } => values.push(("radius", radius)),
1342            Shape::Box { size, .. } => values.extend(size.iter().map(|v| ("size", v))),
1343            Shape::Cylinder { radius, height, .. } => values.extend([("radius", radius), ("height", height)]),
1344        }
1345        match &self.spread {
1346            Spread::None => {}
1347            Spread::Cone { angle, concentration } => {
1348                values.extend([("angle", angle), ("concentration", concentration)])
1349            }
1350            Spread::Ellipse { x, y, concentration } | Spread::Rectangle { x, y, concentration } => {
1351                values.extend([("angle", x), ("angle", y), ("concentration", concentration)]);
1352            }
1353        }
1354        if let Some(m) = &self.law.movement {
1355            match &m.drive {
1356                Drive::None => {}
1357                Drive::Speed { speed, multiplier } => {
1358                    values.extend([("speed", speed), ("speed multiplier", multiplier)])
1359                }
1360                Drive::Acceleration {
1361                    acceleration,
1362                    multiplier,
1363                } => {
1364                    values.extend([
1365                        ("acceleration", acceleration),
1366                        ("acceleration multiplier", multiplier),
1367                    ]);
1368                }
1369            }
1370            values.push(("gravity", &m.gravity));
1371            if let Some(t) = &m.turns {
1372                values.push(("turn angle", &t.max_angle));
1373            }
1374            if let Some(d) = &m.drag {
1375                values.push(("drag", &d.factor));
1376            }
1377            if let Some(f) = &m.fields {
1378                values.extend([
1379                    ("field speed", &f.speed),
1380                    ("field speed multiplier", &f.speed_multiplier),
1381                    ("field acceleration", &f.acceleration),
1382                    ("field acceleration multiplier", &f.acceleration_multiplier),
1383                ]);
1384            }
1385        }
1386        if let Some(n) = &self.motion {
1387            for (what, limit) in [("node min speed", n.min_speed), ("node max speed", n.max_speed)] {
1388                if limit.is_some_and(|x| !x.is_finite()) {
1389                    return Err(format!("{what} {limit:?} is not finite"));
1390                }
1391            }
1392            if let (Some(min), Some(max)) = (n.min_speed, n.max_speed)
1393                && min > max
1394            {
1395                return Err(format!("node min speed {min} is above its max speed {max}"));
1396            }
1397            values.extend([
1398                ("node speed", &n.speed),
1399                ("node speed multiplier", &n.speed_multiplier),
1400                ("node acceleration", &n.acceleration),
1401                ("node acceleration multiplier", &n.acceleration_multiplier),
1402                ("node lift", &n.lift),
1403            ]);
1404            values.extend(n.displacement.iter().map(|v| ("node displacement", v)));
1405            values.extend(n.spin.iter().chain(&n.spin_multiplier).map(|v| ("node spin", v)));
1406            if let Some(t) = &n.turns {
1407                values.push(("node turn angle", &t.max_angle));
1408            }
1409            if let Some(d) = &n.drag {
1410                values.push(("node drag", &d.factor));
1411            }
1412        }
1413        for (what, v) in values {
1414            v.check(what, inputs)?;
1415        }
1416        if let Some(d) = &self.draw {
1417            d.check(inputs)?;
1418        }
1419        Ok(())
1420    }
1421}
1422
1423fn push_map(out: &mut Vec<Entity>, m: &Option<Map>) {
1424    out.extend(m.iter().map(|m| m.texture));
1425}
1426
1427impl Draw {
1428    fn references(&self, out: &mut Vec<Entity>) {
1429        match self {
1430            Draw::Quad(q) => {
1431                push_map(out, &q.map);
1432                if let Some(l) = &q.lit {
1433                    push_map(out, &l.normal_map);
1434                    push_map(out, &l.specular_map);
1435                }
1436            }
1437            Draw::LayeredQuad(q) => {
1438                push_map(out, &q.quad.map);
1439                if let Some(l) = &q.quad.lit {
1440                    push_map(out, &l.normal_map);
1441                    push_map(out, &l.specular_map);
1442                }
1443                push_map(out, &q.base.map);
1444                for l in &q.layers {
1445                    push_map(out, &l.layer.map);
1446                }
1447            }
1448            Draw::Point(p) => push_map(out, &p.map),
1449            Draw::Mesh(m) => {
1450                out.push(m.mesh);
1451                out.extend(m.clip.iter().map(|c| c.clip));
1452            }
1453            Draw::Trail(t) => {
1454                push_map(out, &t.map);
1455                if let Some(d) = &t.distortion {
1456                    push_map(out, &d.normal_map);
1457                }
1458                if let Some(l) = &t.lit {
1459                    push_map(out, &l.normal_map);
1460                    push_map(out, &l.specular_map);
1461                }
1462            }
1463            Draw::Streak(s) => push_map(out, &s.map),
1464            Draw::Refraction(r) => {
1465                push_map(out, &r.normal_map);
1466                push_map(out, &r.mask);
1467            }
1468            Draw::RadialBlur(b) => push_map(out, &b.mask),
1469            Draw::Flare(f) => {
1470                for l in &f.layers {
1471                    push_map(out, &l.map);
1472                }
1473            }
1474            Draw::Ripples(r) => push_map(out, &r.map),
1475            Draw::Light(_) | Draw::Field(_) => {}
1476        }
1477    }
1478
1479    /// Every value of the draw, named for an error.
1480    fn values<'a>(&'a self, out: &mut Vec<(&'static str, &'a Value)>) {
1481        fn terms<'a>(out: &mut Vec<(&'static str, &'a Value)>, t: &'a [Term]) {
1482            out.extend(t.iter().map(|t| ("tint", &t.value)));
1483        }
1484        fn atlas<'a>(out: &mut Vec<(&'static str, &'a Value)>, a: &'a Option<Atlas>) {
1485            out.extend(a.iter().map(|a| ("atlas cell", &a.cell)));
1486        }
1487        fn quad_values<'a>(out: &mut Vec<(&'static str, &'a Value)>, q: &'a Quad) {
1488            out.extend(q.size.iter().map(|v| ("size", v)));
1489            out.extend(q.offset.iter().map(|v| ("offset", v)));
1490            out.extend(q.rotation.iter().map(|v| ("rotation", v)));
1491            out.extend(q.spin.iter().map(|v| ("spin", v)));
1492            out.extend([
1493                ("alpha fade threshold", &q.alpha_fade_threshold),
1494                ("alpha cutoff", &q.alpha_cutoff),
1495            ]);
1496            out.push(("depth bias", &q.depth_bias));
1497            terms(out, &q.tint);
1498            atlas(out, &q.atlas);
1499        }
1500        match self {
1501            Draw::Quad(q) => quad_values(out, q),
1502            Draw::LayeredQuad(q) => {
1503                quad_values(out, &q.quad);
1504                for l in std::iter::once(&q.base).chain(q.layers.iter().map(|l| &l.layer)) {
1505                    out.push(("layer tint", &l.tint));
1506                    out.extend(l.scroll.iter().map(|v| ("layer scroll", v)));
1507                    out.extend(l.offset.iter().map(|v| ("layer offset", v)));
1508                    out.extend(l.scale.iter().map(|v| ("layer scale", v)));
1509                }
1510            }
1511            Draw::Point(p) => {
1512                out.extend([("size", &p.size), ("alpha cutoff", &p.alpha_cutoff)]);
1513                terms(out, &p.tint);
1514            }
1515            Draw::Mesh(m) => {
1516                out.extend(m.size.iter().map(|v| ("size", v)));
1517                out.extend(m.rotation.iter().map(|v| ("rotation", v)));
1518                out.extend(m.spin.iter().map(|v| ("spin", v)));
1519                out.extend(m.scroll.iter().map(|v| ("scroll", v)));
1520                out.extend(m.offset.iter().map(|v| ("offset", v)));
1521                terms(out, &m.tint);
1522                atlas(out, &m.atlas);
1523            }
1524            Draw::Trail(t) => {
1525                out.extend([
1526                    ("width", &t.width),
1527                    ("width scale", &t.width_scale),
1528                    ("texture fraction", &t.texture_fraction),
1529                    ("scroll", &t.scroll_u),
1530                    ("variance", &t.variance_v),
1531                    ("fade start", &t.fade_start),
1532                    ("fade end", &t.fade_end),
1533                    ("alpha fade threshold", &t.alpha_fade_threshold),
1534                    ("alpha cutoff", &t.alpha_cutoff),
1535                ]);
1536                out.extend(t.distortion.iter().map(|d| ("distortion", &d.intensity)));
1537                terms(out, &t.tint);
1538                atlas(out, &t.atlas);
1539            }
1540            Draw::Streak(s) => {
1541                out.extend([
1542                    ("length", &s.length),
1543                    ("start colour", &s.start_colour),
1544                    ("end colour", &s.end_colour),
1545                    ("alpha cutoff", &s.alpha_cutoff),
1546                ]);
1547                out.extend(s.width.iter().map(|w| ("width", w)));
1548                terms(out, &s.tint);
1549            }
1550            Draw::Refraction(r) => {
1551                out.extend(r.offset.iter().map(|v| ("offset", v)));
1552                out.extend(r.size.iter().map(|v| ("size", v)));
1553                out.extend([
1554                    ("intensity", &r.intensity),
1555                    ("stir speed", &r.stir_speed),
1556                    ("radius", &r.radius),
1557                    ("alpha cutoff", &r.alpha_cutoff),
1558                ]);
1559                out.extend(
1560                    r.normal_offset
1561                        .iter()
1562                        .chain(&r.normal_scroll)
1563                        .chain(&r.normal_scale)
1564                        .map(|v| ("normal map", v)),
1565                );
1566                terms(out, &r.tint);
1567            }
1568            Draw::RadialBlur(b) => {
1569                out.extend(b.offset.iter().map(|v| ("offset", v)));
1570                out.extend(b.size.iter().map(|v| ("size", v)));
1571                out.extend([("radius", &b.radius), ("alpha cutoff", &b.alpha_cutoff)]);
1572                terms(out, &b.tint);
1573            }
1574            Draw::Light(l) => out.extend([
1575                ("colour", &l.colour),
1576                ("intensity", &l.intensity),
1577                ("range", &l.range),
1578            ]),
1579            Draw::Flare(f) => {
1580                for l in &f.layers {
1581                    out.extend([
1582                        ("flare width", &l.width),
1583                        ("flare height", &l.height),
1584                        ("flare colour", &l.colour),
1585                    ]);
1586                }
1587            }
1588            Draw::Field(f) => out.push(("force", &f.strength)),
1589            Draw::Ripples(_) => {}
1590        }
1591    }
1592
1593    fn check(&self, inputs: usize) -> Result<(), String> {
1594        let mut values = Vec::new();
1595        self.values(&mut values);
1596        for (what, v) in values {
1597            v.check(what, inputs)?;
1598        }
1599        let atlas = match self {
1600            Draw::Quad(q) => q.atlas.as_ref(),
1601            Draw::LayeredQuad(q) => q.quad.atlas.as_ref(),
1602            Draw::Mesh(m) => m.atlas.as_ref(),
1603            Draw::Trail(t) => t.atlas.as_ref(),
1604            _ => None,
1605        };
1606        if atlas.is_some_and(|a| a.columns == 0 || a.cells == 0) {
1607            return Err("an atlas of no cells".into());
1608        }
1609        if let Draw::Quad(q) = self
1610            && let Orientation::World(n) | Orientation::Frame(n) = q.orientation
1611            && !n.iter().all(|v| v.is_finite())
1612        {
1613            return Err("an orientation that is not finite".into());
1614        }
1615        if let Draw::Mesh(m) = self
1616            && let MeshOrientation::Heading { forward } = m.orientation
1617            && !(forward.iter().all(|v| v.is_finite())
1618                && (forward.iter().map(|v| v * v).sum::<f32>().sqrt() - 1.0).abs() <= 1e-4)
1619        {
1620            return Err("a mesh particle's forward direction that is not a unit vector".into());
1621        }
1622        if let Draw::Trail(t) = self
1623            && !(t.segment_interval.is_finite() && t.segment_life.is_finite() && t.fade_out.is_finite())
1624        {
1625            return Err("a trail time that is not finite".into());
1626        }
1627        if let Draw::LayeredQuad(q) = self
1628            && q.quad.map.is_some()
1629        {
1630            return Err("a layered quad with a texture of its own (its layers are its texture)".into());
1631        }
1632        Ok(())
1633    }
1634}
1635
1636#[cfg(test)]
1637mod tests {
1638    use super::*;
1639
1640    fn key(at: f32, v: f32) -> Key {
1641        Key {
1642            at,
1643            value: [v, 0.0, 0.0, 0.0],
1644            handle_in: [0.0; 4],
1645            handle_out: [0.0; 4],
1646        }
1647    }
1648
1649    fn curve(interpolation: Interpolation, keys: Vec<Key>) -> Curve {
1650        Curve {
1651            width: 1,
1652            interpolation,
1653            looped: false,
1654            keys,
1655        }
1656    }
1657
1658    #[test]
1659    fn curves_hold_run_and_loop() {
1660        let lin = curve(Interpolation::Linear, vec![key(0.0, 0.0), key(2.0, 10.0)]);
1661        assert_eq!(lin.value_at(-5.0)[0], 0.0);
1662        assert_eq!(lin.value_at(1.0)[0], 5.0);
1663        assert_eq!(lin.value_at(9.0)[0], 10.0);
1664        let step = curve(
1665            Interpolation::Step,
1666            vec![key(0.0, 1.0), key(2.0, 2.0), key(4.0, 3.0)],
1667        );
1668        assert_eq!(step.value_at(1.9)[0], 1.0);
1669        assert_eq!(step.value_at(2.0)[0], 2.0);
1670        let mut looped = lin.clone();
1671        looped.looped = true;
1672        assert_eq!(looped.value_at(3.0)[0], 5.0);
1673        let mut still = curve(Interpolation::Linear, vec![key(0.0, 7.0)]);
1674        still.looped = true;
1675        assert_eq!(still.value_at(100.0)[0], 7.0);
1676    }
1677
1678    #[test]
1679    fn a_bezier_segment_is_the_cubic_through_its_handles() {
1680        let mut a = key(0.0, 0.0);
1681        a.handle_out = [1.0, 0.0, 0.0, 0.0];
1682        let mut b = key(1.0, 1.0);
1683        b.handle_in = [1.0, 0.0, 0.0, 0.0];
1684        let c = curve(Interpolation::Bezier, vec![a, b]);
1685        // control points 0, 1, 0, 1 in u
1686        let want = |u: f32| {
1687            let k = 1.0 - u;
1688            3.0 * k * k * u + u * u * u
1689        };
1690        for u in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
1691            assert!((c.value_at(u)[0] - want(u)).abs() < 1e-6, "{u}");
1692        }
1693    }
1694
1695    #[test]
1696    fn draws_are_deterministic_uniform_and_keyed() {
1697        let a = unit(7, 1, 2, 3, 0);
1698        assert_eq!(a, unit(7, 1, 2, 3, 0));
1699        assert!((0.0..1.0).contains(&a));
1700        // any key changes the draw
1701        for other in [
1702            unit(8, 1, 2, 3, 0),
1703            unit(7, 2, 2, 3, 0),
1704            unit(7, 1, 3, 3, 0),
1705            unit(7, 1, 2, 4, 0),
1706            unit(7, 1, 2, 3, 1),
1707        ] {
1708            assert_ne!(a, other);
1709        }
1710        // roughly uniform over many scopes
1711        let n = 20_000;
1712        let mean = (0..n).map(|i| f64::from(unit(1, 0, i, 0, 0))).sum::<f64>() / n as f64;
1713        assert!((mean - 0.5).abs() < 0.01, "{mean}");
1714    }
1715
1716    #[test]
1717    fn variations_apply_in_order_within_their_bounds() {
1718        let mut v = Value::constant(&[10.0]);
1719        v.variation.push(Variation::Delta {
1720            max: [2.0; 4],
1721            stream: 0,
1722        });
1723        v.variation.push(Variation::Fraction {
1724            max: [0.1; 4],
1725            stream: 1,
1726        });
1727        let none = |_: u32| 0.0;
1728        for scope in 0..200 {
1729            let x = v.eval(0.0, 5, 0, scope, &none)[0];
1730            assert!((8.0 * 0.9..=12.0 * 1.1).contains(&x), "{x}");
1731        }
1732        // the same scope gives the same value
1733        assert_eq!(v.eval(0.0, 5, 0, 9, &none), v.eval(3.0, 5, 0, 9, &none));
1734        // an input-scaled value reads its curve at the input
1735        let mut scaled = Value::constant(&[4.0]);
1736        scaled.variation.push(Variation::Scaled {
1737            input: 0,
1738            factor: curve(Interpolation::Linear, vec![key(0.0, 0.0), key(1.0, 1.0)]),
1739        });
1740        assert_eq!(scaled.eval(0.0, 1, 0, 0, &|_| 0.25)[0], 1.0);
1741    }
1742
1743    #[test]
1744    fn a_constant_value_is_fixed() {
1745        let v = Value::constant(&[1.0, 2.0]);
1746        assert!(v.is_fixed());
1747        assert_eq!(v.curve.value_at(9.0)[..2], [1.0, 2.0]);
1748        let mut w = v.clone();
1749        w.variation.push(Variation::Delta {
1750            max: [0.1; 4],
1751            stream: 3,
1752        });
1753        assert!(!w.is_fixed());
1754    }
1755
1756    #[test]
1757    fn a_mesh_particle_turned_along_its_travel_names_a_unit_forward() {
1758        let zero = || Value::constant(&[0.0]);
1759        let mesh = |forward: [f32; 3]| {
1760            Draw::Mesh(MeshParticle {
1761                mesh: Entity::from_index(1),
1762                clip: None,
1763                blend: Blend::Alpha,
1764                orientation: MeshOrientation::Heading { forward },
1765                size: [zero(), zero(), zero()],
1766                size_variance: [1.0; 3],
1767                uniform: true,
1768                rotation: [zero(), zero(), zero()],
1769                spin: [zero(), zero(), zero()],
1770                tint: Vec::new(),
1771                scroll: [zero(), zero()],
1772                offset: [zero(), zero()],
1773                atlas: None,
1774                soft: None,
1775                distance: None,
1776                lit: false,
1777            })
1778        };
1779        assert!(mesh([0.0, 1.0, 0.0]).check(0).is_ok());
1780        assert!(mesh([0.0, 0.0, -1.0]).check(0).is_ok());
1781        for bad in [[0.0; 3], [0.0, 2.0, 0.0], [f32::NAN, 0.0, 0.0]] {
1782            assert!(mesh(bad).check(0).is_err(), "{bad:?}");
1783        }
1784    }
1785
1786    #[test]
1787    fn laid_layers_change_their_target_by_their_operation_at_their_opacity() {
1788        use LayerOperation::{Add, Multiply, Overlay};
1789        use LayerTarget::{Colour, Opacity};
1790        let base = [0.8, 0.4, 0.2, 0.5];
1791        // hand-worked: multiply at half weight, 0.8 * (1 + (0.5 - 1) * 0.5) = 0.6
1792        let c = compose_layers(false, base, &[(Colour, Multiply, [0.5, 0.5, 0.5, 0.5])]);
1793        for (got, want) in c.iter().zip([0.6, 0.3, 0.15, 0.5]) {
1794            assert!((got - want).abs() < 1e-6, "{c:?}");
1795        }
1796        // add at full weight; the opacity is left as it is
1797        let c = compose_layers(false, base, &[(Colour, Add, [0.1, 0.2, 0.3, 1.0])]);
1798        assert!(
1799            (c[0] - 0.9).abs() < 1e-6
1800                && (c[1] - 0.6).abs() < 1e-6
1801                && (c[2] - 0.5).abs() < 1e-6
1802                && c[3] == 0.5
1803        );
1804        // overlay at full weight: 0.8 is above 0.5 (1 - 2 * 0.2 * 0.5 = 0.8), 0.4 below (2 * 0.4 * 0.5 = 0.4)
1805        let c = compose_layers(false, base, &[(Colour, Overlay, [0.5, 0.5, 0.5, 1.0])]);
1806        assert!((c[0] - 0.8).abs() < 1e-6 && (c[1] - 0.4).abs() < 1e-6);
1807        // an opacity layer: brightness (0.2 + 0.4 + 0.6) / 3 = 0.4 multiplies the opacity at full weight; colour kept
1808        let c = compose_layers(false, base, &[(Opacity, Multiply, [0.2, 0.4, 0.6, 1.0])]);
1809        assert_eq!(c[..3], base[..3]);
1810        assert!((c[3] - 0.2).abs() < 1e-6);
1811        // premultiplied: the base's colour by its opacity, the laid layer's (and so its brightness) by its own
1812        let c = compose_layers(true, base, &[(Opacity, Add, [1.0, 1.0, 1.0, 0.5])]);
1813        assert!((c[0] - 0.4).abs() < 1e-6 && (c[3] - 0.75).abs() < 1e-6);
1814        // layers apply in order
1815        let c = compose_layers(
1816            false,
1817            base,
1818            &[
1819                (Colour, Add, [0.2, 0.2, 0.2, 1.0]),
1820                (Colour, Multiply, [0.5, 0.5, 0.5, 1.0]),
1821            ],
1822        );
1823        assert!((c[0] - 0.5).abs() < 1e-6);
1824    }
1825}