mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 19:22:48 +00:00
Merge pull request #6868 from dotCooCoo/fix/blend-shadow-unclipped-mask
Render: blend steps' drop shadows draw under an opacity mask with Clip off
This commit is contained in:
@@ -505,17 +505,31 @@ fn glyphs_box(t: &vectorcraft_doc::TextObject, outset: f64) -> Option<Rect> {
|
||||
|
||||
/// How far the content of live object `n` (a blend, envelope or repeat) can paint past its visual
|
||||
/// bounds: for each object in it, how far the effects in a symbol instance's art paint past that
|
||||
/// art ([`art_effects_reach`]) and how far brush art can paint ([`crate::brush_fx::pad`]), on the
|
||||
/// farthest side. 0 for any other kind of object.
|
||||
/// art ([`art_effects_reach`]), how far brush art can paint ([`crate::brush_fx::pad`]) and how far
|
||||
/// a path's own effects paint past it ([`path_effects_reach`]), on the farthest side. 0 for any
|
||||
/// other kind of object.
|
||||
fn live_content_reach(symbols: &SymbolBoxes, n: &Node) -> f64 {
|
||||
if !vectorcraft_doc::live::is_live(n) {
|
||||
return 0.0;
|
||||
}
|
||||
let mut reach = 0.0f64;
|
||||
n.walk(&mut |c| reach = reach.max(art_effects_reach(symbols, c) + crate::brush_fx::pad(symbols, c)));
|
||||
n.walk(&mut |c| reach = reach.max(art_effects_reach(symbols, c).max(path_effects_reach(symbols, c)) + crate::brush_fx::pad(symbols, c)));
|
||||
reach
|
||||
}
|
||||
|
||||
/// How far the effects of path or compound path `n` (a drop shadow, a glow, a geometry effect)
|
||||
/// paint past its visual bounds, on its farthest side ([`visual_bounds`]): 0 when it has no
|
||||
/// visible effects, and for any other kind of object.
|
||||
fn path_effects_reach(symbols: &SymbolBoxes, n: &Node) -> f64 {
|
||||
if !has_fx(n) {
|
||||
return 0.0;
|
||||
}
|
||||
match (visual_bounds(symbols, n), n.visual_bounds_with(symbols)) {
|
||||
(Some(c), Some(v)) => [v.x0 - c.x0, v.y0 - c.y0, c.x1 - v.x1, c.y1 - v.y1].into_iter().fold(0.0, f64::max),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// How far the effects in the art of symbol instance `n` (and in the art of the symbols the
|
||||
/// instances in it show) paint past that art's visual bounds, on its farthest side, measured on the
|
||||
/// art as the instance places it before its effects run: 0 when the art has no visible effects, and
|
||||
|
||||
@@ -1260,6 +1260,45 @@ fn undecodable_image_in_nested_opacity_mask_leaves_out_outer_object() {
|
||||
assert_eq!(img.pixel(70, 70), [255, 255, 255, 255], "the incomplete inner mask invalidates the outer mask");
|
||||
}
|
||||
|
||||
/// The drop shadows of a blend's steps draw under an opacity mask with Clip off, where the mask
|
||||
/// art leaves them uncovered, as they draw without the mask; with Clip on they are hidden.
|
||||
#[test]
|
||||
fn blend_step_shadows_draw_under_an_unclipped_opacity_mask() {
|
||||
use vectorcraft_doc::live::{BlendSpacing, BlendSpec};
|
||||
use vectorcraft_doc::{NodeKind, OpacityMask};
|
||||
|
||||
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: serde_json::json!({"x": 15, "y": 15, "blur": 0}), visible: true };
|
||||
let blend = |d: &mut Document, mask: Option<bool>| {
|
||||
let end = |d: &mut Document, x: f64| {
|
||||
let mut n = rect_node(d, Rect::new(x, 10.0, x + 10.0, 20.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
n.appearance.effects.push(shadow.clone());
|
||||
Arc::new(n)
|
||||
};
|
||||
let ends = vec![end(d, 10.0), end(d, 70.0)];
|
||||
let spec = BlendSpec { spacing: BlendSpacing::Steps(2), ..Default::default() };
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Blend { children: ends, spec: Box::new(spec) });
|
||||
if let Some(clip) = mask {
|
||||
let white = rect_node(d, Rect::new(0.0, 0.0, 5.0, 5.0), Paint::solid(Color::WHITE), Paint::None, 0.0);
|
||||
n.extras_mut().mask = Some(Box::new(OpacityMask::new(white, clip)));
|
||||
}
|
||||
n
|
||||
};
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let draw = |mask: Option<bool>| {
|
||||
let mut d = Document::new(120.0, 60.0);
|
||||
let n = blend(&mut d, mask);
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
Renderer::new().render(&d, 120, 60, Affine::IDENTITY, &opts)
|
||||
};
|
||||
let (plain, unclipped, clipped) = (draw(None), draw(Some(false)), draw(Some(true)));
|
||||
for (x, y) in [(30, 30), (90, 32)] {
|
||||
assert!(plain.pixel(x, y)[0] < 128, "the shadow draws at ({x}, {y}) without a mask: {:?}", plain.pixel(x, y));
|
||||
assert_eq!(unclipped.pixel(x, y), plain.pixel(x, y), "the shadow draws at ({x}, {y}) under the unclipped mask");
|
||||
assert_eq!(clipped.pixel(x, y), [255, 255, 255, 255], "the clipped mask hides the shadow at ({x}, {y})");
|
||||
}
|
||||
assert_eq!(unclipped.pixel(15, 15), plain.pixel(15, 15), "the first end draws under the unclipped mask");
|
||||
}
|
||||
|
||||
/// #3580: each opacity mask is rendered over its object's pixels, not over the whole frame. 300
|
||||
/// masked 4 × 4 pt squares on a 1000 × 1000 page draw offscreen in proportion to their bounds, and
|
||||
/// still draw where their masks show them.
|
||||
|
||||
Reference in New Issue
Block a user