Skip to main content

motion_content/particles/components/
image.rs

1//! Cached weighted image distributions with bounded preparation.
2
3use super::*;
4
5/// Density policy, independent of inherited color.
6#[derive(Clone, Copy, Debug)]
7pub enum ImageDensity {
8    /// Premultiplied alpha density.
9    Alpha,
10    /// Nonnegative linear luminance, including source alpha.
11    Luminance,
12}
13
14#[derive(Clone)]
15pub(super) struct ImageDistribution {
16    image: Arc<crate::ImageAsset>,
17    bounds: Rect,
18    ends: Arc<[f64]>,
19    total: f64,
20}
21
22impl ImageEmitter {
23    /// Prepares a CDF under logical budgets; zero-weight images fail explicitly.
24    pub fn new(
25        image: Arc<crate::ImageAsset>,
26        bounds: Rect,
27        density: ImageDensity,
28        limits: SimulationLimits,
29    ) -> Result<Self, ValidationError> {
30        motion_core::drawing::validate_rect(bounds, "particles.image", "bounds")?;
31        let count = u64::from(image.width()) * u64::from(image.height());
32        if bounds.width() <= 0.0
33            || bounds.height() <= 0.0
34            || count > 1_000_000
35            || count * 64 > limits.max_bytes
36            || count * 8 > limits.max_work
37        {
38            return Err(limit("Image emitter preparation allowance exhausted"));
39        }
40        let mut ends = Vec::new();
41        ends.try_reserve_exact(count as usize)
42            .map_err(|_| limit("Image CDF allocation failed"))?;
43        let mut total = 0.0;
44        for row in 0..image.height() {
45            for column in 0..image.width() {
46                let pixel = image.levels()[0]
47                    .pixel(column, row)
48                    .ok_or_else(|| invalid("image", "Missing source pixel"))?;
49                let weight = match density {
50                    ImageDensity::Alpha => f64::from(pixel[3]),
51                    ImageDensity::Luminance => (f64::from(pixel[0]) * 0.2126
52                        + f64::from(pixel[1]) * 0.7152
53                        + f64::from(pixel[2]) * 0.0722)
54                        .max(0.0),
55                };
56                let next = total + weight;
57                if !next.is_finite() || (weight > 0.0 && next == total) {
58                    return Err(invalid(
59                        "image",
60                        "Image density cannot represent finite positive weights",
61                    ));
62                }
63                total = next;
64                ends.push(total);
65            }
66        }
67        if total == 0.0 {
68            return Err(invalid(
69                "image",
70                "Image contains no positive emission density",
71            ));
72        }
73        Ok(Self(EmitterSettings::new(EmitterSource::Image(Arc::new(
74            ImageDistribution {
75                image,
76                bounds,
77                ends: ends.into(),
78                total,
79            },
80        )))))
81    }
82}
83
84impl ImageDistribution {
85    pub(super) fn sample(&self, random: &mut Random) -> Result<(Point, [f32; 4]), ValidationError> {
86        let choice = random.next_float() * self.total;
87        let index = self
88            .ends
89            .partition_point(|end| *end <= choice)
90            .min(self.ends.len() - 1);
91        let column = index as u32 % self.image.width();
92        let row = index as u32 / self.image.width();
93        let point = Point::new(
94            self.bounds.left
95                + (f64::from(column) + random.next_float()) / f64::from(self.image.width())
96                    * self.bounds.width(),
97            self.bounds.top
98                + (f64::from(row) + random.next_float()) / f64::from(self.image.height())
99                    * self.bounds.height(),
100        );
101        Ok((
102            point,
103            self.image.levels()[0]
104                .pixel(column, row)
105                .ok_or_else(|| invalid("image", "Image CDF pixel outside source"))?,
106        ))
107    }
108}