use super::lerp_color;
use crate::pipe::Pattern;
pub struct RadialPattern {
color0: [u8; 3],
color1: [u8; 3],
c0x: f64,
c0y: f64,
dcx: f64,
dcy: f64,
r0: f64,
dr: f64,
t0: f64,
t1: f64,
extend_start: bool,
extend_end: bool,
a: f64,
}
impl RadialPattern {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "mirrors PDF shading dict: 2 colors + 2 circles (cx,cy,r each) + t range + 2 extend flags"
)]
pub fn new(
color0: [u8; 3],
color1: [u8; 3],
c0x: f64,
c0y: f64,
r0: f64,
c1x: f64,
c1y: f64,
r1: f64,
t0: f64,
t1: f64,
extend_start: bool,
extend_end: bool,
) -> Self {
let dcx = c1x - c0x;
let dcy = c1y - c0y;
let dr = r1 - r0;
let a = dr.mul_add(-dr, dcx.mul_add(dcx, dcy * dcy));
Self {
color0,
color1,
c0x,
c0y,
dcx,
dcy,
r0,
dr,
t0,
t1,
extend_start,
extend_end,
a,
}
}
fn t_for(&self, xi: i32, yi: i32) -> Option<f64> {
let rel_x = f64::from(xi) - self.c0x;
let rel_y = f64::from(yi) - self.c0y;
let b = self
.r0
.mul_add(-self.dr, rel_x.mul_add(self.dcx, rel_y * self.dcy));
let c = self
.r0
.mul_add(-self.r0, rel_x.mul_add(rel_x, rel_y * rel_y));
let t = if self.a.abs() < 1e-12 {
if b.abs() < 1e-12 {
return None; }
-c / (b + b)
} else {
let disc = b.mul_add(b, -(self.a * c));
if disc < 0.0 {
return None; }
let sq = disc.sqrt();
f64::max((-b + sq) / self.a, (-b - sq) / self.a)
};
let (lo, hi) = if self.t0 <= self.t1 {
(self.t0, self.t1)
} else {
(self.t1, self.t0)
};
if t < lo {
if self.extend_start {
Some(self.t0)
} else {
None
}
} else if t > hi {
if self.extend_end { Some(self.t1) } else { None }
} else {
Some(t)
}
}
}
impl Pattern for RadialPattern {
fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]) {
let t_span = self.t1 - self.t0;
let mut off = 0usize;
for xi in x0..=x1 {
if let Some(t) = self.t_for(xi, y) {
let frac = if t_span.abs() < f64::EPSILON {
0_u32
} else {
#[expect(clippy::cast_sign_loss, reason = "value clamped to 0.0..=256.0")]
#[expect(clippy::cast_possible_truncation, reason = "value ≤ 256")]
{
(((t - self.t0) / t_span).clamp(0.0, 1.0) * 256.0) as u32
}
};
lerp_color(self.color0, self.color1, frac, &mut out[off..off + 3]);
} else {
out[off] = 0;
out[off + 1] = 0;
out[off + 2] = 0;
}
off += 3;
}
}
fn is_static_color(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_concentric() -> RadialPattern {
RadialPattern::new(
[0, 0, 0],
[255, 255, 255],
4.0,
4.0,
0.0,
4.0,
4.0,
4.0,
0.0,
1.0,
true,
true,
)
}
#[test]
fn centre_is_color0() {
let p = make_concentric();
let mut out = [42u8; 3];
p.fill_span(4, 4, 4, &mut out);
assert!(out[0] < 10, "centre should be near-black, got {}", out[0]);
}
#[test]
fn outer_ring_is_color1() {
let p = make_concentric();
let mut out = [0u8; 3];
p.fill_span(4, 8, 8, &mut out); assert!(
out[0] > 240,
"outer ring should be near-white, got {}",
out[0]
);
}
#[test]
fn pixel_on_inner_circle_maps_to_color0() {
let p = RadialPattern::new(
[255, 0, 0],
[0, 0, 255],
4.0,
4.0,
2.0,
4.0,
4.0,
6.0,
0.0,
1.0,
false,
false,
);
let mut out = [0u8; 3];
p.fill_span(4, 6, 6, &mut out);
assert!(out[0] > 240, "inner circle should be near color0 (red)");
assert!(out[2] < 20, "inner circle should have near-zero blue");
}
#[test]
fn no_real_intersection_writes_zeros() {
let p = RadialPattern::new(
[255, 0, 0],
[0, 255, 0],
0.0,
0.0,
1.0,
10.0,
0.0,
1.0,
0.0,
1.0,
false,
false,
);
let mut out = [42u8; 3]; p.fill_span(100, 100, 100, &mut out);
assert_eq!(out, [0, 0, 0], "no intersection should write zeros");
}
#[test]
fn degenerate_a_linear_fallback() {
let p = RadialPattern::new(
[0, 0, 0],
[255, 255, 255],
0.0,
0.0,
0.0,
3.0,
4.0,
5.0,
0.0,
1.0,
true,
true,
);
let mut out = [0u8; 15]; p.fill_span(0, 0, 4, &mut out);
}
#[test]
fn extend_clamps_outside_to_endpoints() {
let p = make_concentric();
let mut out = [42u8; 3];
p.fill_span(4, 14, 14, &mut out);
assert!(
out[0] > 240,
"beyond outer with extend should clamp to color1 (white)"
);
}
}