Envelopes: Anti-Alias and Preserve Shape Using change how distorted images draw

Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that
draws an image read them: a distorted image was always smoothed and always
cut into clip groups.

- With Anti-Alias off, each piece of a distorted image is marked pixelated
  (saved as `pixelated`). The renderer draws it with nearest-neighbor
  sampling and without mips, SVG export writes `image-rendering: pixelated`,
  and PDF export writes it without /Interpolate.
- With Preserve Shape Using Transparency, each piece is the image with a
  white opacity mask in the piece's shape instead of a clip group. The
  renderer paints an image whose mask is one plain white path straight into
  that path, as it does for an image in a clip group, when the path lies
  inside the image's frame.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 11:07:57 +07:00
committed by bflatastic
parent 4651e80973
commit ab69d8f6d9
15 changed files with 345 additions and 28 deletions
+15 -4
View File
@@ -157,8 +157,10 @@ fn affine_between(s: [Point; 3], d: [Point; 3]) -> Option<Affine> {
/// Image `n` (`im`) pushed through the warp as a raster mesh warp: its frame cut into
/// `w.cells`² cells of two triangles, each a clip group showing the image under the affine map
/// that takes the triangle where the warp takes its corners. Each piece reaches a little past its
/// edges (at most half a point) so neighbours overlap and no seams show.
/// that takes the triangle where the warp takes its corners (with Preserve Shape Using
/// Transparency, the image masked by the triangle; with Anti-Alias off, drawn without smoothing).
/// Each piece reaches a little past its edges (at most half a point) so neighbours overlap and no
/// seams show.
fn warp_image(n: &Node, im: &ImageObject, w: &Warper) -> Node {
let k = w.cells.max(1);
// Two triangles a cell, three objects each; an image an inner envelope already cut into
@@ -189,11 +191,20 @@ fn warp_image(n: &Node, im: &ImageObject, w: &Warper) -> Node {
if len > 1e-12 { *p + v * (grow / len) } else { *p }
})
.collect();
let mut clip = Node::path(n.id, PathData::single(SubPath::polyline(&tri, true)), Appearance::default());
let shape = PathData::single(SubPath::polyline(&tri, true));
let mut image = Node::new(n.id, NodeKind::Image(Box::new(ImageObject { xf, ..im.clone() })));
image.extras_mut().pixelated = !w.options.anti_alias;
if w.options.preserve_shape == PreserveShape::Transparency {
// Preserve Shape Using Transparency: the triangle is the piece's alpha (a
// white opacity mask) instead of its clipping path.
let white = Node::path(n.id, shape, Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0));
pieces.push(Arc::new(image.with_mask(Some(Box::new(crate::OpacityMask::new(white, true))))));
continue;
}
let mut clip = Node::path(n.id, shape, Appearance::default());
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
*clipping = true;
}
let image = Node::new(n.id, NodeKind::Image(Box::new(ImageObject { xf, ..im.clone() })));
pieces.push(Arc::new(Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })));
}
}
+4
View File
@@ -23,6 +23,9 @@ pub struct NodeExtras {
/// The group holds the glyph outlines of type. Geometry effects that reshape it move and scale
/// the placed gradients in it with the outlines.
pub type_outlines: bool,
/// An image drawn without smoothing: each pixel of the picture takes the color of the image
/// pixel nearest to it (an envelope's distorted image with Envelope Options › Anti-Alias off).
pub pixelated: bool,
/// Editable Shaper composition; the original art is retained in its source child.
pub shaper: Option<Box<crate::shaper::ShaperSpec>>,
/// The [`crate::GraphicStyle::id`] last applied to this object. It stays linked while it keeps
@@ -56,6 +59,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
graph: None,
series_index: None,
type_outlines: false,
pixelated: false,
shaper: None,
graphic_style: None,
attrs: None,
+5
View File
@@ -30,6 +30,7 @@ impl Serialize for Node {
e.graph.is_none(),
e.series_index.is_none(),
is_default(&e.type_outlines),
is_default(&e.pixelated),
e.shaper.is_none(),
false,
e.graphic_style.is_none(),
@@ -69,6 +70,7 @@ impl Serialize for Node {
field!("graph", &e.graph);
field!("series_index", &e.series_index);
field!("type_outlines", &e.type_outlines);
field!("pixelated", &e.pixelated);
field!("shaper", &e.shaper);
field!("kind", &self.kind);
field!("graphic_style", &e.graphic_style);
@@ -158,6 +160,8 @@ pub(super) struct NodeWire {
#[serde(default)]
type_outlines: bool,
#[serde(default)]
pixelated: bool,
#[serde(default)]
shaper: Option<Box<crate::shaper::ShaperSpec>>,
kind: NodeKind,
#[serde(default)]
@@ -189,6 +193,7 @@ impl From<NodeWire> for Node {
graph: w.graph,
series_index: w.series_index,
type_outlines: w.type_outlines,
pixelated: w.pixelated,
shaper: w.shaper,
graphic_style: w.graphic_style,
attrs: w.attrs,