mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Pattern fills were approximated (dots, bars or a grid per family, on an 8-point cell), so pct30 looked like sparse dots and wave like a dot grid. Every one of the 54 DrawingML presets now draws its 8x8 tile, each cell one 96-dpi pixel (0.75 pt), laid out from the slide's origin. The tiles were read from PowerPoint's own export of each preset at two output pixels per cell; the pattern probe deck now matches it to a mean pixel difference of 0.2 (from 62). Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
209 lines
8.4 KiB
Rust
209 lines
8.4 KiB
Rust
//! Preset pattern fills (`a:pattFill`): an 8×8 two-colour tile, each cell one 96-dpi pixel
|
||
//! (0.75 pt) of the slide, as PowerPoint draws them.
|
||
//!
|
||
//! The tiles were read from PowerPoint's own slide export of every preset (black on white, at
|
||
//! exactly two output pixels per pattern pixel), so the marks, their density and their phase
|
||
//! match it. Bit 7 of each row is the leftmost cell; a set bit is the foreground colour.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use deckcraft_color::Rgba;
|
||
use kurbo::{Affine, BezPath};
|
||
use vello_cpu::{Pixmap, RenderContext, peniko};
|
||
|
||
/// Side of one pattern cell, in points.
|
||
pub const CELL: f64 = 0.75;
|
||
|
||
const TILES: &[(&str, [u8; 8])] = &[
|
||
("pct5", [0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00]),
|
||
("pct10", [0x80, 0x00, 0x08, 0x00, 0x80, 0x00, 0x08, 0x00]),
|
||
("pct20", [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00]),
|
||
("pct25", [0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22]),
|
||
("pct30", [0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11]),
|
||
("pct40", [0xAA, 0x55, 0xAA, 0x51, 0xAA, 0x55, 0xAA, 0x15]),
|
||
("pct50", [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]),
|
||
("pct60", [0xEE, 0x55, 0xBB, 0x55, 0xEE, 0x55, 0xBB, 0x55]),
|
||
("pct70", [0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD]),
|
||
("pct75", [0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF]),
|
||
("pct80", [0xEF, 0xFF, 0xFE, 0xFF, 0xEF, 0xFF, 0xFE, 0xFF]),
|
||
("pct90", [0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F]),
|
||
("horz", [0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||
("vert", [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
|
||
("ltHorz", [0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]),
|
||
("ltVert", [0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88]),
|
||
("dkHorz", [0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00]),
|
||
("dkVert", [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]),
|
||
("narHorz", [0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00]),
|
||
("narVert", [0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55]),
|
||
("dashHorz", [0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00]),
|
||
("dashVert", [0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x08, 0x08]),
|
||
("cross", [0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
|
||
("dnDiag", [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]),
|
||
("upDiag", [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]),
|
||
("ltDnDiag", [0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11]),
|
||
("ltUpDiag", [0x11, 0x22, 0x44, 0x88, 0x11, 0x22, 0x44, 0x88]),
|
||
("dkDnDiag", [0xCC, 0x66, 0x33, 0x99, 0xCC, 0x66, 0x33, 0x99]),
|
||
("dkUpDiag", [0x33, 0x66, 0xCC, 0x99, 0x33, 0x66, 0xCC, 0x99]),
|
||
("wdDnDiag", [0xC1, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x83]),
|
||
("wdUpDiag", [0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xC1]),
|
||
("dashDnDiag", [0x00, 0x00, 0x88, 0x44, 0x22, 0x11, 0x00, 0x00]),
|
||
("dashUpDiag", [0x00, 0x00, 0x11, 0x22, 0x44, 0x88, 0x00, 0x00]),
|
||
("diagCross", [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81]),
|
||
("smCheck", [0x99, 0x66, 0x66, 0x99, 0x99, 0x66, 0x66, 0x99]),
|
||
("lgCheck", [0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F]),
|
||
("smGrid", [0xFF, 0x88, 0x88, 0x88, 0xFF, 0x88, 0x88, 0x88]),
|
||
("lgGrid", [0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
|
||
("dotGrid", [0xAA, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00]),
|
||
("smConfetti", [0x80, 0x08, 0x40, 0x02, 0x10, 0x01, 0x20, 0x04]),
|
||
("lgConfetti", [0xB1, 0x30, 0x03, 0x1B, 0xD8, 0xC0, 0x0C, 0x8D]),
|
||
("horzBrick", [0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08]),
|
||
("diagBrick", [0x01, 0x02, 0x04, 0x08, 0x18, 0x24, 0x42, 0x81]),
|
||
("solidDmnd", [0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00]),
|
||
("openDmnd", [0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x01]),
|
||
("dotDmnd", [0x80, 0x00, 0x22, 0x00, 0x08, 0x00, 0x22, 0x00]),
|
||
("plaid", [0xAA, 0x55, 0xAA, 0x55, 0xF0, 0xF0, 0xF0, 0xF0]),
|
||
("sphere", [0x77, 0x89, 0x8F, 0x8F, 0x77, 0x98, 0xF8, 0xF8]),
|
||
("weave", [0x88, 0x54, 0x22, 0x45, 0x88, 0x14, 0x22, 0x51]),
|
||
("divot", [0x00, 0x10, 0x08, 0x10, 0x00, 0x80, 0x01, 0x80]),
|
||
("shingle", [0x03, 0x84, 0x48, 0x30, 0x0C, 0x02, 0x01, 0x01]),
|
||
("wave", [0x00, 0x18, 0x25, 0xC0, 0x00, 0x18, 0x25, 0xC0]),
|
||
("trellis", [0xFF, 0x66, 0xFF, 0x99, 0xFF, 0x66, 0xFF, 0x99]),
|
||
("zigZag", [0x81, 0x42, 0x24, 0x18, 0x81, 0x42, 0x24, 0x18]),
|
||
];
|
||
|
||
/// The 8×8 tile of preset `name` (`pct50` for an unknown name).
|
||
pub fn tile(name: &str) -> [u8; 8] {
|
||
TILES
|
||
.iter()
|
||
.find(|(n, _)| *n == name)
|
||
.or_else(|| TILES.iter().find(|(n, _)| *n == "pct50"))
|
||
.map(|(_, t)| *t)
|
||
.unwrap_or([0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55])
|
||
}
|
||
|
||
/// Is cell (`x`, `y`) of the tile the foreground?
|
||
pub fn is_fg(tile: &[u8; 8], x: usize, y: usize) -> bool {
|
||
tile.get(y % 8).is_some_and(|row| row & (0x80 >> (x % 8)) != 0)
|
||
}
|
||
|
||
fn premul(c: Rgba, alpha: f64) -> vello_cpu::color::PremulRgba8 {
|
||
let a = (c.a as f64 * alpha.clamp(0.0, 1.0)).round().clamp(0.0, 255.0);
|
||
let m = |v: u8| (v as f64 * a / 255.0).round().clamp(0.0, 255.0) as u8;
|
||
vello_cpu::color::PremulRgba8 { r: m(c.r), g: m(c.g), b: m(c.b), a: a as u8 }
|
||
}
|
||
|
||
/// Fill `path` (in the space `m` maps to the output) with preset `name` in `fg` on `bg`, faded by
|
||
/// `alpha`.
|
||
pub fn fill(ctx: &mut RenderContext, name: &str, fg: Rgba, bg: Rgba, alpha: f64, path: &BezPath, m: Affine) {
|
||
let det = m.determinant();
|
||
if !det.is_finite() || det.abs() <= 1e-12 {
|
||
return;
|
||
}
|
||
let t = tile(name);
|
||
let (f, b) = (premul(fg, alpha), premul(bg, alpha));
|
||
let data: Vec<vello_cpu::color::PremulRgba8> = (0..64).map(|i| if is_fg(&t, i % 8, i / 8) { f } else { b }).collect();
|
||
let pm = Arc::new(Pixmap::from_parts(data, 8, 8));
|
||
ctx.set_transform(m);
|
||
ctx.set_paint(vello_cpu::Image {
|
||
image: vello_cpu::ImageSource::Pixmap(pm),
|
||
sampler: peniko::ImageSampler {
|
||
x_extend: peniko::Extend::Repeat,
|
||
y_extend: peniko::Extend::Repeat,
|
||
quality: peniko::ImageQuality::Low,
|
||
alpha: 1.0,
|
||
},
|
||
});
|
||
// Cells are laid out on the output from its origin (the slide's top-left corner), so
|
||
// neighbouring shapes' patterns line up, as in PowerPoint.
|
||
let k = crate::scale_of(m);
|
||
let k = if k.is_finite() && k > 0.0 { k } else { 1.0 };
|
||
ctx.set_paint_transform(m.inverse() * Affine::scale(CELL * k));
|
||
ctx.fill_path(path);
|
||
ctx.reset_paint_transform();
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn every_drawingml_preset_has_a_tile() {
|
||
// ECMA-376 ST_PresetPatternVal.
|
||
let all = [
|
||
"pct5",
|
||
"pct10",
|
||
"pct20",
|
||
"pct25",
|
||
"pct30",
|
||
"pct40",
|
||
"pct50",
|
||
"pct60",
|
||
"pct70",
|
||
"pct75",
|
||
"pct80",
|
||
"pct90",
|
||
"horz",
|
||
"vert",
|
||
"ltHorz",
|
||
"ltVert",
|
||
"dkHorz",
|
||
"dkVert",
|
||
"narHorz",
|
||
"narVert",
|
||
"dashHorz",
|
||
"dashVert",
|
||
"cross",
|
||
"dnDiag",
|
||
"upDiag",
|
||
"ltDnDiag",
|
||
"ltUpDiag",
|
||
"dkDnDiag",
|
||
"dkUpDiag",
|
||
"wdDnDiag",
|
||
"wdUpDiag",
|
||
"dashDnDiag",
|
||
"dashUpDiag",
|
||
"diagCross",
|
||
"smCheck",
|
||
"lgCheck",
|
||
"smGrid",
|
||
"lgGrid",
|
||
"dotGrid",
|
||
"smConfetti",
|
||
"lgConfetti",
|
||
"horzBrick",
|
||
"diagBrick",
|
||
"solidDmnd",
|
||
"openDmnd",
|
||
"dotDmnd",
|
||
"plaid",
|
||
"sphere",
|
||
"weave",
|
||
"divot",
|
||
"shingle",
|
||
"wave",
|
||
"trellis",
|
||
"zigZag",
|
||
];
|
||
assert_eq!(all.len(), 54);
|
||
for n in all {
|
||
assert!(TILES.iter().any(|(t, _)| *t == n), "{n}");
|
||
}
|
||
assert_eq!(TILES.len(), 54);
|
||
}
|
||
|
||
#[test]
|
||
fn percent_presets_get_denser() {
|
||
// The pctN tiles set more cells as N grows (PowerPoint's own: pct5 sets 2 of 64, pct50
|
||
// half, pct90 all but 2).
|
||
let share = |n: &str| {
|
||
let t = tile(n);
|
||
(0..64).filter(|i| is_fg(&t, i % 8, i / 8)).count()
|
||
};
|
||
let names = ["pct5", "pct10", "pct20", "pct25", "pct30", "pct40", "pct50", "pct60", "pct70", "pct75", "pct80", "pct90"];
|
||
assert!(names.windows(2).all(|w| share(w[0]) <= share(w[1])), "{:?}", names.map(share));
|
||
assert_eq!((share("pct5"), share("pct50"), share("pct90")), (2, 32, 62));
|
||
assert_eq!(tile("nonsense"), tile("pct50"));
|
||
}
|
||
}
|