use super::canvas::{Canvas, Premultiplied, pack, rounded_rect_distance};
use crate::gpu::scene::{Quad, Shadow, Underline};
use std::ops::Range;
fn interior(canvas: &Canvas, bounds: [f32; 4], inset: f32, y: u32, columns: &Range<u32>) -> Range<u32> {
let center_y = canvas.logical(y);
if center_y < bounds[1] + inset || center_y > bounds[1] + bounds[3] - inset {
return 0..0;
}
let first = ((bounds[0] + inset) * canvas.scale - 0.5).ceil().max(columns.start as f32) as u32;
let last = (((bounds[0] + bounds[2] - inset) * canvas.scale - 0.5).floor() + 1.0).min(columns.end as f32);
let last = last.max(first as f32) as u32;
first..last
}
fn max4(values: [f32; 4]) -> f32 {
values.into_iter().fold(0.0, f32::max)
}
pub fn quad(canvas: &mut Canvas, quad: &Quad) {
let bounds = quad.bounds;
let (columns, rows) = canvas.area(bounds, 1.0, quad.clip);
let half = (bounds[2] * 0.5, bounds[3] * 0.5);
let center = (bounds[0] + half.0, bounds[1] + half.1);
let widths = quad.border_widths; let has_border = widths.iter().sum::<f32>() > 0.001;
let inner_half = (half.0 - (widths[3] + widths[1]) * 0.5, half.1 - (widths[0] + widths[2]) * 0.5);
let inner_center = (center.0 + (widths[3] - widths[1]) * 0.5, center.1 + (widths[0] - widths[2]) * 0.5);
let widest = max4(widths);
let inner_radii = quad.corner_radii.map(|radius| (radius - widest).max(0.0));
let fill = Premultiplied::from_straight(quad.background);
let border = Premultiplied::from_straight(quad.border_color);
let gradient = (quad.gradient[2] > 0.5).then(|| {
let direction = (quad.gradient[0], quad.gradient[1]);
let length = (bounds[2] * direction.0).abs() + (bounds[3] * direction.1).abs();
(direction, length.max(0.001), Premultiplied::from_straight(quad.gradient_end))
});
let paint = |point: (f32, f32)| match gradient {
Some((direction, length, end)) => {
let along = (point.0 - center.0) * direction.0 + (point.1 - center.1) * direction.1;
fill.mix(end, (0.5 + along / length).clamp(0.0, 1.0))
}
None => fill,
};
let inset = max4(quad.corner_radii) + widest + 1.0;
let opaque = (gradient.is_none() && fill.a >= 0.999).then(|| pack(fill.r, fill.g, fill.b));
for y in rows {
let center_y = canvas.logical(y);
let solid = interior(canvas, bounds, inset, y, &columns);
let edges = match opaque.filter(|_| !solid.is_empty()) {
Some(packed) => {
canvas.fill_span(y, solid.clone(), packed);
(columns.start..solid.start).chain(solid.end..columns.end)
}
None => (columns.clone()).chain(0..0),
};
for x in edges {
let point = (canvas.logical(x), center_y);
if solid.contains(&x) {
canvas.blend_at(x, y, paint(point));
continue;
}
let local = (point.0 - center.0, point.1 - center.1);
let coverage = (0.5 - rounded_rect_distance(local, half, quad.corner_radii)).clamp(0.0, 1.0);
if coverage <= 0.0 {
continue;
}
let mut color = paint(point);
if has_border {
let inner_local = (point.0 - inner_center.0, point.1 - inner_center.1);
let inner = rounded_rect_distance(inner_local, inner_half, inner_radii);
color = color.mix(border, (0.5 + inner).clamp(0.0, 1.0));
}
canvas.blend_at(x, y, color.scaled(coverage));
}
}
}
fn erf(value: f32) -> f32 {
let x = value.abs();
let t = 1.0 / (1.0 + 0.327_591_1 * x);
let poly = t * (0.254_829_6 + t * (-0.284_496_74 + t * (1.421_413_8 + t * (-1.453_152 + t * 1.061_405_4))));
value.signum() * (1.0 - poly * (-x * x).exp())
}
pub fn shadow(canvas: &mut Canvas, shadow: &Shadow) {
let bounds = shadow.bounds;
let (columns, rows) = canvas.area(bounds, shadow.blur * 3.0, shadow.clip);
let half = (bounds[2] * 0.5, bounds[3] * 0.5);
let center = (bounds[0] + half.0, bounds[1] + half.1);
let sigma = (shadow.blur * 0.5).max(0.001);
let color = Premultiplied::from_straight(shadow.color);
let inset = max4(shadow.corner_radii) + sigma * 4.0;
for y in rows {
let center_y = canvas.logical(y);
let solid = interior(canvas, bounds, inset, y, &columns);
for x in columns.clone() {
if solid.contains(&x) {
canvas.blend_at(x, y, color);
continue;
}
let local = (canvas.logical(x) - center.0, center_y - center.1);
let distance = rounded_rect_distance(local, half, shadow.corner_radii);
let falloff = 0.5 * (1.0 - erf(distance / (sigma * std::f32::consts::SQRT_2)));
if falloff > 0.002 {
canvas.blend_at(x, y, color.scaled(falloff));
}
}
}
}
pub fn underline(canvas: &mut Canvas, underline: &Underline) {
let bounds = underline.bounds;
let (columns, rows) = canvas.area(bounds, 0.0, underline.clip);
let color = Premultiplied::from_straight(underline.color);
let center_y = bounds[3] * 0.5;
for y in rows {
let local_y = canvas.logical(y) - bounds[1];
for x in columns.clone() {
let local_x = canvas.logical(x) - bounds[0];
let wave = (local_x * 1.2).sin() * bounds[3] * 0.25 * underline.wavy;
let distance = (local_y - center_y - wave).abs() - underline.thickness * 0.5;
let alpha = (0.5 - distance).clamp(0.0, 1.0);
if alpha > 0.0 {
canvas.blend_at(x, y, color.scaled(alpha));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Bounds, Corners, Edges, Point, Rgba, Size, px};
use crate::gpu::cpu::canvas::pack;
fn bounds(x: f32, y: f32, w: f32, h: f32) -> Bounds {
Bounds::new(Point::new(px(x), px(y)), Size::new(px(w), px(h)))
}
#[test]
fn rellena_el_interior_redondea_esquinas_y_pinta_el_borde() {
let mut pixels = vec![0xffffff; 20 * 20];
let mut canvas = Canvas { pixels: &mut pixels, width: 20, height: 20, scale: 1.0 };
let red = Rgba::new(1.0, 0.0, 0.0, 1.0);
let all = bounds(0.0, 0.0, 20.0, 20.0);
let rounded = Quad::new(bounds(2.0, 2.0, 16.0, 16.0), all, red)
.corners(Corners::all(px(6.0)))
.border(Edges::all(px(2.0)), Rgba::new(0.0, 0.0, 1.0, 1.0));
quad(&mut canvas, &rounded);
assert_eq!(pixels[10 * 20 + 10], 0xff0000, "centro relleno");
assert_eq!(pixels[10 * 20 + 2], 0x0000ff, "borde izquierdo");
assert_eq!(pixels[2 * 20 + 2], 0xffffff, "la esquina redondeada queda fuera");
}
#[test]
fn la_sombra_se_desvanece_hacia_afuera_y_el_subrayado_es_fino() {
let mut pixels = vec![pack(1.0, 1.0, 1.0); 40 * 40];
let mut canvas = Canvas { pixels: &mut pixels, width: 40, height: 40, scale: 1.0 };
let all = bounds(0.0, 0.0, 40.0, 40.0);
let black = Rgba::new(0.0, 0.0, 0.0, 1.0);
shadow(&mut canvas, &Shadow::new(bounds(10.0, 10.0, 20.0, 20.0), all, black, Corners::all(px(0.0)), 4.0));
let darkness = |index: usize| 255 - (pixels[index] & 0xff);
assert!(darkness(20 * 40 + 20) > 250, "dentro, plena");
assert!(darkness(20 * 40 + 31) < darkness(20 * 40 + 29), "se aclara al salir");
let mut lines = vec![pack(1.0, 1.0, 1.0); 10 * 4];
let mut canvas = Canvas { pixels: &mut lines, width: 10, height: 4, scale: 1.0 };
underline(
&mut canvas,
&Underline::new(bounds(0.0, 0.0, 10.0, 4.0), bounds(0.0, 0.0, 10.0, 4.0), black, 1.0, false),
);
assert!(lines[10 * 2 + 5] & 0xff < 200, "la línea central se pinta");
assert_eq!(lines[5] & 0xff, 255, "la fila de arriba no");
}
}