rasterrocket_render/shading/
axial.rs1use super::lerp_color;
14use crate::pipe::Pattern;
15
16pub struct AxialPattern {
21 color0: [u8; 3],
22 color1: [u8; 3],
23 ax: f64,
24 ay: f64,
25 p0x: f64,
26 p0y: f64,
27 inv_len_sq: f64,
29 t0: f64,
30 t1: f64,
31 extend_start: bool,
32 extend_end: bool,
33}
34
35impl AxialPattern {
36 #[must_use]
49 #[expect(
50 clippy::too_many_arguments,
51 reason = "mirrors PDF shading dict: 2 colors + 2 points + t range + 2 extend flags"
52 )]
53 pub fn new(
54 color0: [u8; 3],
55 color1: [u8; 3],
56 p0x: f64,
57 p0y: f64,
58 p1x: f64,
59 p1y: f64,
60 t0: f64,
61 t1: f64,
62 extend_start: bool,
63 extend_end: bool,
64 ) -> Self {
65 let ax = p1x - p0x;
66 let ay = p1y - p0y;
67 let len_sq = ax.mul_add(ax, ay * ay);
68 let inv_len_sq = if len_sq > 0.0 { 1.0 / len_sq } else { 0.0 };
69 Self {
70 color0,
71 color1,
72 ax,
73 ay,
74 p0x,
75 p0y,
76 inv_len_sq,
77 t0,
78 t1,
79 extend_start,
80 extend_end,
81 }
82 }
83
84 fn t_for(&self, x: i32, y: i32) -> Option<f64> {
89 if self.inv_len_sq == 0.0 {
90 return None;
91 }
92 let dx = f64::from(x) - self.p0x;
93 let dy = f64::from(y) - self.p0y;
94 let t_raw = dx.mul_add(self.ax, dy * self.ay) * self.inv_len_sq;
96 let t = t_raw.mul_add(self.t1 - self.t0, self.t0);
97
98 let (lo, hi) = if self.t0 <= self.t1 {
100 (self.t0, self.t1)
101 } else {
102 (self.t1, self.t0)
103 };
104 if t < lo {
105 if self.extend_start {
106 Some(self.t0)
107 } else {
108 None
109 }
110 } else if t > hi {
111 if self.extend_end { Some(self.t1) } else { None }
112 } else {
113 Some(t)
114 }
115 }
116}
117
118impl Pattern for AxialPattern {
119 fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]) {
120 let t_span = self.t1 - self.t0;
122 let mut off = 0usize;
123 for x in x0..=x1 {
124 if let Some(t) = self.t_for(x, y) {
125 let frac = if t_span.abs() < f64::EPSILON {
127 0_u32
128 } else {
129 #[expect(clippy::cast_sign_loss, reason = "value clamped to 0.0..=256.0")]
130 #[expect(clippy::cast_possible_truncation, reason = "value ≤ 256")]
131 {
132 (((t - self.t0) / t_span).clamp(0.0, 1.0) * 256.0) as u32
133 }
134 };
135 lerp_color(self.color0, self.color1, frac, &mut out[off..off + 3]);
136 } else {
137 out[off] = 0;
138 out[off + 1] = 0;
139 out[off + 2] = 0;
140 }
141 off += 3;
142 }
143 }
144
145 fn is_static_color(&self) -> bool {
146 false
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn make_axial(extend: bool) -> AxialPattern {
155 AxialPattern::new(
156 [0, 0, 0],
157 [255, 255, 255],
158 0.0,
159 0.0,
160 8.0,
161 0.0,
162 0.0,
163 1.0,
164 extend,
165 extend,
166 )
167 }
168
169 #[test]
170 fn midpoint_is_grey() {
171 let p = make_axial(true);
172 let mut out = [0u8; 3];
173 p.fill_span(0, 4, 4, &mut out);
174 assert!(out[0] >= 126 && out[0] <= 129, "mid R={}", out[0]);
175 }
176
177 #[test]
178 fn start_is_black() {
179 let p = make_axial(true);
180 let mut out = [0u8; 3];
181 p.fill_span(0, 0, 0, &mut out);
182 assert_eq!(out[0], 0, "start should be black");
183 }
184
185 #[test]
186 fn end_is_white() {
187 let p = make_axial(true);
188 let mut out = [0u8; 3];
189 p.fill_span(0, 8, 8, &mut out);
190 assert_eq!(out[0], 255, "end should be white");
191 }
192
193 #[test]
194 fn outside_no_extend_writes_zero() {
195 let p = make_axial(false);
196 let mut out = [0u8; 3];
197 p.fill_span(0, -1, -1, &mut out);
198 assert_eq!(
199 out,
200 [0, 0, 0],
201 "before start with no-extend should write zero"
202 );
203 p.fill_span(0, 9, 9, &mut out);
204 assert_eq!(out, [0, 0, 0], "after end with no-extend should write zero");
205 }
206
207 #[test]
208 fn outside_extend_clamps_to_endpoints() {
209 let p = make_axial(true);
210 let mut out = [0u8; 3];
211 p.fill_span(0, -5, -5, &mut out);
212 assert_eq!(out[0], 0, "before start with extend should clamp to color0");
213 p.fill_span(0, 100, 100, &mut out);
214 assert_eq!(out[0], 255, "after end with extend should clamp to color1");
215 }
216
217 #[test]
218 fn degenerate_axis_writes_zeros() {
219 let p = AxialPattern::new(
220 [255, 0, 0],
221 [0, 255, 0],
222 5.0,
223 5.0,
224 5.0,
225 5.0,
226 0.0,
227 1.0,
228 false,
229 false,
230 );
231 let mut out = [42u8; 3];
232 p.fill_span(5, 5, 5, &mut out);
233 assert_eq!(out, [0, 0, 0], "degenerate axis should write zeros");
234 }
235
236 #[test]
237 fn gradient_increases_left_to_right() {
238 let p = make_axial(true);
239 let mut out = vec![0u8; 5 * 3];
240 p.fill_span(0, 0, 4, &mut out);
241 assert!(out[0] < out[3], "gradient R should increase left-to-right");
242 assert!(out[3] < out[6], "gradient R should increase left-to-right");
243 }
244
245 #[test]
246 fn inverted_t_range_reverses_gradient() {
247 let p = AxialPattern::new(
249 [255, 0, 0],
250 [0, 0, 255],
251 0.0,
252 0.0,
253 4.0,
254 0.0,
255 1.0,
256 0.0, true,
258 true,
259 );
260 let mut start = [0u8; 3];
261 let mut end = [0u8; 3];
262 p.fill_span(0, 0, 0, &mut start);
263 p.fill_span(0, 4, 4, &mut end);
264 assert!(start[0] > 200, "x=0 should be near red (color0)");
266 assert!(end[2] > 200, "x=4 should be near blue (color1)");
267 }
268}