mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Gradient mesh: an opaque patch is backed by its outline, so patches that narrow to slivers leave no holes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
cb968353a5
commit
7add248dc3
@@ -290,6 +290,26 @@ impl Renderer {
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
let grow = f.px * 0.5;
|
||||
let alpha = self.alpha;
|
||||
// Where a patch narrows to slivers, many quads share each pixel, each covering a little of it: their edges
|
||||
// composite to part coverage, and hard edges drop each one. An opaque patch is backed by its own outline in
|
||||
// its middle colour, one path that covers fully, so the background can't show through.
|
||||
for r in 0..m.rows as usize {
|
||||
for c in 0..m.cols as usize {
|
||||
if [m.idx(r, c), m.idx(r, c + 1), m.idx(r + 1, c), m.idx(r + 1, c + 1)].iter().any(|&i| m.points[i].opacity < 1.0) {
|
||||
continue;
|
||||
}
|
||||
let (top, right, bottom, left) = (m.edge_u(r, c), m.edge_v(r, c + 1), m.edge_u(r + 1, c), m.edge_v(r, c));
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to(top.p0);
|
||||
bp.curve_to(top.p1, top.p2, top.p3);
|
||||
bp.curve_to(right.p1, right.p2, right.p3);
|
||||
bp.curve_to(bottom.p2, bottom.p1, bottom.p0);
|
||||
bp.curve_to(left.p2, left.p1, left.p0);
|
||||
bp.close_path();
|
||||
ctx.set_paint(f.ink.color(&m.color_at(r, c, 0.5, 0.5).0, alpha));
|
||||
ctx.fill_path(&bp);
|
||||
}
|
||||
}
|
||||
for q in quads.iter() {
|
||||
if q.opacity <= 0.0 {
|
||||
continue;
|
||||
@@ -326,6 +346,42 @@ impl Renderer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_mesh_patch_of_slivers_leaves_no_holes_with_hard_edges() {
|
||||
// GUARDS: patches that narrow to slivers (here fanning out from nearly one point) draw as many sub-pixel quads;
|
||||
// with hard edges each was dropped below half coverage, leaving holes through the mesh.
|
||||
use vectorcraft_doc::live::MeshPoint;
|
||||
let point = |x: f64, y: f64, handles: [(f64, f64); 4]| MeshPoint {
|
||||
p: vectorcraft_geom::Point::new(x, y),
|
||||
color: vectorcraft_color::Color::rgb(0.7, 0.8, 0.9),
|
||||
opacity: 1.0,
|
||||
handles: handles.map(|(x, y)| Vec2::new(x, y)),
|
||||
};
|
||||
let rows = [
|
||||
((100.0, 60.0), (84.0, 46.0)),
|
||||
((98.5, 60.0), (60.0, 51.0)),
|
||||
((97.8, 60.0), (42.0, 52.0)),
|
||||
((70.0, 59.0), (28.0, 51.5)),
|
||||
((15.0, 56.0), (12.0, 55.0)),
|
||||
];
|
||||
let mut points = vec![];
|
||||
for (i, &((x0, y0), (x1, y1))) in rows.iter().enumerate() {
|
||||
let along = if i < 3 { 5.0 } else { 20.0 };
|
||||
points.push(point(x0, y0, [((x1 - x0) * 0.35, (y1 - y0) * 0.1), (0.0, 0.0), (-along, 0.0), (if i > 0 { 5.0 } else { 0.0 }, 0.0)]));
|
||||
points.push(point(x1, y1, [(0.0, 0.0), ((x0 - x1) * 0.35, (y0 - y1) * 0.6), (-8.0, 0.5), (8.0, -0.5)]));
|
||||
}
|
||||
let mut d = Document::new(110.0, 30.0);
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Mesh(GradientMesh { rows: 4, cols: 1, points }))).unwrap();
|
||||
let view = Affine::scale(4.0) * Affine::translate((0.0, -40.0));
|
||||
let draw = |anti_alias| crate::Renderer::new().render(&d, 440, 120, view, &crate::RenderOptions { anti_alias, ..Default::default() });
|
||||
let (smooth, hard) = (draw(crate::AntiAlias::Art), draw(crate::AntiAlias::None));
|
||||
let holes =
|
||||
(0..120).flat_map(|y| (0..440).map(move |x| (x, y))).filter(|&(x, y)| smooth.pixel(x, y)[3] >= 200 && hard.pixel(x, y)[3] == 0).count();
|
||||
assert!(holes < 60, "{holes} pixels the smooth render covers are empty with hard edges");
|
||||
}
|
||||
use vectorcraft_doc::{Appearance, EnvelopeKind, Symbol};
|
||||
use vectorcraft_geom::{Affine, Rect, shapes};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user