Render: a feathered object's bevel and inner effects fade with it

A basic, directional or gradient feather faded only the object's body. Its
bevel, inner glow, inner shadow and satin were drawn afterwards over the
whole shape, so where the feather let the page show through, the bevel's
highlight and the inner glow screened whatever lay behind the object.
Text under the feathered edge of a beveled frame turned lighter until the
frame was moved off it.

Each inner effect is now faded by the same feathers before it blends with
what is behind the object, so a feathered object fades to transparent at
its edge with its effects.
This commit is contained in:
dotCooCoo committed 2026-11-11 21:53:45 -08:00
1 parent 6fff5328eb
commit 6854997294
1 file changed
+349 -216
+349 -216
View File
@@ -15,7 +15,8 @@
//! - Inner glow, bevel and emboss, satin: built like the inner shadow (an offset inverse of the
//! shape, blurred and clipped) with screen / multiply blending; satin XORs two offset copies.
//! - Feather: the object is masked by its shape inset by half the width and blurred, so the
//! edge fades from ~0 at the path to full strength `width` inside.
//! edge fades from ~0 at the path to full strength `width` inside. The basic, directional and
//! gradient feathers fade the inner effects with the object.
use designcraft_doc::Item;
use designcraft_geom::{Affine, BezPath, Rect, Shape, Vec2};
@@ -46,6 +47,17 @@ pub(crate) fn outset(it: &Item) -> f64 {
o
}
/// Whether `it` has a basic feather: a width on a closed shape.
fn basic_feather(it: &Item) -> bool {
it.effects.feather > 0.0 && it.path.is_closed()
}
/// Whether any of `it`'s feathers (basic, directional, gradient) fades it.
fn fades(it: &Item) -> bool {
let e = &it.effects;
basic_feather(it) || e.directional_feather.on && e.directional_feather.widths.iter().any(|w| *w > 0.0) || e.gradient_feather.on
}
/// (blur σ, growth) in points for an effect `size` with `pct` percent hardened.
fn split(size: f64, pct: f64) -> (f64, f64) {
let size = bound(size).max(0.0);
@@ -149,17 +161,245 @@ impl Renderer {
ctx.pop_layer();
}
}
// Gradient feather: the object (feathered or not) drawn in its own layer, then kept as
// much as the opacity gradient says.
let gf = &e.gradient_feather;
let directional = e.directional_feather.on && e.directional_feather.widths.iter().any(|w| *w > 0.0);
if gf.on || directional {
// The object, feathered or not, drawn in its own layer and faded there. A basic feather
// clips the body to the shape at device resolution; only its blurred mask may be drawn
// offscreen at a lower one.
let fading = fades(it);
let basic = basic_feather(it);
if fading {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, None, None, None, None);
}
// The object, feathered or not. Choke keeps that much of the width opaque (a shorter
// fade); Sharp and Rounded corners follow the inset shape more tightly than Diffused.
if e.feather > 0.0 && it.path.is_closed() {
if basic {
ctx.set_transform(f.view * xf);
ctx.push_clip_layer(bp);
}
self.draw_body(ctx, f, it, bp, xf, page);
if basic {
ctx.pop_layer();
}
if fading {
self.fade(ctx, f, it, bp, xf, reach);
ctx.pop_layer();
}
if !it.path.is_closed() {
return;
}
// The feathers fade the inner effects with the object: each one is faded in its own
// layer before it blends with what is behind.
let fade = fading.then_some(it);
// An image's edge is where it is opaque, not the frame: inner shadows, glows and bevels
// follow what is painted when the frame holds a graphic.
let painted = matches!(it.content, designcraft_doc::Content::Graphic(_)).then_some((it, page));
// Inner shadow, clipped to the shape.
if e.inner_shadow.on
&& let Some(c) = doc.resolve_color(&e.inner_shadow.color, 1.0)
{
let s = &e.inner_shadow;
let (sigma, choke) = split(s.size, s.choke);
let off = offset(doc.light_angle(s.angle, s.global_light), s.distance);
self.inner_edge(
ctx,
f,
reach,
bp,
xf,
off,
sigma,
choke,
color_of(&c, s.opacity.clamp(0.0, 1.0)),
crate::blend_mode(s.mode).mix,
painted,
fade,
);
}
// Inner glow: from the edges inwards (or out from the centre), screened over the object.
if e.inner_glow.on
&& let Some(c) = doc.resolve_color(&e.inner_glow.color, 1.0)
{
let g = &e.inner_glow;
let (sigma, choke) = split(g.size, g.choke);
// Precise keeps the glow close to the edge: a tighter blur.
let sigma = if g.technique == designcraft_doc::GlowTechnique::Precise { sigma * 0.4 } else { sigma };
let mix = crate::blend_mode(g.mode).mix;
let paint = color_of(&c, g.opacity.clamp(0.0, 1.0));
if g.center {
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), fade, |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
c.set_transform(fr.view * xf);
c.set_paint(paint);
c.fill_path(bp);
// Fade towards the edge: cut a band of the glow's size away.
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
c.set_transform(fr.view * xf);
c.set_paint(peniko::Color::BLACK);
c.set_stroke(kurbo::Stroke::new((bound(g.size) - choke).max(0.5) * 2.0).with_join(kurbo::Join::Round));
c.stroke_path(bp);
c.pop_layer();
c.pop_layer();
});
} else {
self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, mix, painted, fade);
}
}
// Satin: the shape shifted both ways along the angle, blurred, where exactly one covers.
if e.satin.on
&& let Some(c) = doc.resolve_color(&e.satin.color, 1.0)
{
let st = &e.satin;
let off = offset(st.angle, st.distance / 2.0);
let sigma = bound(st.size).max(0.0) / 2.0;
let paint = color_of(&c, st.opacity.clamp(0.0, 1.0));
let invert = st.invert;
self.with_blended_filters(ctx, f, reach, sigma, Mix::Multiply, (bp, xf), fade, |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
if invert {
c.set_transform(fr.view * xf);
c.set_paint(paint);
c.fill_path(bp);
}
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, if invert { Compose::DestOut } else { Compose::SrcOver })), None, None, None);
c.set_transform(fr.view * Affine::translate(off) * xf);
c.set_paint(paint);
c.fill_path(bp);
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::Xor)), None, None, None);
c.set_transform(fr.view * Affine::translate(-off) * xf);
c.fill_path(bp);
c.pop_layer();
c.pop_layer();
c.pop_layer();
});
}
// Bevel and Emboss (inner bevel): highlight from the lit side, shadow from the other.
if e.bevel.on {
let b = &e.bevel;
let off = offset(doc.light_angle(b.angle, b.global_light), bound(b.size) * (b.depth / 100.0).clamp(0.01, 10.0) * 0.5);
let sigma = bound(b.size).max(0.0) / 2.0;
if let Some(c) = doc.resolve_color(&b.highlight, 1.0) {
// The inverse shape moved away from the light lights the edges facing it.
let paint = color_of(&c, b.highlight_opacity.clamp(0.0, 1.0));
self.inner_edge(ctx, f, reach, bp, xf, off, sigma, 0.0, paint, Mix::Screen, painted, fade);
}
if let Some(c) = doc.resolve_color(&b.shadow, 1.0) {
let paint = color_of(&c, b.shadow_opacity.clamp(0.0, 1.0));
self.inner_edge(ctx, f, reach, bp, xf, -off, sigma, 0.0, paint, Mix::Multiply, painted, fade);
}
}
}
/// An inner shadow-like edge: everything outside the shape moved by `off`, blurred by
/// `sigma`, clipped to the shape and blended with `mix`. With `painted` (an object and the
/// page it shows on), the shape is what the object paints, so a transparent part of an image
/// is outside it: the edge runs along the image's opaque pixels, not the frame. With `fade`,
/// the edge fades with that object's feathers.
#[allow(clippy::too_many_arguments)]
fn inner_edge(
&mut self,
ctx: &mut RenderContext,
f: &Frame,
reach: Rect,
bp: &BezPath,
xf: Affine,
off: Vec2,
sigma: f64,
choke: f64,
paint: peniko::Color,
mix: Mix,
painted: Option<(&Item, ShownOn)>,
fade: Option<&Item>,
) {
let sigma = bound(sigma).max(0.0);
let choke = bound(choke);
if let Some((it, page)) = painted {
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), fade, |me, c, fr| {
let pad = sigma * 4.0 + choke + off.hypot() + 4.0 * fr.px;
let area = reach.inflate(pad, pad);
// The colour everywhere but under the painted pixels moved by `off`, blurred, ...
c.set_transform(fr.view);
c.push_layer(None, None, None, None, None);
c.push_layer(None, None, None, None, Some(blur(sigma)));
c.set_paint(paint);
c.fill_rect(&area);
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
let moved = Frame { view: fr.view * Affine::translate(off), ..*fr };
me.draw_body(c, &moved, it, bp, xf, page);
c.pop_layer();
c.pop_layer();
// ... kept only where the object paints.
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
me.draw_body(c, fr, it, bp, xf, page);
c.pop_layer();
c.pop_layer();
});
return;
}
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), fade, |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
// Everything outside the offset shape, so the colour bleeds in from the edges.
let m = Affine::translate(off) * xf;
let pad = sigma * 4.0 + choke + off.hypot() + 4.0 * fr.px;
let mut inv = m.transform_rect_bbox(bp.bounding_box()).inflate(pad, pad).to_path(0.1);
let mut shape = bp.clone();
shape.apply_affine(m);
inv.extend(shape.iter());
c.set_paint(paint);
c.set_fill_rule(peniko::Fill::EvenOdd);
c.fill_path(&inv);
c.set_fill_rule(peniko::Fill::NonZero);
if choke > 0.0 {
c.set_stroke(kurbo::Stroke::new(choke * 2.0).with_join(kurbo::Join::Round));
c.stroke_path(&shape);
}
c.pop_layer();
});
}
/// [`Renderer::with_filters`] clipped to `clip` (a path and its spread transform), blended with
/// `mix`. The effect's blend must see the object's backdrop, outside its clipping and offscreen
/// filter layers. Putting it inside the clip blends with transparent pixels instead. With
/// `fade`, what `draw` paints is faded by that object's feathers before it blends.
#[allow(clippy::too_many_arguments)]
fn with_blended_filters(
&mut self,
ctx: &mut RenderContext,
f: &Frame,
reach: Rect,
sigma: f64,
mix: Mix,
clip: (&BezPath, Affine),
fade: Option<&Item>,
draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame),
) {
let layered = mix != Mix::Normal || fade.is_some();
if layered {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None);
}
self.with_filters(ctx, f, reach, sigma, Some(clip), draw);
if let Some(it) = fade {
self.fade(ctx, f, it, clip.0, clip.1, reach);
}
if layered {
ctx.pop_layer();
}
}
/// Fade what was drawn in the current layer by `it`'s feathers: the basic feather's blurred
/// inset shape, a ramp for each directional feather side and the gradient feather. `bp` and
/// `xf` are the object's shape and spread transform, `reach` (spread space) what it paints.
fn fade(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, reach: Rect) {
let e = &it.effects;
// Choke keeps that much of the width opaque (a shorter fade); Sharp and Rounded corners
// follow the inset shape more tightly than Diffused.
if basic_feather(it) {
let fo = &e.feather_options;
let w = bound(e.feather) * (1.0 - fo.choke.clamp(0.0, 100.0) / 100.0).max(0.01);
let (sigma, join) = match fo.corners {
@@ -167,12 +407,7 @@ impl Renderer {
designcraft_doc::FeatherCorners::Rounded => (w / 3.0, kurbo::Join::Round),
designcraft_doc::FeatherCorners::Sharp => (w / 6.0, kurbo::Join::Miter),
};
// The body and its clip stay at device resolution; only the blurred mask may be drawn
// offscreen at a lower one.
ctx.set_transform(f.view * xf);
ctx.push_clip_layer(bp);
self.draw_body(ctx, f, it, bp, xf, page);
// Keep the body where the blurred inset silhouette is.
// Keep what was drawn where the blurred inset silhouette is.
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
self.with_filters(ctx, f, reach, sigma, None, |_, c, fr| {
@@ -190,9 +425,6 @@ impl Renderer {
c.pop_layer();
});
ctx.pop_layer();
ctx.pop_layer();
} else {
self.draw_body(ctx, f, it, bp, xf, page);
}
// Directional feather: one fading ramp per side, multiplied in.
let df = &e.directional_feather;
@@ -222,206 +454,9 @@ impl Renderer {
ctx.pop_layer();
}
}
if gf.on {
if e.gradient_feather.on {
gradient_feather_mask(ctx, f, it, xf);
}
if gf.on || directional {
ctx.pop_layer();
}
if !it.path.is_closed() {
return;
}
// An image's edge is where it is opaque, not the frame: inner shadows, glows and bevels
// follow what is painted when the frame holds a graphic.
let painted = matches!(it.content, designcraft_doc::Content::Graphic(_)).then_some((it, page));
// Inner shadow, clipped to the shape.
if e.inner_shadow.on
&& let Some(c) = doc.resolve_color(&e.inner_shadow.color, 1.0)
{
let s = &e.inner_shadow;
let (sigma, choke) = split(s.size, s.choke);
let off = offset(doc.light_angle(s.angle, s.global_light), s.distance);
self.inner_edge(
ctx,
f,
reach,
bp,
xf,
off,
sigma,
choke,
color_of(&c, s.opacity.clamp(0.0, 1.0)),
crate::blend_mode(s.mode).mix,
painted,
);
}
// Inner glow: from the edges inwards (or out from the centre), screened over the object.
if e.inner_glow.on
&& let Some(c) = doc.resolve_color(&e.inner_glow.color, 1.0)
{
let g = &e.inner_glow;
let (sigma, choke) = split(g.size, g.choke);
// Precise keeps the glow close to the edge: a tighter blur.
let sigma = if g.technique == designcraft_doc::GlowTechnique::Precise { sigma * 0.4 } else { sigma };
let mix = crate::blend_mode(g.mode).mix;
let paint = color_of(&c, g.opacity.clamp(0.0, 1.0));
if g.center {
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
c.set_transform(fr.view * xf);
c.set_paint(paint);
c.fill_path(bp);
// Fade towards the edge: cut a band of the glow's size away.
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
c.set_transform(fr.view * xf);
c.set_paint(peniko::Color::BLACK);
c.set_stroke(kurbo::Stroke::new((bound(g.size) - choke).max(0.5) * 2.0).with_join(kurbo::Join::Round));
c.stroke_path(bp);
c.pop_layer();
c.pop_layer();
});
} else {
self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, mix, painted);
}
}
// Satin: the shape shifted both ways along the angle, blurred, where exactly one covers.
if e.satin.on
&& let Some(c) = doc.resolve_color(&e.satin.color, 1.0)
{
let st = &e.satin;
let off = offset(st.angle, st.distance / 2.0);
let sigma = bound(st.size).max(0.0) / 2.0;
let paint = color_of(&c, st.opacity.clamp(0.0, 1.0));
let invert = st.invert;
self.with_blended_filters(ctx, f, reach, sigma, Mix::Multiply, (bp, xf), |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
if invert {
c.set_transform(fr.view * xf);
c.set_paint(paint);
c.fill_path(bp);
}
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, if invert { Compose::DestOut } else { Compose::SrcOver })), None, None, None);
c.set_transform(fr.view * Affine::translate(off) * xf);
c.set_paint(paint);
c.fill_path(bp);
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::Xor)), None, None, None);
c.set_transform(fr.view * Affine::translate(-off) * xf);
c.fill_path(bp);
c.pop_layer();
c.pop_layer();
c.pop_layer();
});
}
// Bevel and Emboss (inner bevel): highlight from the lit side, shadow from the other.
if e.bevel.on {
let b = &e.bevel;
let off = offset(doc.light_angle(b.angle, b.global_light), bound(b.size) * (b.depth / 100.0).clamp(0.01, 10.0) * 0.5);
let sigma = bound(b.size).max(0.0) / 2.0;
if let Some(c) = doc.resolve_color(&b.highlight, 1.0) {
// The inverse shape moved away from the light lights the edges facing it.
self.inner_edge(ctx, f, reach, bp, xf, off, sigma, 0.0, color_of(&c, b.highlight_opacity.clamp(0.0, 1.0)), Mix::Screen, painted);
}
if let Some(c) = doc.resolve_color(&b.shadow, 1.0) {
self.inner_edge(ctx, f, reach, bp, xf, -off, sigma, 0.0, color_of(&c, b.shadow_opacity.clamp(0.0, 1.0)), Mix::Multiply, painted);
}
}
}
/// An inner shadow-like edge: everything outside the shape moved by `off`, blurred by
/// `sigma`, clipped to the shape and blended with `mix`. With `painted` (an object and the
/// page it shows on), the shape is what the object paints, so a transparent part of an image
/// is outside it: the edge runs along the image's opaque pixels, not the frame.
#[allow(clippy::too_many_arguments)]
fn inner_edge(
&mut self,
ctx: &mut RenderContext,
f: &Frame,
reach: Rect,
bp: &BezPath,
xf: Affine,
off: Vec2,
sigma: f64,
choke: f64,
paint: peniko::Color,
mix: Mix,
painted: Option<(&Item, ShownOn)>,
) {
let sigma = bound(sigma).max(0.0);
let choke = bound(choke);
if let Some((it, page)) = painted {
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), |me, c, fr| {
let pad = sigma * 4.0 + choke + off.hypot() + 4.0 * fr.px;
let area = reach.inflate(pad, pad);
// The colour everywhere but under the painted pixels moved by `off`, blurred, ...
c.set_transform(fr.view);
c.push_layer(None, None, None, None, None);
c.push_layer(None, None, None, None, Some(blur(sigma)));
c.set_paint(paint);
c.fill_rect(&area);
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
let moved = Frame { view: fr.view * Affine::translate(off), ..*fr };
me.draw_body(c, &moved, it, bp, xf, page);
c.pop_layer();
c.pop_layer();
// ... kept only where the object paints.
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
me.draw_body(c, fr, it, bp, xf, page);
c.pop_layer();
c.pop_layer();
});
return;
}
self.with_blended_filters(ctx, f, reach, sigma, mix, (bp, xf), |_, c, fr| {
c.set_transform(fr.view);
c.push_layer(None, None, None, None, Some(blur(sigma)));
// Everything outside the offset shape, so the colour bleeds in from the edges.
let m = Affine::translate(off) * xf;
let pad = sigma * 4.0 + choke + off.hypot() + 4.0 * fr.px;
let mut inv = m.transform_rect_bbox(bp.bounding_box()).inflate(pad, pad).to_path(0.1);
let mut shape = bp.clone();
shape.apply_affine(m);
inv.extend(shape.iter());
c.set_paint(paint);
c.set_fill_rule(peniko::Fill::EvenOdd);
c.fill_path(&inv);
c.set_fill_rule(peniko::Fill::NonZero);
if choke > 0.0 {
c.set_stroke(kurbo::Stroke::new(choke * 2.0).with_join(kurbo::Join::Round));
c.stroke_path(&shape);
}
c.pop_layer();
});
}
/// [`Renderer::with_filters`] clipped to `clip` (a path and its spread transform), blended with
/// `mix`. The effect's blend must see the object's backdrop, outside its clipping and offscreen
/// filter layers. Putting it inside the clip blends with transparent pixels instead.
#[allow(clippy::too_many_arguments)]
fn with_blended_filters(
&mut self,
ctx: &mut RenderContext,
f: &Frame,
reach: Rect,
sigma: f64,
mix: Mix,
clip: (&BezPath, Affine),
draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame),
) {
if mix != Mix::Normal {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None);
}
self.with_filters(ctx, f, reach, sigma, Some(clip), draw);
if mix != Mix::Normal {
ctx.pop_layer();
}
}
/// Run `draw`, which pushes filter layers. Single-threaded contexts draw directly. On a
@@ -607,4 +642,102 @@ mod tests {
// The transparent half shows the page: no shadow painted there.
assert!(lum(150) > 700, "nothing over the transparent part: {}", lum(150));
}
/// A feathered object fades to transparent at its edge, its bevel and inner glow included: where
/// the feather shows what is behind the object, the highlight and shadow leave it as it is.
#[test]
fn feathers_fade_the_inner_effects_with_the_object() {
use designcraft_doc::build::NewDocument;
use designcraft_doc::{Document, Fill, SpreadRef};
let doc = |image: bool, feather: u8, inner: u8| {
let mut d = Document::new(&NewDocument::default());
let lid = d.default_layer();
let mut back = Item::new(
designcraft_doc::ItemId(d.alloc()),
lid,
designcraft_doc::Shape::Rectangle,
designcraft_geom::shapes::rectangle(Rect::new(50.0, 50.0, 350.0, 350.0)),
);
back.fill = Fill::swatch("C=0 M=100 Y=0 K=0");
d.insert_item(SpreadRef::Doc(0), back, None).unwrap();
let mut it = Item::new(
designcraft_doc::ItemId(d.alloc()),
lid,
designcraft_doc::Shape::Rectangle,
designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 300.0, 300.0)),
);
if image {
let img = image::RgbaImage::from_pixel(20, 20, image::Rgba([40, 160, 90, 255]));
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let aid = designcraft_doc::AssetId(d.alloc());
d.assets.insert(
aid,
std::sync::Arc::new(designcraft_doc::Asset {
id: aid,
name: "green.png".into(),
mime: "image/png".into(),
data: std::sync::Arc::new(png),
pixels: Some((20, 20)),
..Default::default()
}),
);
it.content = designcraft_doc::Content::Graphic(designcraft_doc::Graphic {
layer_options: None,
asset: aid,
size: (200.0, 200.0),
xf: Affine::translate((100.0, 100.0)),
clip: None,
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
wrap: Default::default(),
fill: Default::default(),
});
} else {
it.fill = Fill::swatch("C=100 M=0 Y=0 K=0");
}
let e = &mut it.effects;
match feather {
0 => e.feather = 30.0,
1 => (e.directional_feather.on, e.directional_feather.widths) = (true, [30.0; 4]),
_ => e.gradient_feather.on = true,
}
match inner {
0 => {}
1 => (e.bevel.on, e.bevel.size) = (true, 12.0),
_ => (e.inner_glow.on, e.inner_glow.color, e.inner_glow.opacity, e.inner_glow.size) = (true, "[Paper]".into(), 1.0, 20.0),
}
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
d
};
let px = |d: &Document, threads: u16, x: u32| {
let mut r = Renderer::new();
r.threads = threads;
r.render_page(d, &crate::Cache::new(), 0, 1.0, false, &crate::RenderOptions::default()).unwrap().pixel(x, 200)
};
let diff = |a: [u8; 4], b: [u8; 4]| a.iter().zip(b).map(|(p, q)| u32::from(p.abs_diff(q))).sum::<u32>();
for image in [false, true] {
for feather in 0..3 {
for threads in [0, 4] {
let plain = doc(image, feather, 0);
// The object's faded edges: the left and right ones for the basic and directional
// feathers, the right one for the gradient feather (opaque on the left).
let edges: &[u32] = if feather == 2 { &[299] } else { &[100, 299] };
for &x in edges {
let behind = px(&plain, threads, 70);
let faded = px(&plain, threads, x);
assert!(diff(faded, behind) < 40, "image {image} feather {feather}: the edge at {x} shows what is behind");
for inner in [1, 2] {
let with = px(&doc(image, feather, inner), threads, x);
assert!(
diff(with, faded) < 30,
"image {image} feather {feather} inner {inner} threads {threads}: {with:?} over the faded edge at {x}, {faded:?} without"
);
}
}
}
}
}
}
}