Skip to main content

dinamika_cpu/paint/
pattern.rs

1//! Texture shader: fills with a [`Pixmap`] image with tiling and a
2//! transformation. The basis for sprites, backgrounds and patterns.
3
4use std::sync::Arc;
5
6use crate::color::{Color, PremultipliedColor};
7use crate::geometry::{Point, Transform};
8use crate::pixmap::Pixmap;
9
10use super::gradient::SpreadMode;
11use super::Shader;
12
13/// Interpolation quality when sampling a [`Pattern`] texture.
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15pub enum FilterQuality {
16    /// Nearest pixel — sharp, no smoothing (fast).
17    #[default]
18    Nearest,
19    /// Bilinear interpolation of four neighbors — soft.
20    Bilinear,
21}
22
23/// A texture shader: fills with a [`Pixmap`] image with tiling and a
24/// transformation. The basis for sprites, backgrounds and patterns.
25#[derive(Clone)]
26pub struct Pattern {
27    pixmap: Arc<Pixmap>,
28    spread: SpreadMode,
29    filter: FilterQuality,
30    /// Overall transparency multiplier `0..=1`.
31    opacity: f32,
32    inv_transform: Transform,
33}
34
35impl std::fmt::Debug for Pattern {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("Pattern")
38            .field("size", &(self.pixmap.width(), self.pixmap.height()))
39            .field("spread", &self.spread)
40            .field("filter", &self.filter)
41            .field("opacity", &self.opacity)
42            .finish()
43    }
44}
45
46impl Pattern {
47    /// Creates a texture shader from a shared ([`Arc`]) image. `transform`
48    /// maps texture pixel coordinates to canvas coordinates. `None` if the
49    /// matrix is singular.
50    #[allow(clippy::new_ret_no_self)] // as with gradients: the constructor returns a Shader
51    pub fn new(
52        pixmap: Arc<Pixmap>,
53        spread: SpreadMode,
54        filter: FilterQuality,
55        opacity: f32,
56        transform: Transform,
57    ) -> Option<Shader> {
58        let inv_transform = transform.invert()?;
59        Some(Shader::Pattern(Pattern {
60            pixmap,
61            spread,
62            filter,
63            opacity: opacity.clamp(0.0, 1.0),
64            inv_transform,
65        }))
66    }
67
68    /// Samples a premultiplied texture pixel by integer indices, taking into
69    /// account the tiling mode on each axis.
70    #[inline]
71    fn texel(&self, ix: i32, iy: i32) -> PremultipliedColor {
72        let w = self.pixmap.width() as i32;
73        let h = self.pixmap.height() as i32;
74        let x = wrap_index(ix, w, self.spread);
75        let y = wrap_index(iy, h, self.spread);
76        let px = self.pixmap.pixel(x as u32, y as u32).unwrap();
77        PremultipliedColor {
78            r: px.red() as f32 / 255.0,
79            g: px.green() as f32 / 255.0,
80            b: px.blue() as f32 / 255.0,
81            a: px.alpha() as f32 / 255.0,
82        }
83    }
84
85    pub(super) fn color_at(&self, p: Point) -> Color {
86        if self.pixmap.width() == 0 || self.pixmap.height() == 0 {
87            return Color::TRANSPARENT;
88        }
89        self.color_for(self.inv_transform.map_point(p))
90    }
91
92    /// Shades a horizontal run of `len` pixels starting at pixel `(x, y)`,
93    /// appending one [`Color`] per pixel to `out`. The inverse transform is
94    /// applied once at the run start; each pixel then advances the mapped
95    /// texture point by a constant delta — see [`super::Shader::shade_span`].
96    pub(super) fn shade_span(&self, x: usize, y: usize, len: usize, out: &mut Vec<Color>) {
97        if self.pixmap.width() == 0 || self.pixmap.height() == 0 {
98            out.extend(std::iter::repeat_n(Color::TRANSPARENT, len));
99            return;
100        }
101        let mut tp = self.inv_transform.map_point(Point::new(x as f32 + 0.5, y as f32 + 0.5));
102        // One pixel step to the right in screen space advances the mapped
103        // texture point by the inverse transform's first column.
104        let step = Point::new(self.inv_transform.sx, self.inv_transform.ky);
105        for _ in 0..len {
106            out.push(self.color_for(tp));
107            tp = tp + step;
108        }
109    }
110
111    /// Samples the texture at an already-mapped texture-space point `tp`,
112    /// returning a straight (non-premultiplied) color.
113    fn color_for(&self, tp: Point) -> Color {
114        let premul = match self.filter {
115            FilterQuality::Nearest => self.texel(tp.x.floor() as i32, tp.y.floor() as i32),
116            FilterQuality::Bilinear => {
117                // Texel centers are at half-pixel positions.
118                let fx = tp.x - 0.5;
119                let fy = tp.y - 0.5;
120                let x0 = fx.floor();
121                let y0 = fy.floor();
122                let tx = fx - x0;
123                let ty = fy - y0;
124                let (x0, y0) = (x0 as i32, y0 as i32);
125                let c00 = self.texel(x0, y0);
126                let c10 = self.texel(x0 + 1, y0);
127                let c01 = self.texel(x0, y0 + 1);
128                let c11 = self.texel(x0 + 1, y0 + 1);
129                let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
130                let mix = |a: PremultipliedColor, b: PremultipliedColor, t: f32| PremultipliedColor {
131                    r: lerp(a.r, b.r, t),
132                    g: lerp(a.g, b.g, t),
133                    b: lerp(a.b, b.b, t),
134                    a: lerp(a.a, b.a, t),
135                };
136                let top = mix(c00, c10, tx);
137                let bot = mix(c01, c11, tx);
138                mix(top, bot, ty)
139            }
140        };
141        // Return a straight (non-premultiplied) color — the fill pipeline will
142        // multiply by alpha itself. The pattern's overall transparency is in the alpha.
143        let a = premul.a;
144        if a <= 0.0 {
145            return Color::from_rgba(0.0, 0.0, 0.0, 0.0);
146        }
147        Color::from_rgba(premul.r / a, premul.g / a, premul.b / a, a * self.opacity)
148    }
149}
150
151/// Brings a texel index into the valid range `[0, size)` by the tiling mode.
152#[inline]
153fn wrap_index(i: i32, size: i32, spread: SpreadMode) -> i32 {
154    if size <= 0 {
155        return 0;
156    }
157    match spread {
158        SpreadMode::Pad => i.clamp(0, size - 1),
159        SpreadMode::Repeat => i.rem_euclid(size),
160        SpreadMode::Reflect => {
161            let period = 2 * size;
162            let m = i.rem_euclid(period);
163            if m < size {
164                m
165            } else {
166                period - 1 - m
167            }
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::pixmap::Pixmap;
176
177    #[test]
178    fn pattern_samples_texture() {
179        let mut tex = Pixmap::new(2, 1).unwrap();
180        tex.fill(Color::from_rgba8(0, 0, 0, 0));
181        // left pixel — red, right — blue
182        {
183            let d = tex.data_mut();
184            d[0..4].copy_from_slice(&[255, 0, 0, 255]);
185            d[4..8].copy_from_slice(&[0, 0, 255, 255]);
186        }
187        let shader = Pattern::new(
188            Arc::new(tex),
189            SpreadMode::Repeat,
190            FilterQuality::Nearest,
191            1.0,
192            Transform::identity(),
193        )
194        .unwrap();
195        let left = shader.color_at(0.5, 0.5);
196        let right = shader.color_at(1.5, 0.5);
197        assert!(left.red() > 0.9 && left.blue() < 0.1, "left={left:?}");
198        assert!(right.blue() > 0.9 && right.red() < 0.1, "right={right:?}");
199        // Repeat tiling: x=2.5 is the left one again (red)
200        let wrapped = shader.color_at(2.5, 0.5);
201        assert!(wrapped.red() > 0.9, "wrapped={wrapped:?}");
202    }
203
204    /// Batched [`Shader::shade_span`] must agree with per-pixel
205    /// [`Shader::color_at`] for a pattern under a transform.
206    #[test]
207    fn shade_span_matches_color_at() {
208        let mut tex = Pixmap::new(4, 4).unwrap();
209        for (i, px) in tex.data_mut().chunks_exact_mut(4).enumerate() {
210            px.copy_from_slice(&[(i * 16) as u8, 32, 200, 255]);
211        }
212        let transform = Transform::from_translate(2.0, 1.0)
213            .pre_concat(Transform::from_rotate(15.0))
214            .pre_concat(Transform::from_scale(0.6, 0.9));
215        let shader = Pattern::new(
216            Arc::new(tex),
217            SpreadMode::Repeat,
218            FilterQuality::Bilinear,
219            0.75,
220            transform,
221        )
222        .unwrap();
223
224        let (x0, y, len) = (3usize, 6usize, 20usize);
225        let mut span = Vec::new();
226        shader.shade_span(x0, y, len, &mut span);
227        assert_eq!(span.len(), len);
228        for (i, c) in span.iter().enumerate() {
229            let want = shader.color_at((x0 + i) as f32 + 0.5, y as f32 + 0.5);
230            assert!((c.red() - want.red()).abs() < 1e-4, "red @{i}: {c:?} vs {want:?}");
231            assert!((c.green() - want.green()).abs() < 1e-4, "green @{i}");
232            assert!((c.blue() - want.blue()).abs() < 1e-4, "blue @{i}");
233            assert!((c.alpha() - want.alpha()).abs() < 1e-4, "alpha @{i}");
234        }
235    }
236}