mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-09 03:51:42 +00:00
M8: a scaled node's spread shadow keeps its room in SVG (#1803)
The SVG export blurs a spread drop shadow in a filter whose region is the silhouette's page bounds grown by the spread's mitre and the blur. The spread grows the outline in the node's own units, though, and the transform then scales it: on a star scaled three times, a spread of 10 mitred out to some 97 units while the region only made room for 48, so SVG viewers cut the shadow's points off. The spread now grows the region as far across and down as the transform takes a local step of that length, as the paint bounds already do. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
83c28cf3ae
commit
6b2e464361
2 files changed
+49
-3
No files matched your search
@@ -83,3 +83,45 @@ fn hostile_shadow_values_still_export() {
|
||||
assert!(export_svg(&d, &[n], &SvgOptions::default()).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
/// The filter a spread shadow is blurred in makes room for the spread as the node's transform
|
||||
/// draws it: on a star scaled three times, the spread's mitres reach three times as far, and on a
|
||||
/// skewed one further across than down.
|
||||
#[test]
|
||||
fn a_scaled_spread_shadow_keeps_its_filter_room() {
|
||||
use figcraft_doc::SPREAD_MITER_LIMIT;
|
||||
use figcraft_geom::{Affine, kurbo};
|
||||
let star = NodeKind::Star { count: 5, inner_ratio: 0.4 };
|
||||
let s =
|
||||
Shadow { color: Color::from_hex("000000").unwrap(), offset: (6.0, -9.0), radius: 4.0, spread: 10.0, show_behind: true, ..Default::default() };
|
||||
for (why, xf) in [
|
||||
("scaled 3", Affine::scale(3.0)),
|
||||
("scaled (3, 1)", Affine::scale_non_uniform(3.0, 1.0)),
|
||||
("skewed", Affine::new([1.0, 0.0, 2.0, 1.0, 0.0, 0.0])),
|
||||
("turned and stretched", Affine::rotate(0.5) * Affine::scale_non_uniform(3.0, 1.0)),
|
||||
("as drawn", Affine::IDENTITY),
|
||||
] {
|
||||
let (mut d, n) = with_effects(star.clone(), vec![EffectKind::DropShadow(s.clone())]);
|
||||
let t = d.get(n).unwrap().transform;
|
||||
d.get_mut(n).unwrap().transform = t * xf;
|
||||
let svg = export(&d, n);
|
||||
let tag = &svg[svg.find("<filter ").unwrap()..];
|
||||
let attr = |name: &str| -> f64 {
|
||||
let at = tag.find(&format!(" {name}=\"")).unwrap() + name.len() + 3;
|
||||
tag[at..at + tag[at..].find('"').unwrap()].parse().unwrap()
|
||||
};
|
||||
let region = kurbo::Rect::new(attr("x"), attr("y"), attr("x") + attr("width"), attr("y") + attr("height"));
|
||||
// What the shadow paints, as the renderer draws it: the outline stroked twice the spread
|
||||
// wide with mitred joins, through the node's transform, moved by the offset (in the
|
||||
// export's frame, which starts at the node's export bounds) and blurred.
|
||||
let bounds = figcraft_render::export_bounds(&d, n).unwrap();
|
||||
let at = Affine::translate((s.offset.0 - bounds.x0, s.offset.1 - bounds.y0)) * d.world_transform(n);
|
||||
let shape = figcraft_render::node_outline(&d, d.get(n).unwrap()).unwrap();
|
||||
let stroke = kurbo::Stroke::new(s.spread * 2.0).with_join(kurbo::Join::Miter).with_miter_limit(SPREAD_MITER_LIMIT);
|
||||
let mut grown = kurbo::stroke(shape.0.iter(), &stroke, &Default::default(), 0.01);
|
||||
grown.apply_affine(at);
|
||||
let blur = s.radius * 1.5;
|
||||
let painted = figcraft_geom::Shape::bounding_box(&grown).inflate(blur, blur);
|
||||
assert!(region.union(painted) == region, "{why}: the filter {region:?} cuts the shadow at {painted:?}");
|
||||
}
|
||||
}
|
||||
@@ -1052,10 +1052,14 @@ impl W<'_> {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let reach = s.spread.abs() * SPREAD_MITER_LIMIT + s.radius.abs() * 1.5 + 2.0;
|
||||
// The spread grows the outline in its own units (mitres up to the limit), drawn through
|
||||
// `xf`: as far across and down as a local step that long goes. The blur is in page units.
|
||||
let spread = if s.spread.is_finite() { s.spread.abs() * SPREAD_MITER_LIMIT } else { 0.0 };
|
||||
let [a, b_, c, d, _, _] = xf.as_coeffs();
|
||||
let blur = s.radius.abs() * 1.5 + 2.0;
|
||||
let region = b.inflate(spread * a.hypot(c) + blur, spread * b_.hypot(d) + blur);
|
||||
let cover = if s.show_behind { None } else { self.cover_input(n, xf) };
|
||||
let filter =
|
||||
self.blur_filter(s.radius, b.inflate(reach, reach), cover.as_deref()).map(|f| format!(" filter=\"url(#{f})\"")).unwrap_or_default();
|
||||
let filter = self.blur_filter(s.radius, region, cover.as_deref()).map(|f| format!(" filter=\"url(#{f})\"")).unwrap_or_default();
|
||||
let op = f64::from(s.color.a * n.opacity.clamp(0.0, 1.0));
|
||||
let blend = if matches!(s.blend, figcraft_color::BlendMode::Normal | figcraft_color::BlendMode::PassThrough) {
|
||||
String::new()
|
||||
|
||||
Reference in new issue
Block a user