mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
442be148ae |
@@ -13,6 +13,7 @@ mod images;
|
|||||||
mod morph_path;
|
mod morph_path;
|
||||||
mod morph_text;
|
mod morph_text;
|
||||||
mod paint;
|
mod paint;
|
||||||
|
mod pattern;
|
||||||
mod placed;
|
mod placed;
|
||||||
mod ramp;
|
mod ramp;
|
||||||
mod table;
|
mod table;
|
||||||
|
|||||||
@@ -47,55 +47,11 @@ pub fn fill_path(
|
|||||||
Fill::Pattern(p) => {
|
Fill::Pattern(p) => {
|
||||||
let fg = rctx.color(&p.fg, ph);
|
let fg = rctx.color(&p.fg, ph);
|
||||||
let bg = rctx.color(&p.bg, ph);
|
let bg = rctx.color(&p.bg, ph);
|
||||||
ctx.set_paint(color(bg, alpha));
|
crate::pattern::fill(ctx, &p.preset, fg, bg, alpha, path, m);
|
||||||
ctx.fill_path(path);
|
|
||||||
ctx.push_clip_layer(path);
|
|
||||||
ctx.set_paint(color(fg, alpha));
|
|
||||||
pattern(ctx, &p.preset, bounds, m);
|
|
||||||
ctx.pop_layer();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a pattern's foreground marks over `bounds` (8 pt cells).
|
|
||||||
fn pattern(ctx: &mut RenderContext, preset: &str, b: Rect, m: Affine) {
|
|
||||||
let k = crate::scale_of(m);
|
|
||||||
let cell = 6.0 / if k.is_finite() { k.clamp(0.25, 4.0) } else { 1.0 };
|
|
||||||
let cell = cell.max(2.0);
|
|
||||||
let mut p = BezPath::new();
|
|
||||||
let (nx, ny) = (((b.width() / cell).ceil() as i64).clamp(0, 2000), ((b.height() / cell).ceil() as i64).clamp(0, 2000));
|
|
||||||
let lw = cell * 0.18;
|
|
||||||
let dense = preset.starts_with("dk") || preset.contains("50") || preset.contains("75") || preset.contains("90");
|
|
||||||
for j in 0..=ny {
|
|
||||||
for i in 0..=nx {
|
|
||||||
let x = b.x0 + i as f64 * cell;
|
|
||||||
let y = b.y0 + j as f64 * cell;
|
|
||||||
let r = match preset {
|
|
||||||
p if p.contains("Horz") || p == "horz" || p.contains("horzBrick") => Rect::new(x, y, x + cell, y + lw),
|
|
||||||
p if p.contains("Vert") || p == "vert" => Rect::new(x, y, x + lw, y + cell),
|
|
||||||
p if p.contains("cross") || p.contains("Grid") || p.contains("grid") => {
|
|
||||||
p_add(&mut p_rect(x, y, cell, lw), &mut BezPath::new());
|
|
||||||
Rect::new(x, y, x + cell, y + lw).union(Rect::new(x, y, x + lw, y + cell))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let d = if dense { cell * 0.35 } else { cell * 0.2 };
|
|
||||||
Rect::new(x, y, x + d, y + d)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
p.extend(r.to_path(0.1).iter());
|
|
||||||
if preset.contains("cross") || preset.contains("Grid") || preset.contains("grid") {
|
|
||||||
p.extend(Rect::new(x, y, x + lw, y + cell).to_path(0.1).iter());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.fill_path(&p);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn p_rect(x: f64, y: f64, w: f64, h: f64) -> BezPath {
|
|
||||||
Rect::new(x, y, x + w, y + h).to_path(0.1)
|
|
||||||
}
|
|
||||||
fn p_add(_a: &mut BezPath, _b: &mut BezPath) {}
|
|
||||||
|
|
||||||
/// `pf` with its duotone colours resolved to plain RGB against the slide's colours and the
|
/// `pf` with its duotone colours resolved to plain RGB against the slide's colours and the
|
||||||
/// placeholder colour `ph` (theme background pictures recolour with `phClr`), so the decoded-image
|
/// placeholder colour `ph` (theme background pictures recolour with `phClr`), so the decoded-image
|
||||||
/// cache can apply and key them without a colour scheme.
|
/// cache can apply and key them without a colour scheme.
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
//! 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -750,3 +750,29 @@ fn text_glow_and_shadow_are_drawn() {
|
|||||||
let grey = |p: &[u8; 4]| p[0] > 60 && p[0] < 230 && p[0] == p[1] && p[1] == p[2];
|
let grey = |p: &[u8; 4]| p[0] > 60 && p[0] < 230 && p[0] == p[1] && p[1] == p[2];
|
||||||
assert!(count(&img, &grey) > count(&base, &grey) + 300, "a grey shadow appears");
|
assert!(count(&img, &grey) > count(&base, &grey) + 300, "a grey shadow appears");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pattern_fills_draw_powerpoints_tiles() {
|
||||||
|
// dkVert at one output pixel per pattern cell (0.75 pt): columns 11001100… across, as
|
||||||
|
// PowerPoint's export draws it; pct50 is a checkerboard.
|
||||||
|
use deckcraft_model::style::{Fill, PatternFill};
|
||||||
|
let rgb = |v| ColorRef::rgb(deckcraft_color::Rgba::rgb(v, v, v));
|
||||||
|
let shape = |id, x: f64, preset: &str| Shape {
|
||||||
|
id: ShapeId(id),
|
||||||
|
xfrm: Some(Xfrm::new(x, 0.0, 60.0, 60.0)),
|
||||||
|
fill: Some(Fill::Pattern(PatternFill { preset: preset.into(), fg: rgb(0), bg: rgb(255) })),
|
||||||
|
line: Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let p = deck_with(vec![shape(750, 0.0, "dkVert"), shape(751, 120.0, "pct50")]);
|
||||||
|
let img = render_slide(&p, 0, &RenderOpts { scale: 4.0 / 3.0, ..Default::default() });
|
||||||
|
let dark = |x: u32, y: u32| img.pixel(x, y)[0] < 128;
|
||||||
|
let row: Vec<bool> = (8..16).map(|x| dark(x, 20)).collect();
|
||||||
|
assert_eq!(row, [true, true, false, false, true, true, false, false], "dkVert columns");
|
||||||
|
assert!((8..16).all(|y| dark(8, y) == dark(8, 20)), "dkVert is vertical");
|
||||||
|
// pct50 starts at slide x = 120 pt = 160 px.
|
||||||
|
for (x, y) in [(168, 8), (169, 9), (170, 12)] {
|
||||||
|
assert_ne!(dark(x, y), dark(x + 1, y), "pct50 alternates across at {x},{y}");
|
||||||
|
assert_eq!(dark(x, y), dark(x + 1, y + 1), "pct50 repeats diagonally at {x},{y}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user