Files
figcraft/crates/doc/src/effects_tests.rs
BFlatandClaude Opus 5.5 f9b4609631 M3: effects fidelity: backdrop blur in clipped frames, exact shadow spread, glass, noise and texture (#69)
- Background blur sees the whole backdrop under the layer: the renderer keeps the stack of
  isolated surfaces it draws into (clipping frames, rounded clips, faded groups, masks) and
  flattens it for the layer's area, reading past the layer's own box. A blur inside a clipping
  frame now shows the frame's fill instead of the sharp layers below, on the canvas and in PNG
  exports. The PDF export draws background blur and glass through the same code from a render of
  what lies under the node (`backdrop::render_beneath`, which the PDF's backdrop now shares), and
  its images match the PNG's pixels.
- Shadow spread offsets the outline: the shape filled and stroked twice the spread wide with
  mitred joins (grow) or cut away (shrink), the stroked bands cached per outline. Ellipses stay
  ellipses, stars keep sharp points, text keeps its letterforms, rounded corners grow their
  radius. Inner shadows shrink the outline the same way. Canvas, PNG and PDF share the shadow
  code; SVG writes spread shadows as the same geometry and now exports inner shadows.
- Glass effect (light angle and intensity, refraction, depth, dispersion, frost, splay): the
  frosted backdrop bent towards the inside within `depth` of the edge, colour fringes, light on
  the rim, from an exact distance transform with smoothed normals.
- Noise: mono, duo and multi colour modes, grain size, density, opacity and blend mode; grains
  live in the layer's coordinates (stable when panning and zooming). Texture is now drawn
  (pixels scattered by the radius, grain size, clip to shape).
- Design panel rows for noise mode/colours/opacity, texture clip to shape and every glass value;
  `effect.add` takes `glass`; REST import of NOISE, TEXTURE and GLASS; Inspect rows and CSS;
  the scale tool scales grain sizes, texture radius, glass depth and frost.
- Tests in new modules: render backdrop/spread/grain tests, PDF backdrop tests, SVG, doc,
  engine and REST effect tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:59:59 +07:00

81 lines
3.9 KiB
Rust

//! Effect kinds: names, files from before noise/texture grew modes, glass values and reach.
use crate::*;
#[test]
fn effect_types_by_name() {
for (label, ty) in Effect::TYPES {
let e = Effect::of_type(ty).unwrap();
assert_eq!(e.label(), label);
assert!(e.visible);
}
assert!(matches!(Effect::of_type("DROP_SHADOW").unwrap().kind, EffectKind::DropShadow(_)));
assert!(matches!(Effect::of_type("background blur").unwrap().kind, EffectKind::BackgroundBlur { .. }));
assert!(matches!(Effect::of_type("blur").unwrap().kind, EffectKind::LayerBlur { .. }));
assert!(matches!(Effect::of_type("Glass").unwrap().kind, EffectKind::Glass(_)));
assert!(Effect::of_type("sparkle").is_none());
}
#[test]
fn old_noise_and_texture_files_still_open() {
let e: Effect = serde_json::from_str(r#"{"type":"noise","size":1.0,"density":0.3,"color":{"r":1.0,"g":0.0,"b":0.0,"a":0.5}}"#).unwrap();
let EffectKind::Noise(n) = &e.kind else { panic!("{e:?}") };
assert_eq!((n.mode, n.size, n.density, n.size_y), (NoiseMode::Mono, 1.0, 0.3, None));
assert_eq!(n.color.a, 0.5);
let e: Effect = serde_json::from_str(r#"{"type":"texture","size":2.0,"radius":3.0,"visible":false}"#).unwrap();
let EffectKind::Texture(t) = &e.kind else { panic!("{e:?}") };
assert_eq!((t.size, t.radius, t.clip_to_shape, e.visible), (2.0, 3.0, false, false));
// Camel-case names (as in the REST API) are read too.
let e: Effect = serde_json::from_str(r#"{"type":"glass","lightAngle":30,"lightIntensity":0.5,"depth":12}"#).unwrap();
let EffectKind::Glass(g) = &e.kind else { panic!("{e:?}") };
assert_eq!((g.light_angle, g.light_intensity, g.depth, g.refraction), (30.0, 0.5, 12.0, Glass::default().refraction));
for e in [Effect::noise(), Effect::texture(), Effect::glass()] {
let back: Effect = serde_json::from_str(&serde_json::to_string(&e).unwrap()).unwrap();
assert_eq!(back, e);
}
}
#[test]
fn glass_values_are_clamped_and_reach_their_backdrop() {
let g = Glass { refraction: 7.0, depth: -3.0, dispersion: f64::NAN, frost: f64::INFINITY, splay: -1.0, ..Glass::default() }.sanitized();
assert_eq!((g.refraction, g.depth, g.dispersion, g.splay), (1.0, 1.0, Glass::default().dispersion, 0.0));
assert!(g.frost.is_finite());
let e = Effect::glass();
let EffectKind::Glass(g) = &e.kind else { panic!("{e:?}") };
assert!(e.backdrop_radius() >= g.frost + g.max_bend() / 1.5 - 1e-9);
assert_eq!(Effect::background_blur().backdrop_radius(), 4.0);
assert_eq!(Effect::drop_shadow().backdrop_radius(), 0.0);
let hidden = Effect { visible: false, ..Effect::glass() };
assert_eq!(hidden.backdrop_radius(), 0.0);
}
#[test]
fn effect_colours() {
let mut e = Effect::noise();
assert!(e.color(true).is_some());
*e.color_mut(true).unwrap() = color::Color::WHITE;
let EffectKind::Noise(n) = &e.kind else { panic!("{e:?}") };
assert_eq!(n.secondary, color::Color::WHITE);
assert!(Effect::drop_shadow().color(true).is_none());
assert!(Effect::glass().color(false).is_none());
// The colour visitor (palette edits, colour profile conversion) sees both noise colours.
let mut seen = 0;
e.for_each_color_mut(&mut |_| seen += 1);
assert_eq!(seen, 2);
}
#[test]
fn spread_reaches_further_at_sharp_corners_only() {
let mut d = Document::new("r");
let p = d.first_page().unwrap();
let shadow =
Effect { kind: EffectKind::DropShadow(Shadow { radius: 0.0, offset: (0.0, 0.0), spread: 10.0, ..Shadow::default() }), visible: true };
let r = d.create(p, NodeKind::Rectangle, "R", 0.0, 0.0, 10.0, 10.0).unwrap();
let s = d.create(p, NodeKind::Star { count: 5, inner_ratio: 0.4 }, "S", 0.0, 0.0, 10.0, 10.0).unwrap();
for id in [r, s] {
d.get_mut(id).unwrap().effects = vec![shadow.clone()];
}
assert_eq!(d.node(r).unwrap().paint_reach(), 10.0);
assert_eq!(d.node(s).unwrap().paint_reach(), 10.0 * SPREAD_MITER_LIMIT);
}