mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #4789 from dotCooCoo/fix/mask-root-node-mask
Render: an object used as an opacity mask keeps its own opacity mask
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