mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
This commit is contained in:
@@ -18,6 +18,13 @@ use vello_cpu::{Pixmap, RenderContext, peniko};
|
||||
/// Longest side of the rasterised gradient, in pixels (bilinear sampling smooths the rest).
|
||||
const MAX_SIDE: f64 = 384.0;
|
||||
|
||||
/// Most pixel × outline-edge tests one `shape` gradient may cost; a denser outline is simplified
|
||||
/// to fit (each pixel tests every edge).
|
||||
const MAX_EDGE_WORK: u64 = 64_000_000;
|
||||
|
||||
/// Fewest edges a simplified polygon keeps, so a coarse outline still has a shape.
|
||||
const MIN_POLY_EDGES: usize = 8;
|
||||
|
||||
/// Colour position (0 at the focus, 1 at the edge) of `p` for path `kind` in `bounds` with the
|
||||
/// focus rect `focus` (fractions inset from left, top, right, bottom), `outline` being the
|
||||
/// shape's flattened polygon(s) for `shape`.
|
||||
@@ -136,6 +143,65 @@ pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
|
||||
out
|
||||
}
|
||||
|
||||
/// `outline` cut down to at most about `max_edges` edges in total: first by dropping points
|
||||
/// closer than `tol` to the last kept one (detail finer than a raster pixel), then, if still too
|
||||
/// many, by keeping every k-th point. Each polygon stays closed. Returns it unchanged when it fits.
|
||||
fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
|
||||
let edges = |o: &[Vec<Point>]| o.iter().map(|p| p.len().saturating_sub(1)).sum::<usize>();
|
||||
if edges(&outline) <= max_edges {
|
||||
return outline;
|
||||
}
|
||||
let tol = if tol.is_finite() && tol > 0.0 { tol } else { 0.0 };
|
||||
let mut out: Vec<Vec<Point>> = outline
|
||||
.into_iter()
|
||||
.map(|poly| {
|
||||
let mut kept: Vec<Point> = Vec::with_capacity(poly.len());
|
||||
let last = poly.last().copied();
|
||||
for p in poly {
|
||||
if kept.last().is_none_or(|q| (p - *q).hypot() >= tol) {
|
||||
kept.push(p);
|
||||
}
|
||||
}
|
||||
if let (Some(l), Some(k)) = (last, kept.last().copied())
|
||||
&& k != l
|
||||
{
|
||||
kept.push(l);
|
||||
}
|
||||
kept
|
||||
})
|
||||
.filter(|p| p.len() > 2)
|
||||
.collect();
|
||||
let total = edges(&out);
|
||||
if total > max_edges {
|
||||
let k = total.div_ceil(max_edges.max(1)).max(2);
|
||||
for poly in &mut out {
|
||||
let n = poly.len();
|
||||
if n <= MIN_POLY_EDGES + 1 {
|
||||
continue;
|
||||
}
|
||||
let k = k.min((n - 1) / MIN_POLY_EDGES).max(1);
|
||||
let last = poly.last().copied();
|
||||
let mut kept: Vec<Point> = poly.iter().step_by(k).copied().collect();
|
||||
if let (Some(l), Some(q)) = (last, kept.last().copied())
|
||||
&& q != l
|
||||
{
|
||||
kept.push(l);
|
||||
}
|
||||
*poly = kept;
|
||||
}
|
||||
// Polygons too small to thin stay whole; drop the smallest ones if they alone overflow.
|
||||
if edges(&out) > max_edges {
|
||||
out.sort_by_key(|p| std::cmp::Reverse(p.len()));
|
||||
let mut sum = 0usize;
|
||||
out.retain(|p| {
|
||||
sum = sum.saturating_add(p.len().saturating_sub(1));
|
||||
sum <= max_edges
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A 256-entry colour ramp for the sorted, already expanded `stops` (linear in sRGB between them).
|
||||
pub fn ramp(stops: &[(f64, Rgba)]) -> Vec<Rgba> {
|
||||
(0..256)
|
||||
@@ -166,7 +232,14 @@ pub fn fill(ctx: &mut RenderContext, kind: &str, focus: [f64; 4], stops: &[(f64,
|
||||
let longest = bounds.width().max(bounds.height()) * k;
|
||||
let s = (MAX_SIDE / longest).min(1.0) * k;
|
||||
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 4096), ((bounds.height() * s).ceil() as u32).clamp(1, 4096));
|
||||
let outline = if kind == "shape" { polygons(path) } else { Vec::new() };
|
||||
let outline = if kind == "shape" {
|
||||
let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1);
|
||||
let max_edges = usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES);
|
||||
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
|
||||
simplify(polygons(path), pixel, max_edges)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let lut = ramp(stops);
|
||||
let a = alpha.clamp(0.0, 1.0);
|
||||
let mut data = Vec::with_capacity((pw * ph) as usize);
|
||||
@@ -252,6 +325,53 @@ mod tests {
|
||||
assert!((half - 0.5).abs() < 0.02, "{half}");
|
||||
}
|
||||
|
||||
fn many_segment_square(n: usize) -> BezPath {
|
||||
// A 200x200 square whose sides are split into `n` segments each.
|
||||
let mut p = BezPath::new();
|
||||
p.move_to((0.0, 0.0));
|
||||
let corners = [(200.0, 0.0), (200.0, 200.0), (0.0, 200.0), (0.0, 0.0)];
|
||||
let mut from = (0.0, 0.0);
|
||||
for to in corners {
|
||||
for i in 1..=n {
|
||||
let t = i as f64 / n as f64;
|
||||
p.line_to((from.0 + (to.0 - from.0) * t, from.1 + (to.1 - from.1) * t));
|
||||
}
|
||||
from = to;
|
||||
}
|
||||
p.close_path();
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_outlines_are_simplified_to_the_work_budget() {
|
||||
// Below the per-polygon flatten cap, so the simplifier has to do the work.
|
||||
let polys = polygons(&many_segment_square(20_000));
|
||||
assert!(polys.iter().map(|p| p.len() - 1).sum::<usize>() >= 80_000);
|
||||
let b = Rect::new(0.0, 0.0, 200.0, 200.0);
|
||||
let simple = simplify(polys, 200.0 / 384.0, 400);
|
||||
let edges: usize = simple.iter().map(|p| p.len() - 1).sum();
|
||||
assert!((MIN_POLY_EDGES..=400).contains(&edges), "{edges}");
|
||||
// Still the square: half way to a side or a corner is 0.5.
|
||||
for q in [Point::new(150.0, 100.0), Point::new(150.0, 150.0), Point::new(100.0, 50.0)] {
|
||||
let t = position("shape", b, CENTRE, &simple, q);
|
||||
assert!((t - 0.5).abs() < 0.01, "{q:?} {t}");
|
||||
}
|
||||
// Small outlines pass through untouched.
|
||||
let tri = vec![vec![Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(0.0, 1.0), Point::new(0.0, 0.0)]];
|
||||
assert_eq!(simplify(tri.clone(), 10.0, 400), tri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dense_shape_gradient_renders_promptly() {
|
||||
let path = many_segment_square(20_000);
|
||||
let mut ctx = RenderContext::new(400, 400);
|
||||
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (1.0, Rgba::rgb(255, 255, 255))];
|
||||
let t = std::time::Instant::now();
|
||||
fill(&mut ctx, "shape", CENTRE, &stops, Rect::new(0.0, 0.0, 200.0, 200.0), &path, Affine::scale(2.0), 1.0);
|
||||
// Unbudgeted this is 147k pixels × 80k edges ≈ 1.2e10 tests (minutes); budgeted, well under a second in release.
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(20), "{:?}", t.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ramp_runs_through_the_stops() {
|
||||
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (0.5, Rgba::rgb(128, 128, 128)), (1.0, Rgba::rgb(255, 255, 255))];
|
||||
|
||||
Reference in New Issue
Block a user