use kurbo::{Affine, Point};
use pdfrum_page::Axial;
use crate::pixmap::Pixmap;
use crate::shading::steps::ColorSteps;
pub fn draw(dest: &mut Pixmap, axial: &Axial, steps: &ColorSteps, to_bitmap: Affine) {
let Some(inverse) = invert(to_bitmap) else {
return;
};
let x_span = axial.end.x - axial.start.x;
let y_span = axial.end.y - axial.start.y;
let axis_len_square = x_span * x_span + y_span * y_span;
for row in 0..dest.height() {
for col in 0..dest.width() {
let pos = inverse * Point::new(f64::from(col), f64::from(row));
let scale = ((pos.x - axial.start.x) * x_span + (pos.y - axial.start.y) * y_span)
/ axis_len_square;
#[expect(
clippy::cast_possible_truncation,
reason = "the ramp lookup ports the truncating cast"
)]
let Some(color) = steps.lookup(scale as f32, axial.extend_start, axial.extend_end)
else {
continue;
};
dest.set_pixel(col, row, crate::pixmap::premultiply(color.to_peniko()));
}
}
}
fn invert(m: Affine) -> Option<Affine> {
let det = m.determinant();
(det != 0.0 && det.is_finite()).then(|| m.inverse())
}
#[cfg(test)]
mod tests {
use pdfrum_page::Rgb;
use super::*;
use crate::shading::steps::STEPS;
fn black_to_white() -> ColorSteps {
let mut colors = [Rgb::BLACK; STEPS];
for (i, c) in colors.iter_mut().enumerate() {
#[expect(clippy::cast_precision_loss, reason = "i < 256 is exact")]
let v = i as f32 / 255.0;
*c = Rgb { r: v, g: v, b: v };
}
ColorSteps::from_colors(colors, 255)
}
fn axis(extend: (bool, bool)) -> Axial {
Axial {
start: Point::new(0.0, 0.0),
end: Point::new(8.0, 0.0),
t_min: 0.0,
t_max: 1.0,
extend_start: extend.0,
extend_end: extend.1,
}
}
#[test]
fn ramps_along_the_axis_by_pixel_corner() {
let mut p = Pixmap::new(8, 1);
draw(
&mut p,
&axis((true, true)),
&black_to_white(),
Affine::IDENTITY,
);
assert_eq!(p.pixel(0, 0).map(|px| px[0]), Some(0));
assert_eq!(p.pixel(4, 0).map(|px| px[0]), Some(127));
assert_eq!(p.pixel(7, 0).map(|px| px[0]), Some(223));
}
#[test]
fn unextended_ends_leave_pixels_untouched() {
let mut p = Pixmap::filled(12, 1, peniko::Color::from_rgba8(9, 9, 9, 255));
let shifted = Affine::translate((4.0, 0.0));
draw(&mut p, &axis((false, false)), &black_to_white(), shifted);
assert_eq!(p.pixel(0, 0).map(|px| px[0]), Some(9));
assert_eq!(p.pixel(8, 0).map(|px| px[0]), Some(127));
}
#[test]
fn extended_ends_clamp_to_the_ramp() {
let mut p = Pixmap::filled(12, 1, peniko::Color::from_rgba8(9, 9, 9, 255));
draw(
&mut p,
&axis((true, true)),
&black_to_white(),
Affine::translate((4.0, 0.0)),
);
assert_eq!(
p.pixel(0, 0).map(|px| px[0]),
Some(0),
"clamped to the ramp's start"
);
assert_eq!(p.pixel(11, 0).map(|px| px[0]), Some(223));
let mut p = Pixmap::filled(20, 1, peniko::Color::from_rgba8(9, 9, 9, 255));
draw(
&mut p,
&axis((true, true)),
&black_to_white(),
Affine::IDENTITY,
);
assert_eq!(
p.pixel(19, 0).map(|px| px[0]),
Some(255),
"clamped to its end"
);
}
#[test]
fn a_degenerate_axis_paints_nothing_or_clamps() {
let degenerate = Axial {
end: Point::new(0.0, 0.0),
extend_start: false,
extend_end: false,
..axis((false, false))
};
let mut p = Pixmap::filled(4, 1, peniko::Color::from_rgba8(9, 9, 9, 255));
draw(&mut p, °enerate, &black_to_white(), Affine::IDENTITY);
for col in 0..4 {
assert_eq!(p.pixel(col, 0).map(|px| px[0]), Some(9));
}
}
#[test]
fn a_singular_matrix_is_declined() {
let mut p = Pixmap::filled(4, 1, peniko::Color::from_rgba8(9, 9, 9, 255));
draw(
&mut p,
&axis((true, true)),
&black_to_white(),
Affine::new([0.0; 6]),
);
assert_eq!(p.pixel(0, 0).map(|px| px[0]), Some(9));
}
}