use crate::core::Rgba;
use std::ops::Range;
pub struct Canvas<'a> {
pub pixels: &'a mut [u32],
pub width: u32,
pub height: u32,
pub scale: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Premultiplied {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Premultiplied {
pub fn from_straight(color: Rgba) -> Self {
Self { r: color.r * color.a, g: color.g * color.a, b: color.b * color.a, a: color.a }
}
pub fn scaled(self, factor: f32) -> Self {
Self { r: self.r * factor, g: self.g * factor, b: self.b * factor, a: self.a * factor }
}
pub fn mix(self, other: Self, t: f32) -> Self {
let lerp = |from: f32, to: f32| from + (to - from) * t;
Self { r: lerp(self.r, other.r), g: lerp(self.g, other.g), b: lerp(self.b, other.b), a: lerp(self.a, other.a) }
}
}
pub fn pack(red: f32, green: f32, blue: f32) -> u32 {
let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
(channel(red) << 16) | (channel(green) << 8) | channel(blue)
}
fn unpack(pixel: u32) -> (f32, f32, f32) {
let channel = |shift: u32| ((pixel >> shift) & 0xff) as f32 / 255.0;
(channel(16), channel(8), channel(0))
}
#[inline]
pub fn blend(destination: u32, source: Premultiplied) -> u32 {
if source.a >= 0.999 {
return pack(source.r, source.g, source.b);
}
if source.a <= 0.001 {
return destination;
}
let (red, green, blue) = unpack(destination);
let keep = 1.0 - source.a;
pack(source.r + red * keep, source.g + green * keep, source.b + blue * keep)
}
impl Canvas<'_> {
pub fn clear(&mut self, color: Rgba) {
self.pixels.fill(pack(color.r, color.g, color.b));
}
fn span(&self, low: f32, high: f32, limit: u32) -> Range<u32> {
let first = (low * self.scale - 0.5).ceil().max(0.0) as u32;
let last = ((high * self.scale - 0.5).floor() + 1.0).clamp(0.0, limit as f32) as u32;
first.min(last)..last
}
pub fn area(&self, bounds: [f32; 4], margin: f32, clip: [f32; 4]) -> (Range<u32>, Range<u32>) {
let left = (bounds[0] - margin).max(clip[0]);
let top = (bounds[1] - margin).max(clip[1]);
let right = (bounds[0] + bounds[2] + margin).min(clip[0] + clip[2]);
let bottom = (bounds[1] + bounds[3] + margin).min(clip[1] + clip[3]);
(self.span(left, right, self.width), self.span(top, bottom, self.height))
}
#[inline]
pub fn logical(&self, pixel: u32) -> f32 {
(pixel as f32 + 0.5) / self.scale
}
pub fn fill_span(&mut self, y: u32, columns: Range<u32>, packed: u32) {
let row = (y * self.width) as usize;
self.pixels[row + columns.start as usize..row + columns.end as usize].fill(packed);
}
#[inline]
pub fn blend_at(&mut self, x: u32, y: u32, source: Premultiplied) {
let index = (y * self.width + x) as usize;
self.pixels[index] = blend(self.pixels[index], source);
}
}
#[inline]
pub fn rounded_rect_distance(point: (f32, f32), half: (f32, f32), radii: [f32; 4]) -> f32 {
let radius = match (point.0 > 0.0, point.1 > 0.0) {
(false, false) => radii[0],
(true, false) => radii[1],
(true, true) => radii[2],
(false, true) => radii[3],
};
let qx = point.0.abs() - half.0 + radius;
let qy = point.1.abs() - half.1 + radius;
qx.max(qy).min(0.0) + (qx.max(0.0).powi(2) + qy.max(0.0).powi(2)).sqrt() - radius
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn la_mezcla_premultiplicada_coincide_con_la_gpu() {
let white = pack(1.0, 1.0, 1.0);
let half_black = Premultiplied { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
assert_eq!(blend(white, half_black), pack(0.5, 0.5, 0.5));
assert_eq!(blend(white, Premultiplied { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }), 0xff0000);
assert_eq!(blend(white, Premultiplied { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }), white);
}
#[test]
fn los_tramos_usan_el_centro_del_pixel_y_el_recorte() {
let mut pixels = vec![0; 100];
let canvas = Canvas { pixels: &mut pixels, width: 10, height: 10, scale: 2.0 };
let (columns, rows) = canvas.area([1.0, 1.0, 2.0, 2.0], 0.0, [0.0, 0.0, 100.0, 100.0]);
assert_eq!((columns, rows), (2..6, 2..6));
let (columns, _) = canvas.area([1.0, 1.0, 2.0, 2.0], 0.0, [0.0, 0.0, 2.0, 100.0]);
assert_eq!(columns, 2..4, "el recorte acota antes de pintar");
assert!(rounded_rect_distance((0.0, 0.0), (2.0, 2.0), [1.0; 4]) < 0.0);
assert!(rounded_rect_distance((1.9, 1.9), (2.0, 2.0), [1.0; 4]) > 0.0, "fuera de la esquina redonda");
}
}