mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 14:14:11 +00:00
Merge pull request #5859 from mockydevs/fix/inner-glow-image-pixels
Effects: an image's Inner Glow follows its visible pixels, not its frame (#5308)
This commit is contained in:
@@ -697,11 +697,48 @@ impl Renderer {
|
||||
} else {
|
||||
paint(self, ctx, f);
|
||||
}
|
||||
// Above the content: inner glows, clipped to its outline.
|
||||
// Above the content: inner glows, clipped to its outline. An image's glow follows its
|
||||
// visible pixels, not its frame (#5308).
|
||||
let Some((g, rule)) = content.outline else { return };
|
||||
let pixels = matches!(content.node.kind, NodeKind::Image(_));
|
||||
for fx in rfx {
|
||||
let RasterFx::InnerGlow { mode, opacity, blur, color, center } = fx else { continue };
|
||||
let filter = shadow_filter(0.0, 0.0, *blur, pcolor(f.ink, color));
|
||||
if pixels {
|
||||
self.with_filters(ctx, f, g.bounding_box(), *blur, Some((*mode, *opacity)), |r, c, fr, comp| {
|
||||
if let Some((m, o)) = comp {
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(blend_mode(m)), Some(o), None, None);
|
||||
}
|
||||
c.set_transform(fr.view);
|
||||
c.set_fill_rule(fill_rule(rule));
|
||||
c.push_clip_layer(g);
|
||||
c.push_layer(None, None, None, None, Some(filter));
|
||||
if !*center {
|
||||
// Everything around the visible pixels, so the glow bleeds in from their edges.
|
||||
let pad = blur * 2.0 + 4.0 * fr.px;
|
||||
c.set_transform(fr.view);
|
||||
c.set_paint(peniko::Color::BLACK);
|
||||
c.fill_rect(&g.bounding_box().inflate(pad, pad));
|
||||
c.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut)), None, None, None);
|
||||
}
|
||||
// The glow is cast by the pixels' alpha alone (the filter takes no color from them).
|
||||
paint(r, c, fr);
|
||||
if !*center {
|
||||
c.pop_layer();
|
||||
}
|
||||
c.pop_layer();
|
||||
// Only where the image shows, as much as it shows.
|
||||
c.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestIn)), None, None, None);
|
||||
paint(r, c, fr);
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
if comp.is_some() {
|
||||
c.pop_layer();
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
self.with_filters(ctx, f, g.bounding_box(), *blur, Some((*mode, *opacity)), |_, c, fr, comp| {
|
||||
// The blend layer goes outside the clip: a clip layer is isolated, so a blend
|
||||
// inside it would mix with nothing instead of the content below.
|
||||
|
||||
@@ -1076,3 +1076,44 @@ fn photoshop_filters_on_content_larger_than_the_budget() {
|
||||
assert!(lum(img.pixel(190, 100)) >= WHITE - 3 * 25, "{id}: outside {:?}", img.pixel(190, 100));
|
||||
}
|
||||
}
|
||||
|
||||
/// #5308: an image's Inner Glow follows its visible pixels, not its frame. A 40 × 40 image whose
|
||||
/// left half is transparent and right half opaque black, on a black page, with a white glow from
|
||||
/// the edges: the transparent half stays black, and the glow lights the opaque half's left edge
|
||||
/// (inside the frame), on the single-threaded and the multithreaded renderer alike.
|
||||
#[test]
|
||||
fn an_image_inner_glow_follows_its_visible_pixels() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let rgba: Vec<u8> = (0..40 * 40).flat_map(|i| if i % 40 < 20 { [0, 0, 0, 0] } else { [0, 0, 0, 255] }).collect();
|
||||
let png = crate::encode::png::encode(&rgba, 40, 40, &Default::default()).unwrap();
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("half".into(), ImageBlob::new("image/png", png));
|
||||
let im = ImageObject {
|
||||
key: "half".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((30.0, 30.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects = vec![Effect {
|
||||
id: "stylize.innerGlow".into(),
|
||||
params: json!({"color": "#ffffff", "mode": "normal", "opacity": 100, "blur": 4}),
|
||||
visible: true,
|
||||
}];
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let opts = RenderOptions { background: Some([0, 0, 0, 255]), ..Default::default() };
|
||||
for threads in [0, 3] {
|
||||
let mut r = Renderer::new();
|
||||
r.threads = threads;
|
||||
let img = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
for (x, y) in [(31, 50), (40, 31), (45, 50)] {
|
||||
assert!(lum(img.pixel(x, y)) < 10, "threads {threads}: no glow on the transparent pixels at ({x}, {y}): {:?}", img.pixel(x, y));
|
||||
}
|
||||
assert!(lum(img.pixel(51, 50)) > 60, "threads {threads}: the glow lights the visible pixels' edge: {:?}", img.pixel(51, 50));
|
||||
assert!(lum(img.pixel(68, 50)) > 60, "threads {threads}: and the frame's edge where the pixels reach it: {:?}", img.pixel(68, 50));
|
||||
assert!(lum(img.pixel(60, 50)) < lum(img.pixel(51, 50)), "threads {threads}: fading inward");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Properties: Quick Actions has Repeat Options for one selected repeat, #5181; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
|
||||
Reference in New Issue
Block a user