use crate::vk::{Frame, Quad, Text, Thumb, THUMB_ATLAS_H, THUMB_ATLAS_W, THUMB_H, THUMB_W};
pub struct Canvas {
w: usize,
h: usize,
px: Vec<u32>, thumbs: Vec<u8>, }
impl Canvas {
pub fn new(w: u32, h: u32) -> Canvas {
let (w, h) = (w as usize, h as usize);
let thumbs = vec![0u8; (THUMB_ATLAS_W * THUMB_ATLAS_H * 4) as usize];
Canvas { w, h, px: vec![0; w * h], thumbs }
}
pub fn upload_thumb(&mut self, slot: usize, w: u32, h: u32, rgba: &[u8]) -> Result<Thumb, String> {
let (w, h) = (w.min(THUMB_W), h.min(THUMB_H));
let row = (w * 4) as usize;
if w == 0 || h == 0 || rgba.len() < row * h as usize {
return Err(format!("thumb: {w}x{h} with {} bytes", rgba.len()));
}
let (x, y) = crate::vk::slot_origin(slot);
let pitch = (THUMB_ATLAS_W * 4) as usize;
for r in 0..h as usize {
let dst = (y as usize + r) * pitch + x as usize * 4;
self.thumbs[dst..dst + row].copy_from_slice(&rgba[r * row..(r + 1) * row]);
}
Ok(Thumb::new(slot, w, h))
}
pub fn render(&mut self, text: &Text, frame: &Frame) {
self.px.fill(0);
for q in frame.quads() {
self.quad(text, &q);
}
}
pub fn write(&self, dst: &mut [u8], stride: usize) {
for (y, row) in self.px.chunks_exact(self.w).enumerate() {
let out = &mut dst[y * stride..y * stride + self.w * 4];
if cfg!(target_endian = "little") {
let bytes = unsafe { std::slice::from_raw_parts(row.as_ptr().cast::<u8>(), row.len() * 4) };
out.copy_from_slice(bytes);
} else {
for (o, p) in out.chunks_exact_mut(4).zip(row) {
o.copy_from_slice(&p.to_le_bytes());
}
}
}
}
fn quad(&mut self, text: &Text, q: &Quad) {
let [x0, y0, x1, y1] = q.rect;
let ix0 = ((x0 - 0.5).ceil().max(0.0) as usize).min(self.w);
let ix1 = ((x1 - 0.5).ceil().max(0.0) as usize).min(self.w);
let iy0 = ((y0 - 0.5).ceil().max(0.0) as usize).min(self.h);
let iy1 = ((y1 - 0.5).ceil().max(0.0) as usize).min(self.h);
if ix0 >= ix1 || iy0 >= iy1 {
return;
}
let [r, g, b, a] = q.color.map(|c| c.clamp(0.0, 1.0));
let [u0, v0, u1, v1] = q.uv;
if !q.thumb && u0 == u1 && v0 == v1 {
let src = pack([r * a, g * a, b * a, a]);
for y in iy0..iy1 {
let row = &mut self.px[y * self.w + ix0..y * self.w + ix1];
if a >= 1.0 {
row.fill(src);
} else {
row.iter_mut().for_each(|d| *d = over(src, *d));
}
}
return;
}
let (tex, tw, th, channels): (&[u8], usize, usize, usize) = if q.thumb {
(&self.thumbs, THUMB_ATLAS_W as usize, THUMB_ATLAS_H as usize, 4)
} else {
let (p, w, h) = text.atlas();
(p, w, h, 1)
};
let tx = |x: usize| (u0 + (x as f32 + 0.5 - x0) / (x1 - x0) * (u1 - u0)) * tw as f32 - 0.5;
let ty = |y: usize| (v0 + (y as f32 + 0.5 - y0) / (y1 - y0) * (v1 - v0)) * th as f32 - 0.5;
let cols: Vec<(usize, usize, f32)> = (ix0..ix1).map(|x| taps(tx(x), tw)).collect();
for y in iy0..iy1 {
let (ya, yb, fy) = taps(ty(y), th);
let row = &mut self.px[y * self.w + ix0..y * self.w + ix1];
for (d, &(xa, xb, fx)) in row.iter_mut().zip(&cols) {
let src = if channels == 1 {
let s = |x: usize, y: usize| tex[y * tw + x] as f32;
let c = lerp(lerp(s(xa, ya), s(xb, ya), fx), lerp(s(xa, yb), s(xb, yb), fx), fy) / 255.0;
let ca = c * a;
if ca <= 0.0 {
continue;
}
[r * ca, g * ca, b * ca, ca]
} else {
let s = |x: usize, y: usize, k: usize| tex[(y * tw + x) * 4 + k] as f32;
let ch = |k: usize| {
lerp(lerp(s(xa, ya, k), s(xb, ya, k), fx), lerp(s(xa, yb, k), s(xb, yb, k), fx), fy) / 255.0
};
[ch(0) * r * a, ch(1) * g * a, ch(2) * b * a, a]
};
*d = over(pack(src), *d);
}
}
}
}
fn taps(t: f32, n: usize) -> (usize, usize, f32) {
let f = t.floor();
let last = n as isize - 1;
let i = f as isize;
((i.clamp(0, last)) as usize, ((i + 1).clamp(0, last)) as usize, t - f)
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn pack([r, g, b, a]: [f32; 4]) -> u32 {
let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
q(a) << 24 | q(r) << 16 | q(g) << 8 | q(b)
}
fn over(src: u32, dst: u32) -> u32 {
let inv = 255 - (src >> 24);
if inv == 0 {
return src;
}
let mut out = 0;
for shift in [0, 8, 16, 24] {
let s = (src >> shift) & 0xff;
let d = (dst >> shift) & 0xff;
let v = s + (d * inv + 127) / 255;
out |= v.min(255) << shift;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn over_is_premultiplied_source_over() {
assert_eq!(over(0xff10_2030, 0x8080_8080), 0xff10_2030);
assert_eq!(over(0, 0x8040_2010), 0x8040_2010);
assert_eq!(over(0x8000_0000, 0xffff_ffff), 0xff7f_7f7f);
}
#[test]
fn rects_follow_the_pixel_center_rule() {
let text = Text::new().unwrap();
let mut canvas = Canvas::new(8, 4);
let mut frame = Frame::default();
frame.rect(&text, 1.0, 1.0, 2.0, 2.0, [1.0, 0.0, 0.0, 1.0]); frame.rect(&text, 4.6, 0.0, 2.0, 1.0, [0.0, 0.0, 1.0, 0.5]); canvas.render(&text, &frame);
let at = |x: usize, y: usize| canvas.px[y * 8 + x];
assert_eq!((at(0, 1), at(1, 1), at(2, 2), at(3, 1)), (0, 0xffff_0000, 0xffff_0000, 0));
assert_eq!((at(4, 0), at(5, 0), at(6, 0), at(7, 0)), (0, 0x8000_0080, 0x8000_0080, 0));
let mut out = vec![0u8; 4 * 40];
canvas.write(&mut out, 40);
assert_eq!(&out[40 + 4..40 + 8], &[0x00, 0x00, 0xff, 0xff]); }
#[test]
fn text_and_thumbs_cover_their_quads() {
let text = Text::new().unwrap();
let mut canvas = Canvas::new(64, 32);
let rgba: Vec<u8> = [10u8, 200, 30, 255].repeat(16 * 9);
let thumb = canvas.upload_thumb(3, 16, 9, &rgba).unwrap();
let mut frame = Frame::default();
frame.image(thumb.uv, 0.0, 0.0, 32.0, 18.0, [1.0, 1.0, 1.0, 1.0]);
frame.text(&text, 34.0, 0.0, 24.0, "H", [1.0, 1.0, 1.0, 1.0]);
canvas.render(&text, &frame);
assert_eq!(canvas.px[9 * 64 + 16], 0xff0a_c81e);
let lit = (0..32).flat_map(|y| (34..64).map(move |x| (x, y))).filter(|&(x, y)| canvas.px[y * 64 + x] >> 24 == 0xff);
assert!(lit.count() > 20, "glyph H has opaque stems");
}
}