Skip to main content

rasterrocket_render/shading/
mod.rs

1//! Shaded fills — gradient patterns and Gouraud-shaded triangles.
2//!
3//! Replaces `Splash::shadedFill` and `Splash::gouraudTriangleShadedFill`.
4//!
5//! # Module layout (named for orientation; visibility varies)
6//! - `axial`    — [`crate::AxialPattern`]: linear gradient along an axis vector
7//! - `radial`   — [`crate::RadialPattern`]: gradient between two circles
8//! - `function` — [`crate::FunctionPattern`]: PDF function sampled per pixel
9//! - `gouraud`  — `gouraud_triangle_fill`: vertex-coloured triangle scan
10//!
11//! # Entry point
12//!
13//! [`shaded_fill`] is a thin wrapper around [`fill::fill`] / [`fill::eo_fill`]
14//! that passes the pattern as a [`PipeSrc::Pattern`].  The gradient patterns
15//! implement [`Pattern`] and are injected by the caller (typically `pdf_bridge`).
16
17pub mod axial;
18pub mod function;
19pub mod gouraud;
20pub mod radial;
21
22use crate::bitmap::Bitmap;
23use crate::clip::Clip;
24use crate::fill;
25use crate::path::Path;
26use crate::pipe::{Pattern, PipeSrc, PipeState};
27use color::Pixel;
28use color::convert::lerp_u8;
29
30/// Linearly interpolate an RGB triple from `a` to `b` with `frac ∈ [0, 256]`.
31///
32/// Shared by [`axial`] and [`radial`] to avoid duplicating the per-channel lerp.
33/// `frac = 0` → `a`; `frac = 256` → `b`.
34#[inline]
35pub(super) fn lerp_color(a: [u8; 3], b: [u8; 3], frac: u32, out: &mut [u8]) {
36    debug_assert_eq!(out.len(), 3, "lerp_color: out must be exactly 3 bytes");
37    out[0] = lerp_u8(a[0], b[0], frac);
38    out[1] = lerp_u8(a[1], b[1], frac);
39    out[2] = lerp_u8(a[2], b[2], frac);
40}
41
42/// Fill `path` using a shading pattern as the colour source.
43///
44/// Equivalent to `Splash::shadedFill`.  The path defines the shading's bounding
45/// shape; `pattern` supplies per-pixel colour via the [`Pattern`] trait.
46/// `eo` selects even-odd vs. non-zero winding rule.
47/// The caller sets `pipe.a_input` to control fill/stroke opacity.
48#[expect(
49    clippy::too_many_arguments,
50    reason = "mirrors shadedFill signature: bitmap+clip+path+pipe+pattern+matrix+flatness+aa+eo"
51)]
52pub fn shaded_fill<P: Pixel>(
53    bitmap: &mut Bitmap<P>,
54    clip: &Clip,
55    path: &Path,
56    pipe: &PipeState<'_>,
57    pattern: &dyn Pattern,
58    matrix: &[f64; 6],
59    flatness: f64,
60    vector_antialias: bool,
61    eo: bool,
62) {
63    let src = PipeSrc::Pattern(pattern);
64    if eo {
65        fill::eo_fill::<P>(
66            bitmap,
67            clip,
68            path,
69            pipe,
70            &src,
71            matrix,
72            flatness,
73            vector_antialias,
74        );
75    } else {
76        fill::fill::<P>(
77            bitmap,
78            clip,
79            path,
80            pipe,
81            &src,
82            matrix,
83            flatness,
84            vector_antialias,
85        );
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::bitmap::Bitmap;
93    use crate::clip::Clip;
94    use crate::path::PathBuilder;
95    use crate::shading::axial::AxialPattern;
96    use crate::testutil::{identity_matrix, rect_path, simple_pipe};
97    use color::Rgb8;
98
99    #[test]
100    fn shaded_fill_axial_paints_interior() {
101        // 8×8 bitmap; fill (1,1)→(6,6) with a left→right gradient black→white.
102        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
103        let clip = Clip::new(0.0, 0.0, 7.999, 7.999, false);
104        let pipe = simple_pipe();
105        let path = rect_path(1.0, 1.0, 6.0, 6.0);
106
107        let pattern = AxialPattern::new(
108            [0u8, 0, 0],
109            [255u8, 255, 255],
110            1.0,
111            3.5,
112            6.0,
113            3.5,
114            0.0,
115            1.0,
116            false,
117            false,
118        );
119
120        shaded_fill::<Rgb8>(
121            &mut bmp,
122            &clip,
123            &path,
124            &pipe,
125            &pattern,
126            &identity_matrix(),
127            1.0,
128            false,
129            false,
130        );
131
132        let r3 = bmp.row(3);
133        assert!(r3[1].r < 60, "x=1 should be near-black (got {})", r3[1].r);
134        assert!(r3[5].r > 180, "x=5 should be near-white (got {})", r3[5].r);
135    }
136
137    #[test]
138    fn shaded_fill_eo_and_nonzero_both_work() {
139        // Same rect path, eo=true vs eo=false — single contour, both should fill.
140        let mut bmp_nz: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
141        let mut bmp_eo: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
142        let clip = Clip::new(0.0, 0.0, 7.999, 7.999, false);
143        let pipe = simple_pipe();
144        let path = rect_path(1.0, 1.0, 6.0, 6.0);
145        let pattern = AxialPattern::new(
146            [200u8, 0, 0],
147            [200u8, 0, 0],
148            0.0,
149            0.0,
150            1.0,
151            0.0,
152            0.0,
153            1.0,
154            true,
155            true,
156        );
157        shaded_fill::<Rgb8>(
158            &mut bmp_nz,
159            &clip,
160            &path,
161            &pipe,
162            &pattern,
163            &identity_matrix(),
164            1.0,
165            false,
166            false,
167        );
168        shaded_fill::<Rgb8>(
169            &mut bmp_eo,
170            &clip,
171            &path,
172            &pipe,
173            &pattern,
174            &identity_matrix(),
175            1.0,
176            false,
177            true,
178        );
179        assert_eq!(
180            bmp_nz.row(3)[3].r,
181            bmp_eo.row(3)[3].r,
182            "non-zero and eo must agree for a simple convex path"
183        );
184    }
185
186    #[test]
187    fn shaded_fill_empty_path_is_noop() {
188        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
189        let clip = Clip::new(0.0, 0.0, 7.999, 7.999, false);
190        let pipe = simple_pipe();
191        let path = PathBuilder::new().build();
192        let pattern = AxialPattern::new(
193            [255u8, 0, 0],
194            [0u8, 255, 0],
195            0.0,
196            0.0,
197            8.0,
198            0.0,
199            0.0,
200            1.0,
201            false,
202            false,
203        );
204        shaded_fill::<Rgb8>(
205            &mut bmp,
206            &clip,
207            &path,
208            &pipe,
209            &pattern,
210            &identity_matrix(),
211            1.0,
212            false,
213            false,
214        );
215        assert_eq!(bmp.row(4)[4].r, 0, "empty path must not paint");
216    }
217}