mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Render: a mask art root, symbol art root or thumbnail keeps its own opacity mask
The renderer drew the root node of an opacity mask's art, of placed symbol art, of an art brush piece, of an Image Trace source image and of a node thumbnail without its own opacity mask. Make Opacity Mask uses the top object as the mask art, so a mask made from an object that already had an opacity mask ignored that inner mask. These roots now draw through the same masked group as any other node, still drawn whatever their visibility and bounds, and an incomplete inner mask still leaves out the outer object.
This commit is contained in:
@@ -212,7 +212,7 @@ impl Renderer {
|
||||
self.draw_kept_node(ctx, f, p);
|
||||
self.fx_owner = owner;
|
||||
} else {
|
||||
self.draw_node(ctx, f, p, true);
|
||||
self.draw_forced_arc(ctx, f, p);
|
||||
}
|
||||
}
|
||||
(self.made_again, self.cur) = outer;
|
||||
@@ -260,6 +260,36 @@ mod tests {
|
||||
assert_eq!(again.pixels, img.pixels);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_brush_piece_applies_its_root_mask() {
|
||||
use vectorcraft_brush::{ArtBrush, ArtScale, Brush, BrushKind, Colorization};
|
||||
use vectorcraft_doc::OpacityMask;
|
||||
|
||||
let mut d = doc_with_line(Some("Masked art"), 1.0);
|
||||
let mut art = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(0.0, -5.0, 10.0, 5.0)),
|
||||
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||
);
|
||||
let black = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 100.0)),
|
||||
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||
);
|
||||
art.extras_mut().mask = Some(Box::new(OpacityMask::new(black, true)));
|
||||
let brush = Brush {
|
||||
name: "Masked art".into(),
|
||||
kind: BrushKind::Art(ArtBrush { art, scale: ArtScale::Stretch, colorization: Colorization::None, ..Default::default() }),
|
||||
};
|
||||
vectorcraft_brush::store(&mut d, &[brush]);
|
||||
|
||||
let mut renderer = Renderer::new();
|
||||
for _ in 0..2 {
|
||||
let img = render(&mut renderer, &d);
|
||||
assert_eq!(img.pixel(100, 50), [255, 255, 255, 255]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Object opacity fades a brushed stroke's art as a whole, with or without a dash and a fill:
|
||||
/// the same as the object at full opacity inside a group at that opacity.
|
||||
#[test]
|
||||
|
||||
@@ -361,7 +361,7 @@ impl Renderer {
|
||||
self.begin_frame();
|
||||
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
|
||||
self.clip_paths.clear();
|
||||
self.draw_node(&mut ctx, &frame, n, true);
|
||||
self.draw_forced_arc(&mut ctx, &frame, &Arc::new(n.clone()));
|
||||
let pm = self.finish(&mut ctx, vello_cpu::RasterizerSettings::default());
|
||||
self.end_frame();
|
||||
Some(Rendered { width: size, height: size, pixels: pm.data_as_u8_slice().to_vec() })
|
||||
|
||||
@@ -157,23 +157,39 @@ impl Renderer {
|
||||
self.stats.drawn += 1;
|
||||
return;
|
||||
}
|
||||
if let Some(m) = a.extras().mask.as_deref()
|
||||
&& !m.disabled
|
||||
&& !f.opts.outline
|
||||
&& !self.is_shape(a)
|
||||
{
|
||||
// A masked object is a non-isolated group of its own, drawn over its own pixels: the
|
||||
// mask is rendered over those only.
|
||||
let bounds = self.bounds_of(f.symbols, a);
|
||||
let Some((r, sub)) = group::area(ctx, f, bounds) else { return };
|
||||
let Some(mask) = self.mask_values(&sub, m, r.w, r.h) else { return };
|
||||
let blends = a.blend != vectorcraft_color::BlendMode::Normal || (!a.isolate && self.blends_through(a));
|
||||
self.group(ctx, f, Composite { mask: Some(mask), blends, bounds, ..Default::default() }, &mut |r, c, fr| r.draw_arc_body(c, fr, a));
|
||||
if self.draw_masked_arc(ctx, f, a, false) {
|
||||
return;
|
||||
}
|
||||
self.draw_arc_body(ctx, f, a);
|
||||
}
|
||||
|
||||
/// Draw `a` through its opacity mask when it has one. Forced roots draw regardless of their
|
||||
/// visibility and bounds.
|
||||
fn draw_masked_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, force: bool) -> bool {
|
||||
let Some(m) = a.extras().mask.as_deref().filter(|m| !m.disabled && !f.opts.outline && !self.is_shape(a)) else { return false };
|
||||
// A masked object is a non-isolated group of its own, drawn over its own pixels: the
|
||||
// mask is rendered over those only.
|
||||
let bounds = self.bounds_of(f.symbols, a);
|
||||
let Some((r, sub)) = group::area(ctx, f, bounds) else { return true };
|
||||
let Some(mask) = self.mask_values(&sub, m, r.w, r.h) else { return true };
|
||||
let blends = a.blend != vectorcraft_color::BlendMode::Normal || (!a.isolate && self.blends_through(a));
|
||||
self.group(ctx, f, Composite { mask: Some(mask), blends, bounds, ..Default::default() }, &mut |r, c, fr| {
|
||||
if force {
|
||||
r.draw_node(c, fr, a, true);
|
||||
} else {
|
||||
r.draw_arc_body(c, fr, a);
|
||||
}
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
/// Draw an `Arc` regardless of its visibility and bounds, applying its opacity mask.
|
||||
pub(crate) fn draw_forced_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
|
||||
if !self.draw_masked_arc(ctx, f, a, true) {
|
||||
self.draw_node(ctx, f, a, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// The coverage of opacity mask `m` per output pixel (row-major): its art rendered offscreen,
|
||||
/// as luminance ([`mask_value`]).
|
||||
pub(super) fn mask_values(&mut self, f: &Frame, m: &vectorcraft_doc::OpacityMask, w: u16, h: u16) -> Option<Vec<u8>> {
|
||||
@@ -489,7 +505,7 @@ impl Renderer {
|
||||
if kept {
|
||||
self.draw_kept_node(ctx, f, art);
|
||||
} else {
|
||||
self.draw_node(ctx, f, art, true);
|
||||
self.draw_forced_arc(ctx, f, art);
|
||||
}
|
||||
self.made_again = outer;
|
||||
}
|
||||
@@ -500,7 +516,7 @@ impl Renderer {
|
||||
pub(crate) fn draw_kept_node(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
|
||||
let live = vectorcraft_doc::live::is_live(a).then(|| a.clone());
|
||||
let outer = std::mem::replace(&mut self.live_root, live);
|
||||
self.draw_node(ctx, f, a, true);
|
||||
self.draw_forced_arc(ctx, f, a);
|
||||
self.live_root = outer;
|
||||
}
|
||||
|
||||
@@ -518,7 +534,7 @@ impl Renderer {
|
||||
fn draw_trace_view(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], view: TraceView, knockout: bool) {
|
||||
let Some((source, shapes)) = children.split_first() else { return };
|
||||
if view.shows_source() && matches!(source.kind, NodeKind::Image(_)) {
|
||||
self.draw_node(ctx, f, source, true);
|
||||
self.draw_forced_arc(ctx, f, source);
|
||||
}
|
||||
if view.shows_result() {
|
||||
self.draw_children(ctx, f, shapes, knockout);
|
||||
|
||||
@@ -662,6 +662,39 @@ fn trace_source_draws_the_source_image_of_the_listed_objects() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_source_applies_the_hidden_image_mask() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind, OpacityMask};
|
||||
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("trace-source".into(), ImageBlob::new("image/png", png));
|
||||
let image = ImageObject {
|
||||
key: "trace-source".into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
xf: Affine::translate((10.0, 10.0)) * Affine::scale(80.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut source = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
|
||||
source.visible = false;
|
||||
let mask = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
source.extras_mut().mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
let result = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
|
||||
let mut trace = Node::group(d.alloc_id(), vec![Arc::new(source), Arc::new(result)]);
|
||||
trace.extras_mut().trace = Some(Box::new(serde_json::json!({ "view": "tracingResult" })));
|
||||
let trace_id = trace.id;
|
||||
d.insert(Some(d.layers[0].id), 0, trace).unwrap();
|
||||
|
||||
let opts = RenderOptions { trace_views: true, trace_source: Some(Arc::new(vec![trace_id])), background: Some([255; 4]), ..Default::default() };
|
||||
let img = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert_eq!(img.pixel(30, 50), [255, 0, 0, 255]);
|
||||
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboards_white_on_pasteboard() {
|
||||
let d = Document::new(50.0, 50.0);
|
||||
@@ -747,6 +780,94 @@ fn opacity_mask_without_clip_and_inverted() {
|
||||
assert_eq!(img.pixel(80, 50), [255, 0, 0, 255], "inverted clip background → visible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_mask_art_root_applies_its_own_mask() {
|
||||
use vectorcraft_doc::OpacityMask;
|
||||
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut object = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mut art = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
let inner = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
art.extras_mut().mask = Some(Box::new(OpacityMask::new(inner, true)));
|
||||
art.visible = false;
|
||||
object.extras_mut().mask = Some(Box::new(OpacityMask::new(art, true)));
|
||||
let object_id = object.id;
|
||||
d.insert(Some(d.layers[0].id), 0, object).unwrap();
|
||||
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut renderer = Renderer::new();
|
||||
let img = renderer.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert_eq!(img.pixel(30, 50), [255, 0, 0, 255]);
|
||||
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255]);
|
||||
let view = renderer.render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { mask_view: Some(object_id), ..opts });
|
||||
assert_eq!(view.pixel(30, 50), [255, 255, 255, 255]);
|
||||
assert_eq!(view.pixel(70, 50), [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undecodable_image_in_mask_art_root_mask_leaves_out_outer_object() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind, OpacityMask};
|
||||
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("broken-root-mask-image".into(), ImageBlob::new("image/png", b"not a PNG".to_vec()));
|
||||
let mut object = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mut art = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
let image = ImageObject {
|
||||
key: "broken-root-mask-image".into(),
|
||||
width: 20,
|
||||
height: 20,
|
||||
xf: Affine::translate((30.0, 30.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
art.extras_mut().mask = Some(Box::new(OpacityMask::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(image))), false)));
|
||||
object.extras_mut().mask = Some(Box::new(OpacityMask::new(art, true)));
|
||||
let object_id = object.id;
|
||||
d.insert(Some(d.layers[0].id), 0, object).unwrap();
|
||||
|
||||
let opts = RenderOptions { background: Some([255; 4]), ..Default::default() };
|
||||
let mut renderer = Renderer::new();
|
||||
let img = renderer.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255]);
|
||||
let view = renderer.render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { mask_view: Some(object_id), ..opts });
|
||||
assert_eq!(view.pixel(70, 50), [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_symbol_art_root_applies_its_own_mask() {
|
||||
use vectorcraft_doc::{NodeKind, OpacityMask};
|
||||
|
||||
for grouped in [true, false] {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let red = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mut art = if grouped { Node::group(d.alloc_id(), vec![Arc::new(red)]) } else { red };
|
||||
let inner = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
art.extras_mut().mask = Some(Box::new(OpacityMask::new(inner, true)));
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Masked".into(), art: Arc::new(art) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Masked".into(), xf: Affine::IDENTITY });
|
||||
d.insert(Some(d.layers[0].id), 0, instance).unwrap();
|
||||
|
||||
let img = render(&d);
|
||||
assert_eq!(img.pixel(30, 50), [255, 0, 0, 255], "grouped {grouped}");
|
||||
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255], "grouped {grouped}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_thumbnail_applies_the_root_mask() {
|
||||
use vectorcraft_doc::OpacityMask;
|
||||
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut node = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mask = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
node.extras_mut().mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
|
||||
let img = Renderer::new().render_node_thumbnail(&d, &node, 100, Some([255; 4])).unwrap();
|
||||
assert_eq!(img.pixel(25, 50), [255, 0, 0, 255]);
|
||||
assert_eq!(img.pixel(75, 50), [255, 255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_mask_with_undecodable_image_leaves_out_object() {
|
||||
use vectorcraft_doc::{Effect, ImageBlob, ImageObject, NodeKind, OpacityMask};
|
||||
|
||||
Reference in New Issue
Block a user