use crate::marks::{Element, Visibility};
use simdriver::{Frame, Grid};
const PALETTE: [[u8; 3]; 8] = [
[236, 64, 122],
[255, 145, 0],
[0, 200, 83],
[41, 121, 255],
[170, 0, 255],
[0, 184, 212],
[255, 214, 0],
[255, 23, 68],
];
const COVERED: [u8; 3] = [140, 140, 140];
const GRID: [u8; 3] = [236, 64, 122];
struct Canvas {
w: usize,
h: usize,
px: Vec<u8>,
}
impl Canvas {
fn from_frame(f: &Frame) -> Canvas {
let (w, h) = (f.width as usize, f.height as usize);
let mut px = Vec::with_capacity(w * h * 3);
for p in f.data.as_chunks::<4>().0.iter().take(w * h) {
px.extend_from_slice(&[p[2], p[1], p[0]]);
}
Canvas { w, h, px }
}
fn blend(&mut self, x: i64, y: i64, c: [u8; 3], alpha: u16) {
if x < 0 || y < 0 || x as usize >= self.w || y as usize >= self.h {
return;
}
let i = (y as usize * self.w + x as usize) * 3;
for (dst, &src) in self.px[i..i + 3].iter_mut().zip(&c) {
*dst = ((u16::from(*dst) * (256 - alpha) + u16::from(src) * alpha) >> 8) as u8;
}
}
fn fill(&mut self, x0: i64, y0: i64, x1: i64, y1: i64, c: [u8; 3], alpha: u16) {
for y in y0.max(0)..y1.min(self.h as i64) {
for x in x0.max(0)..x1.min(self.w as i64) {
self.blend(x, y, c, alpha);
}
}
}
fn outline(&mut self, x0: i64, y0: i64, x1: i64, y1: i64, t: i64, c: [u8; 3]) {
self.fill(x0, y0, x1, y0 + t, c, 256);
self.fill(x0, y1 - t, x1, y1, c, 256);
self.fill(x0, y0, x0 + t, y1, c, 256);
self.fill(x1 - t, y0, x1, y1, c, 256);
}
fn tag(&mut self, x: i64, y: i64, text: &str, s: i64, c: [u8; 3]) {
let (tw, th) = (text.len() as i64 * 6 * s - s, 7 * s);
let pad = 2 * s;
let x = x.min(self.w as i64 - tw - 2 * pad).max(0);
let y = y.min(self.h as i64 - th - 2 * pad).max(0);
self.fill(x, y, x + tw + 2 * pad, y + th + 2 * pad, c, 256);
self.text(x + pad, y + pad, text, s, [255, 255, 255]);
}
fn text(&mut self, x: i64, y: i64, text: &str, s: i64, c: [u8; 3]) {
for (i, ch) in text.chars().enumerate() {
for (r, bits) in glyph(ch).iter().enumerate() {
for b in 0..5 {
if bits & (0x10 >> b) != 0 {
let gx = x + i as i64 * 6 * s + b * s;
let gy = y + r as i64 * s;
self.fill(gx, gy, gx + s, gy + s, c, 256);
}
}
}
}
}
fn blit_scaled(&mut self, f: &Frame, x0: i64, y0: i64, w: usize, h: usize) {
let (sw, sh) = (f.width as usize, f.height as usize);
if sw == 0 || sh == 0 || f.data.len() < sw * sh * 4 {
return;
}
for dy in 0..h {
let (ya, yb) = (
dy * sh / h,
((dy + 1) * sh / h).max(dy * sh / h + 1).min(sh),
);
for dx in 0..w {
let (xa, xb) = (
dx * sw / w,
((dx + 1) * sw / w).max(dx * sw / w + 1).min(sw),
);
let mut sum = [0u32; 3];
for y in ya..yb {
for p in f.data[(y * sw + xa) * 4..(y * sw + xb) * 4]
.as_chunks::<4>()
.0
{
sum[0] += u32::from(p[2]);
sum[1] += u32::from(p[1]);
sum[2] += u32::from(p[0]);
}
}
let n = ((yb - ya) * (xb - xa)) as u32;
let c = [(sum[0] / n) as u8, (sum[1] / n) as u8, (sum[2] / n) as u8];
self.blend(x0 + dx as i64, y0 + dy as i64, c, 256);
}
}
}
fn png(&self) -> Vec<u8> {
let mut out = Vec::new();
let mut enc = png::Encoder::new(&mut out, self.w as u32, self.h as u32);
enc.set_color(png::ColorType::Rgb);
enc.set_depth(png::BitDepth::Eight);
enc.set_compression(png::Compression::Fast);
let ok = enc
.write_header()
.and_then(|mut w| w.write_image_data(&self.px).and_then(|()| w.finish()))
.is_ok();
if !ok {
out.clear();
}
out
}
}
pub fn elements(frame: &Frame, screen: (f64, f64), els: &[Element]) -> Vec<u8> {
let mut c = Canvas::from_frame(frame);
let k = frame.width as f64 / screen.0;
let t = (k * 1.5).round().max(2.0) as i64;
let s = (k * 1.4).round().max(2.0) as i64;
for (i, e) in els.iter().enumerate() {
let color = if e.visibility == Visibility::Covered {
COVERED
} else {
PALETTE[i % PALETTE.len()]
};
let f = e.frame;
let (x0, y0) = ((f[0] * k) as i64, (f[1] * k) as i64);
let (x1, y1) = (((f[0] + f[2]) * k) as i64, ((f[1] + f[3]) * k) as i64);
c.fill(x0, y0, x1, y1, color, 28);
c.outline(x0, y0, x1, y1, t, color);
c.tag(x0, y0, &e.reference, s, color);
}
c.png()
}
pub fn grid(frame: &Frame, grid: &Grid) -> Vec<u8> {
let mut c = Canvas::from_frame(frame);
let (w, h) = (c.w as f64, c.h as f64);
let t = ((w / 400.0).round() as i64).max(1);
let s = ((w / 300.0).round() as i64).max(2);
for col in 0..=grid.cols {
let x = (f64::from(col) * w / f64::from(grid.cols)) as i64;
c.fill(x - t / 2, 0, x - t / 2 + t, h as i64, GRID, 220);
}
for row in 0..=grid.rows {
let y = (f64::from(row) * h / f64::from(grid.rows)) as i64;
c.fill(0, y - t / 2, w as i64, y - t / 2 + t, GRID, 220);
}
for row in 0..grid.rows {
for col in 0..grid.cols {
let x = (f64::from(col) * w / f64::from(grid.cols)) as i64 + 2 * t;
let y = (f64::from(row) * h / f64::from(grid.rows)) as i64 + 2 * t;
c.tag(x, y, &grid.name(row, col), s, GRID);
}
}
c.png()
}
pub fn crop(frame: &Frame, screen: (f64, f64), rect: [f64; 4]) -> Option<Vec<u8>> {
let k = frame.width as f64 / screen.0;
let x0 = (rect[0] * k).floor().max(0.0) as usize;
let y0 = (rect[1] * k).floor().max(0.0) as usize;
let x1 = ((rect[0] + rect[2]) * k).ceil().min(frame.width as f64) as usize;
let y1 = ((rect[1] + rect[3]) * k).ceil().min(frame.height as f64) as usize;
if x1 <= x0 || y1 <= y0 {
return None;
}
let (w, h) = (x1 - x0, y1 - y0);
let mut rgb = Vec::with_capacity(w * h * 3);
for y in y0..y1 {
let row =
&frame.data[(y * frame.width as usize + x0) * 4..(y * frame.width as usize + x1) * 4];
for p in row.as_chunks::<4>().0 {
rgb.extend_from_slice(&[p[2], p[1], p[0]]);
}
}
let mut out = Vec::new();
let mut enc = png::Encoder::new(&mut out, w as u32, h as u32);
enc.set_color(png::ColorType::Rgb);
enc.set_depth(png::BitDepth::Eight);
enc.set_compression(png::Compression::Fast);
let mut writer = enc.write_header().ok()?;
writer.write_image_data(&rgb).ok()?;
writer.finish().ok()?;
Some(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TilePhase {
Before,
During,
After,
}
pub struct Tile<'a> {
pub frame: &'a Frame,
pub label: String,
pub phase: TilePhase,
}
const SHEET_MAX: usize = 1568;
const TILE_W: usize = 180;
const GUTTER: usize = 8;
const BORDER: usize = 3;
const LABEL_H: usize = 20;
const LABEL_SCALE: i64 = 2;
const LEGEND_H: usize = LABEL_H;
const LEGEND: [(TilePhase, &str); 3] = [
(TilePhase::Before, "BEFORE"),
(TilePhase::During, "DURING"),
(TilePhase::After, "AFTER"),
];
const SWATCH: usize = 14;
const SWATCH_GAP: usize = 6;
const LEGEND_GAP: usize = 16;
const TOP: usize = GUTTER + LEGEND_H + GUTTER;
const SHEET_BG: [u8; 3] = [24, 24, 24];
const PHASE_BEFORE: [u8; 3] = [140, 140, 140];
const PHASE_DURING: [u8; 3] = [41, 121, 255];
const PHASE_AFTER: [u8; 3] = [0, 200, 83];
fn phase_color(p: TilePhase) -> [u8; 3] {
match p {
TilePhase::Before => PHASE_BEFORE,
TilePhase::During => PHASE_DURING,
TilePhase::After => PHASE_AFTER,
}
}
fn text_w(text: &str, s: usize) -> usize {
(text.len() * 6 * s).saturating_sub(s)
}
fn legend_w() -> usize {
LEGEND
.iter()
.map(|(_, word)| SWATCH + SWATCH_GAP + text_w(word, LABEL_SCALE as usize))
.sum::<usize>()
+ LEGEND_GAP * (LEGEND.len() - 1)
}
fn draw_legend(c: &mut Canvas) {
let mut x = GUTTER as i64;
let y = GUTTER as i64;
let sy = y + (LEGEND_H - SWATCH) as i64 / 2;
let ty = y + (LEGEND_H as i64 - 7 * LABEL_SCALE) / 2;
for (phase, word) in LEGEND {
c.fill(
x,
sy,
x + SWATCH as i64,
sy + SWATCH as i64,
phase_color(phase),
256,
);
x += (SWATCH + SWATCH_GAP) as i64;
c.text(x, ty, word, LABEL_SCALE, [255, 255, 255]);
x += (text_w(word, LABEL_SCALE as usize) + LEGEND_GAP) as i64;
}
}
pub fn frame_png(frame: &Frame) -> Vec<u8> {
Canvas::from_frame(frame).png()
}
pub fn contact_sheet(tiles: &[Tile]) -> Vec<Vec<u8>> {
let cell_w = TILE_W + 2 * BORDER;
let cols = ((SHEET_MAX - GUTTER) / (cell_w + GUTTER)).max(1);
let max_img = SHEET_MAX - TOP - GUTTER - 2 * BORDER - LABEL_H;
let img_h = |t: &Tile| {
let f = t.frame;
(TILE_W * f.height as usize / (f.width as usize).max(1)).clamp(1, max_img)
};
let rows: Vec<&[Tile]> = tiles.chunks(cols).collect();
let row_h = |r: &[Tile]| r.iter().map(img_h).max().unwrap_or(0) + 2 * BORDER + LABEL_H;
let mut sheets: Vec<Vec<&[Tile]>> = Vec::new();
let mut used = TOP;
for r in rows {
let h = row_h(r) + GUTTER;
match sheets.last_mut() {
Some(s) if used + h <= SHEET_MAX => {
s.push(r);
used += h;
}
_ => {
sheets.push(vec![r]);
used = TOP + h;
}
}
}
sheets
.iter()
.map(|rows| {
let n = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let w = (GUTTER + n * (cell_w + GUTTER)).max(GUTTER + legend_w() + GUTTER);
let h = TOP + rows.iter().map(|r| row_h(r) + GUTTER).sum::<usize>();
let mut c = Canvas {
w,
h,
px: SHEET_BG.repeat(w * h),
};
draw_legend(&mut c);
let mut y = TOP;
for r in rows {
for (i, t) in r.iter().enumerate() {
let x = GUTTER + i * (cell_w + GUTTER);
let ih = img_h(t);
let (bx, by) = (x as i64, y as i64);
let (bx1, by1) = (bx + cell_w as i64, by + (ih + 2 * BORDER) as i64);
c.blit_scaled(t.frame, bx + BORDER as i64, by + BORDER as i64, TILE_W, ih);
c.outline(bx, by, bx1, by1, BORDER as i64, phase_color(t.phase));
let ty = by1 + (LABEL_H as i64 - 7 * LABEL_SCALE) / 2;
c.text(bx, ty, &t.label, LABEL_SCALE, [255, 255, 255]);
}
y += row_h(r) + GUTTER;
}
c.png()
})
.collect()
}
fn glyph(c: char) -> [u8; 7] {
match c {
'0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
'1' => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
'2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
'3' => [0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E],
'4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
'5' => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
'6' => [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E],
'7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
'8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
'9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
'D' => [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C],
'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F],
'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
'J' => [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C],
'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
'N' => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A],
'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
'Y' => [0x11, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04],
'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
'e' => [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E],
'm' => [0x00, 0x00, 0x1A, 0x15, 0x15, 0x15, 0x15],
's' => [0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E],
'#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A],
'+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
'-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
' ' => [0; 7],
_ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::marks::{elements as marks, tests::settings};
fn frame(w: u32, h: u32) -> Frame {
Frame {
width: w,
height: h,
source_width: w,
source_height: h,
seq: 2,
data: vec![0x20; (w * h * 4) as usize],
}
}
fn decode(png_bytes: &[u8]) -> (u32, u32, Vec<u8>) {
let mut r = png::Decoder::new(std::io::Cursor::new(png_bytes))
.read_info()
.unwrap();
let mut buf = vec![0; r.output_buffer_size().unwrap()];
let info = r.next_frame(&mut buf).unwrap();
(info.width, info.height, buf)
}
#[test]
fn element_boxes_land_on_the_frames() {
let (els, _) = marks(&settings(), (400.0, 800.0));
let f = frame(800, 1600); let (w, h, px) = decode(&elements(&f, (400.0, 800.0), &els));
assert_eq!((w, h), (800, 1600));
let at = |x: u32, y: u32| {
let i = ((y * w + x) * 3) as usize;
[px[i], px[i + 1], px[i + 2]]
};
assert_eq!(at(33, 450), PALETTE[0]);
assert_eq!(at(790, 20), [0x20, 0x20, 0x20]);
}
#[test]
fn covered_elements_are_grey() {
let (els, _) = marks(&settings(), (400.0, 800.0));
let passcode = els.iter().find(|e| e.name() == "Passcode").unwrap();
assert_eq!(passcode.visibility, Visibility::Covered);
let (w, _, px) = decode(&elements(
&frame(400, 800),
(400.0, 800.0),
std::slice::from_ref(passcode),
));
let i = ((775 * w + 17) * 3) as usize; assert_eq!([px[i], px[i + 1], px[i + 2]], COVERED);
}
#[test]
fn grid_overlay_names_every_cell() {
let g = Grid::new((400.0, 800.0), 6);
let (w, h, px) = decode(&grid(&frame(400, 800), &g));
assert_eq!((w, h), (400, 800));
let i = ((4 * w + 4) * 3) as usize;
assert_eq!([px[i], px[i + 1], px[i + 2]], GRID);
}
#[test]
fn every_ref_and_cell_character_has_a_glyph() {
let unknown = glyph('?');
for ch in ('0'..='9')
.chain('A'..='Z')
.chain(['e', 'm', 's', '#', '+', '-', ' '])
.chain(LEGEND.iter().flat_map(|(_, word)| word.chars()))
{
assert_ne!(glyph(ch), unknown, "{ch}");
}
}
fn tiles<'a>(f: &'a Frame, n: usize) -> Vec<Tile<'a>> {
(0..n)
.map(|i| Tile {
frame: f,
label: format!("#{i} +{}ms", i * 250),
phase: TilePhase::During,
})
.collect()
}
#[test]
fn frame_png_round_trips_size() {
let (w, h, _) = decode(&frame_png(&frame(603, 1311)));
assert_eq!((w, h), (603, 1311));
}
#[test]
fn sheet_fits_24_iphone_frames_on_one_sheet() {
let f = frame(603, 1311);
let sheets = contact_sheet(&tiles(&f, 24));
assert_eq!(sheets.len(), 1);
let (w, h, _) = decode(&sheets[0]);
assert!(w <= 1568 && h <= 1568, "{w}x{h}");
}
#[test]
fn sheet_overflows_to_a_second_sheet() {
let f = frame(603, 1311);
let sheets = contact_sheet(&tiles(&f, 40));
assert_eq!(sheets.len(), 2);
for s in &sheets {
let (w, h, _) = decode(s);
assert!(w <= 1568 && h <= 1568, "{w}x{h}");
}
}
#[test]
fn sheet_handles_mixed_frame_sizes() {
let (tall, wide) = (frame(603, 1311), frame(1311, 603));
let t = |f, label: &str| Tile {
frame: f,
label: label.into(),
phase: TilePhase::Before,
};
let sheets = contact_sheet(&[t(&tall, "#0 +0ms"), t(&wide, "#1 +50ms")]);
assert_eq!(sheets.len(), 1);
let (w, h, px) = decode(&sheets[0]);
let cell = TILE_W + 2 * BORDER;
assert_eq!(w as usize, GUTTER + 2 * (cell + GUTTER));
assert_eq!(h as usize, TOP + 391 + 2 * BORDER + LABEL_H + GUTTER);
let i = ((TOP + 2 * BORDER + 82 + 20) * w as usize + GUTTER + cell + GUTTER + 90) * 3;
assert_eq!(px[i..i + 3], SHEET_BG);
}
#[test]
fn zero_size_frame_does_not_panic() {
let (empty, tall) = (frame(0, 0), frame(603, 1311));
let short = Frame {
data: vec![0; 16], ..frame(100, 200)
};
for f in [&empty, &frame(0, 100), &frame(100, 0), &short] {
let sheets = contact_sheet(&[
Tile {
frame: f,
label: "#0 +0ms".into(),
phase: TilePhase::Before,
},
Tile {
frame: &tall,
label: "#1 +40ms".into(),
phase: TilePhase::After,
},
]);
assert_eq!(sheets.len(), 1);
let (w, h, _) = decode(&sheets[0]);
assert!(w <= 1568 && h <= 1568, "{w}x{h}");
assert!(frame_png(f).is_empty());
}
}
#[test]
fn phase_borders_differ() {
let f = frame(100, 200);
let mk = |phase| Tile {
frame: &f,
label: "#0".into(),
phase,
};
let sheets = contact_sheet(&[mk(TilePhase::Before), mk(TilePhase::During)]);
let (w, _, px) = decode(&sheets[0]);
let at = |tile: usize| {
let i =
((TOP + 1) * w as usize + GUTTER + tile * (TILE_W + 2 * BORDER + GUTTER) + 1) * 3;
[px[i], px[i + 1], px[i + 2]]
};
assert_eq!(at(0), PHASE_BEFORE);
assert_eq!(at(1), PHASE_DURING);
}
#[test]
fn legend_names_the_border_colours() {
let f = frame(100, 200);
let one = [Tile {
frame: &f,
label: "#0 +0ms".into(),
phase: TilePhase::Before,
}];
let (w, h, px) = decode(&contact_sheet(&one)[0]);
assert_eq!(w as usize, GUTTER + legend_w() + GUTTER);
assert!(legend_w() > TILE_W + 2 * BORDER);
assert!(h as usize <= SHEET_MAX);
let at = |x: usize, y: usize| {
let i = (y * w as usize + x) * 3;
[px[i], px[i + 1], px[i + 2]]
};
let mid = GUTTER + LEGEND_H / 2;
let mut x = GUTTER;
for (phase, word) in LEGEND {
assert_eq!(at(x + SWATCH / 2, mid), phase_color(phase), "{word}");
x += SWATCH + SWATCH_GAP + text_w(word, LABEL_SCALE as usize) + LEGEND_GAP;
}
let word_x = GUTTER + SWATCH + SWATCH_GAP;
let white = (GUTTER..GUTTER + LEGEND_H)
.flat_map(|y| (word_x..word_x + 60).map(move |x| (x, y)))
.any(|(x, y)| at(x, y) == [255, 255, 255]);
assert!(white);
}
}