rasterrocket_render/shading/
radial.rs1use super::lerp_color;
17use crate::pipe::Pattern;
18
19pub struct RadialPattern {
21 color0: [u8; 3],
22 color1: [u8; 3],
23 c0x: f64,
24 c0y: f64,
25 dcx: f64,
27 dcy: f64,
28 r0: f64,
29 dr: f64,
31 t0: f64,
32 t1: f64,
33 extend_start: bool,
34 extend_end: bool,
35 a: f64,
38}
39
40impl RadialPattern {
41 #[must_use]
50 #[expect(
51 clippy::too_many_arguments,
52 reason = "mirrors PDF shading dict: 2 colors + 2 circles (cx,cy,r each) + t range + 2 extend flags"
53 )]
54 pub fn new(
55 color0: [u8; 3],
56 color1: [u8; 3],
57 c0x: f64,
58 c0y: f64,
59 r0: f64,
60 c1x: f64,
61 c1y: f64,
62 r1: f64,
63 t0: f64,
64 t1: f64,
65 extend_start: bool,
66 extend_end: bool,
67 ) -> Self {
68 let dcx = c1x - c0x;
69 let dcy = c1y - c0y;
70 let dr = r1 - r0;
71 let a = dr.mul_add(-dr, dcx.mul_add(dcx, dcy * dcy));
73 Self {
74 color0,
75 color1,
76 c0x,
77 c0y,
78 dcx,
79 dcy,
80 r0,
81 dr,
82 t0,
83 t1,
84 extend_start,
85 extend_end,
86 a,
87 }
88 }
89
90 fn t_for(&self, xi: i32, yi: i32) -> Option<f64> {
98 let rel_x = f64::from(xi) - self.c0x;
99 let rel_y = f64::from(yi) - self.c0y;
100
101 let b = self
102 .r0
103 .mul_add(-self.dr, rel_x.mul_add(self.dcx, rel_y * self.dcy));
104 let c = self
105 .r0
106 .mul_add(-self.r0, rel_x.mul_add(rel_x, rel_y * rel_y));
107
108 let t = if self.a.abs() < 1e-12 {
109 if b.abs() < 1e-12 {
111 return None; }
113 -c / (b + b)
114 } else {
115 let disc = b.mul_add(b, -(self.a * c));
116 if disc < 0.0 {
117 return None; }
119 let sq = disc.sqrt();
120 f64::max((-b + sq) / self.a, (-b - sq) / self.a)
122 };
123
124 let (lo, hi) = if self.t0 <= self.t1 {
126 (self.t0, self.t1)
127 } else {
128 (self.t1, self.t0)
129 };
130 if t < lo {
131 if self.extend_start {
132 Some(self.t0)
133 } else {
134 None
135 }
136 } else if t > hi {
137 if self.extend_end { Some(self.t1) } else { None }
138 } else {
139 Some(t)
140 }
141 }
142}
143
144impl Pattern for RadialPattern {
145 fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]) {
146 let t_span = self.t1 - self.t0;
148 let mut off = 0usize;
149 for xi in x0..=x1 {
150 if let Some(t) = self.t_for(xi, y) {
151 let frac = if t_span.abs() < f64::EPSILON {
152 0_u32
153 } else {
154 #[expect(clippy::cast_sign_loss, reason = "value clamped to 0.0..=256.0")]
155 #[expect(clippy::cast_possible_truncation, reason = "value ≤ 256")]
156 {
157 (((t - self.t0) / t_span).clamp(0.0, 1.0) * 256.0) as u32
158 }
159 };
160 lerp_color(self.color0, self.color1, frac, &mut out[off..off + 3]);
161 } else {
162 out[off] = 0;
163 out[off + 1] = 0;
164 out[off + 2] = 0;
165 }
166 off += 3;
167 }
168 }
169
170 fn is_static_color(&self) -> bool {
171 false
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn make_concentric() -> RadialPattern {
181 RadialPattern::new(
182 [0, 0, 0],
183 [255, 255, 255],
184 4.0,
185 4.0,
186 0.0,
187 4.0,
188 4.0,
189 4.0,
190 0.0,
191 1.0,
192 true,
193 true,
194 )
195 }
196
197 #[test]
198 fn centre_is_color0() {
199 let p = make_concentric();
200 let mut out = [42u8; 3];
201 p.fill_span(4, 4, 4, &mut out);
202 assert!(out[0] < 10, "centre should be near-black, got {}", out[0]);
204 }
205
206 #[test]
207 fn outer_ring_is_color1() {
208 let p = make_concentric();
209 let mut out = [0u8; 3];
210 p.fill_span(4, 8, 8, &mut out); assert!(
212 out[0] > 240,
213 "outer ring should be near-white, got {}",
214 out[0]
215 );
216 }
217
218 #[test]
219 fn pixel_on_inner_circle_maps_to_color0() {
220 let p = RadialPattern::new(
222 [255, 0, 0],
223 [0, 0, 255],
224 4.0,
225 4.0,
226 2.0,
227 4.0,
228 4.0,
229 6.0,
230 0.0,
231 1.0,
232 false,
233 false,
234 );
235 let mut out = [0u8; 3];
236 p.fill_span(4, 6, 6, &mut out);
238 assert!(out[0] > 240, "inner circle should be near color0 (red)");
239 assert!(out[2] < 20, "inner circle should have near-zero blue");
240 }
241
242 #[test]
243 fn no_real_intersection_writes_zeros() {
244 let p = RadialPattern::new(
246 [255, 0, 0],
247 [0, 255, 0],
248 0.0,
249 0.0,
250 1.0,
251 10.0,
252 0.0,
253 1.0,
254 0.0,
255 1.0,
256 false,
257 false,
258 );
259 let mut out = [42u8; 3]; p.fill_span(100, 100, 100, &mut out);
261 assert_eq!(out, [0, 0, 0], "no intersection should write zeros");
262 }
263
264 #[test]
265 fn degenerate_a_linear_fallback() {
266 let p = RadialPattern::new(
269 [0, 0, 0],
270 [255, 255, 255],
271 0.0,
272 0.0,
273 0.0,
274 3.0,
275 4.0,
276 5.0,
277 0.0,
278 1.0,
279 true,
280 true,
281 );
282 let mut out = [0u8; 15]; p.fill_span(0, 0, 4, &mut out);
285 }
286
287 #[test]
288 fn extend_clamps_outside_to_endpoints() {
289 let p = make_concentric();
290 let mut out = [42u8; 3];
291 p.fill_span(4, 14, 14, &mut out);
293 assert!(
294 out[0] > 240,
295 "beyond outer with extend should clamp to color1 (white)"
296 );
297 }
298}