mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-10 12:22:36 +00:00
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885) The backdrop bends most a 1-unit bevel in from the edge, falls off as the 3.5th power of the way out of the curve to nothing `depth` further in, and moves a fifth as much on the bevel itself (it fell off as the square of the way in from the very edge). Dispersion samples each channel at 8 steps along the bend with a triangular window, red moving furthest and blue least (it was three samples, blue furthest), as far apart as the reference tool's: at 100% red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`). `Glass::max_bend` bounds red's furthest step, so the backdrop read around a pane covers every sample, and the field margin covers the bevel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885) Review follow-up: the bend reaches `depth` × `refraction` a bevel in from the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now checks for several refractions. A single white pixel on black pins the dispersion's triangular window, which a ramp and a flat backdrop can't tell from one centred sample. The parity assessment records the bend, the dispersion and how they were measured against the reference tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1885) The light is a line on the 1-unit bevel added to the backdrop, as the reference tool's: fully where the edge faces the light, none below 0.35 of facing it and 0.6 as much on the opposite side, with a faint gradient across the curve, 5% darker facing the light and lighter opposite. It was a glow mixed towards white, as deep as `splay` made it, so `light_reach` goes. `splay` turns the bend from along the edge's normal towards the way away from the layer's centre, and no longer widens the light: files with a splay above 0 look different. The direction to the centre is worked out in the shape's own frame (`own_frame`, shared with the field cache), so cut renders match the whole one byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: cut renders of glass match with splay turning a dispersed bend (#1885) Review follow-up: the cut-versus-whole test had splay only on looks without refraction, so no turned bend reached the dispersion. A look with splay 1 and the default bend and dispersion now renders the same however it is cut. The parity assessment records the light on the bevel and splay's new meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885) Review follow-up: `bend_at` returns 0 straight away past `depth` + the bevel, where the curve and the bevel's easing are both 0, instead of working out a power of 0 for every pixel of a pane's flat inside. A test moves one white pixel along a row under every bend of the profile at full dispersion and checks that no channel ever reads it further out than `Glass::max_bend`, which sizes the backdrop read around a pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885) Review follow-up: the light was added to the backdrop's colour and then clamped to the backdrop's alpha, so with nothing under the glass (an export of the pane alone) the line on the bevel vanished, where the old glow raised the alpha and showed. Now the alpha rises as far as the lit colour needs (`lit`), and stays the backdrop's wherever the light fits under it, so renders over an opaque backdrop don't change. Past `depth` + the bevel the rim light, the gradient and the bend are all 0, so those pixels take one sample of the backdrop and skip the light's direction, facing and splay work, as the old glow's `reach` early-out did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
354 lines
16 KiB
Rust
354 lines
16 KiB
Rust
//! Backdrop effects (background blur, glass) see the whole backdrop under the layer: everything
|
||
//! composited below it, ancestors' own fills included, however the layer's ancestors isolate it
|
||
//! (clipping frames, rounded clips, group opacity, masks). The expected composite is built here
|
||
//! from first principles: the scene rendered without the layer, blurred, through the shape.
|
||
|
||
use figcraft_color::{Color, Paint};
|
||
use figcraft_doc::{Document, Effect, EffectKind, Glass, NodeId, NodeKind};
|
||
use tiny_skia::{PixmapPaint, Transform};
|
||
|
||
use super::*;
|
||
|
||
const BLUR: f64 = 12.0;
|
||
|
||
fn solid(d: &mut Document, id: NodeId, hex: &str) {
|
||
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::from_hex(hex).unwrap())];
|
||
}
|
||
|
||
/// A 240 × 160 red frame (clipping) with blue and green stripes inside and, on top, a transparent
|
||
/// pane with a background blur at (60, 40) 120 × 80 under `holder` (what isolates it). Returns the
|
||
/// document, the frame and the pane.
|
||
fn scene(holder: Holder) -> (Document, NodeId, NodeId) {
|
||
let mut d = Document::new("backdrop");
|
||
let p = d.first_page().unwrap();
|
||
let f = d.create(p, NodeKind::Frame, "Frame", 0.0, 0.0, 240.0, 160.0).unwrap();
|
||
solid(&mut d, f, "E03030");
|
||
d.get_mut(f).unwrap().clip = true;
|
||
if holder == Holder::RoundedClip {
|
||
d.get_mut(f).unwrap().corners.radii = [24.0; 4];
|
||
}
|
||
let parent = match holder {
|
||
Holder::Group => {
|
||
let g = d.create(f, NodeKind::Group, "Group", 0.0, 0.0, 240.0, 160.0).unwrap();
|
||
d.get_mut(g).unwrap().opacity = 0.5;
|
||
g
|
||
}
|
||
_ => f,
|
||
};
|
||
for (i, x) in [30.0, 90.0, 150.0].into_iter().enumerate() {
|
||
let s = d.create(parent, NodeKind::Rectangle, "Stripe", x, 0.0, 30.0, 160.0).unwrap();
|
||
solid(&mut d, s, if i % 2 == 0 { "2040E0" } else { "20C060" });
|
||
}
|
||
if holder == Holder::Mask {
|
||
// A mask covering the frame: everything above it shows through its alpha.
|
||
let m = d.create(f, NodeKind::Rectangle, "Mask", 0.0, 0.0, 240.0, 160.0).unwrap();
|
||
solid(&mut d, m, "000000");
|
||
d.get_mut(m).unwrap().is_mask = true;
|
||
let c = d.get_mut(f).unwrap();
|
||
let mi = c.children.iter().position(|k| *k == m).unwrap();
|
||
let mid = c.children.remove(mi);
|
||
c.children.insert(0, mid);
|
||
}
|
||
let pane = d.create(parent, NodeKind::Rectangle, "Pane", 60.0, 40.0, 120.0, 80.0).unwrap();
|
||
let n = d.get_mut(pane).unwrap();
|
||
n.fills = vec![];
|
||
n.effects = vec![Effect { kind: EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(BLUR)), visible: true }];
|
||
(d, f, pane)
|
||
}
|
||
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
enum Holder {
|
||
ClipFrame,
|
||
RoundedClip,
|
||
Group,
|
||
Mask,
|
||
}
|
||
|
||
fn px(p: &Pixmap, x: u32, y: u32) -> [u8; 4] {
|
||
let i = ((y * p.width() + x) * 4) as usize;
|
||
[p.data()[i], p.data()[i + 1], p.data()[i + 2], p.data()[i + 3]]
|
||
}
|
||
|
||
/// The largest channel difference between `a` and `b` over the pixels well inside the pane
|
||
/// (60..180 × 40..120, 2 px in), at `scale`.
|
||
fn max_diff_in_pane(a: &Pixmap, b: &Pixmap, scale: f64) -> u8 {
|
||
let r = |v: f64| (v * scale) as u32;
|
||
let mut worst = 0;
|
||
for y in r(42.0)..r(118.0) {
|
||
for x in r(62.0)..r(178.0) {
|
||
let (p, q) = (px(a, x, y), px(b, x, y));
|
||
worst = worst.max(p.iter().zip(q).map(|(c, d)| c.abs_diff(d)).max().unwrap());
|
||
}
|
||
}
|
||
worst
|
||
}
|
||
|
||
/// The largest channel difference between two same-sized pixmaps.
|
||
fn max_diff(a: &Pixmap, b: &Pixmap) -> u8 {
|
||
a.data().iter().zip(b.data()).map(|(p, q)| p.abs_diff(*q)).max().unwrap_or(0)
|
||
}
|
||
|
||
/// The scene's frame exported without the pane, blurred (sigma = radius / 2): what the pane
|
||
/// must show.
|
||
fn expected_blurred(d: &Document, f: NodeId, pane: NodeId, scale: f64) -> (Pixmap, Pixmap) {
|
||
let mut without = d.clone();
|
||
without.get_mut(pane).unwrap().visible = false;
|
||
let back = render_node(&without, f, scale).unwrap();
|
||
let mut blurred = back.clone();
|
||
fx::blur(&mut blurred, BLUR / 2.0 * scale);
|
||
(back, blurred)
|
||
}
|
||
|
||
#[test]
|
||
fn background_blur_in_a_clipping_frame_sees_the_frames_fill() {
|
||
for holder in [Holder::ClipFrame, Holder::RoundedClip, Holder::Mask] {
|
||
for scale in [1.0, 2.0] {
|
||
let (d, f, pane) = scene(holder);
|
||
let got = render_node(&d, f, scale).unwrap();
|
||
let (_, want) = expected_blurred(&d, f, pane, scale);
|
||
let diff = max_diff_in_pane(&got, &want, scale);
|
||
assert!(
|
||
diff <= 3,
|
||
"{} at {scale}×: differs by {diff}",
|
||
match holder {
|
||
Holder::ClipFrame => "clip frame",
|
||
Holder::RoundedClip => "rounded clip",
|
||
Holder::Mask => "mask",
|
||
Holder::Group => "group",
|
||
}
|
||
);
|
||
// The frame's red shows through the blur between the stripes (it used to be missing,
|
||
// the sharp stripes below showing instead).
|
||
let mid = px(&got, (75.0 * scale) as u32, (80.0 * scale) as u32);
|
||
assert!(mid[0] > 80 && mid[3] == 255, "{mid:?}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn background_blur_inside_a_faded_group_blends_with_the_group() {
|
||
let (d, f, pane) = scene(Holder::Group);
|
||
let got = render_node(&d, f, 1.0).unwrap();
|
||
let (_, blurred) = expected_blurred(&d, f, pane, 1.0);
|
||
// The pane (the blurred backdrop, opaque) lives in the group, which is half transparent over
|
||
// the frame: half the blurred backdrop over the frame alone.
|
||
let group = d.node(pane).unwrap().parent.unwrap();
|
||
let mut frame_only = d.clone();
|
||
frame_only.get_mut(group).unwrap().visible = false;
|
||
let mut want = render_node(&frame_only, f, 1.0).unwrap();
|
||
want.draw_pixmap(0, 0, blurred.as_ref(), &PixmapPaint { opacity: 0.5, ..Default::default() }, Transform::identity(), None);
|
||
let diff = max_diff_in_pane(&got, &want, 1.0);
|
||
assert!(diff <= 3, "differs by {diff}");
|
||
}
|
||
|
||
#[test]
|
||
fn background_blur_in_a_clipping_frame_inside_a_faded_group() {
|
||
// The clipping frame's children are drawn on a copy of the group's pixels: the pane's
|
||
// backdrop is that copy where the group puts it, at the group's 50% (half the frame), not the
|
||
// copy at full strength.
|
||
let (mut d, f, pane) = scene(Holder::ClipFrame);
|
||
let p = d.first_page().unwrap();
|
||
let g = d.create(p, NodeKind::Group, "Faded", 0.0, 0.0, 240.0, 160.0).unwrap();
|
||
d.get_mut(g).unwrap().opacity = 0.5;
|
||
d.reparent(f, g, None).unwrap();
|
||
let got = render_node(&d, g, 1.0).unwrap();
|
||
// The pane draws the blurred half-strength frame over the frame, and the group halves both.
|
||
let (frame, blurred) = expected_blurred(&d, f, pane, 1.0);
|
||
let mut want = Pixmap::new(frame.width(), frame.height()).unwrap();
|
||
want.draw_pixmap(0, 0, frame.as_ref(), &PixmapPaint { opacity: 0.25, ..Default::default() }, Transform::identity(), None);
|
||
let plus = PixmapPaint { opacity: 0.25, blend_mode: tiny_skia::BlendMode::Plus, ..Default::default() };
|
||
want.draw_pixmap(0, 0, blurred.as_ref(), &plus, Transform::identity(), None);
|
||
let diff = max_diff_in_pane(&got, &want, 1.0);
|
||
assert!(diff <= 3, "differs by {diff}");
|
||
}
|
||
|
||
#[test]
|
||
fn background_blur_on_the_canvas_matches_the_export() {
|
||
let (d, f, _) = scene(Holder::ClipFrame);
|
||
let p = d.first_page().unwrap();
|
||
let export = render_node(&d, f, 1.0).unwrap();
|
||
let canvas = render_view(&d, p, &View { origin: (0.0, 0.0), scale: 1.0, width: 240, height: 160 }, &Options::default()).unwrap();
|
||
assert!(max_diff_in_pane(&export, &canvas, 1.0) <= 1);
|
||
}
|
||
|
||
#[test]
|
||
fn background_blur_reads_past_its_own_box() {
|
||
// The pane's own border pixels blur what lies around them (the backdrop past the pane's box),
|
||
// not transparency: they match the blurred scene like the inside does.
|
||
let (d, f, pane) = scene(Holder::ClipFrame);
|
||
let got = render_node(&d, f, 1.0).unwrap();
|
||
let (_, want) = expected_blurred(&d, f, pane, 1.0);
|
||
for (x, y) in [(60, 80), (179, 80), (120, 40), (120, 119), (60, 40), (179, 119)] {
|
||
let (g, w) = (px(&got, x, y), px(&want, x, y));
|
||
assert!(g.iter().zip(w).all(|(a, b)| a.abs_diff(b) <= 3), "({x}, {y}): {g:?} vs {w:?}");
|
||
}
|
||
}
|
||
|
||
/// The frame with a glass pane at 80..200 × 40..120 instead of the blur: (document, frame, pane).
|
||
fn glass_scene(g: Glass) -> (Document, NodeId, NodeId) {
|
||
let (mut d, f, pane) = scene(Holder::ClipFrame);
|
||
let n = d.get_mut(pane).unwrap();
|
||
n.effects = vec![Effect { kind: EffectKind::Glass(g), visible: true }];
|
||
// 80..200 × 40..120: stripe edges (x = 90, 150, 180) run through its rims.
|
||
n.transform = Affine::translate((80.0, 40.0));
|
||
(d, f, pane)
|
||
}
|
||
|
||
#[test]
|
||
fn glass_centre_is_the_frosted_backdrop() {
|
||
let g = Glass { frost: BLUR, depth: 10.0, ..Glass::default() };
|
||
let (d, f, pane) = glass_scene(g);
|
||
let got = render_node(&d, f, 1.0).unwrap();
|
||
let (_, want) = expected_blurred(&d, f, pane, 1.0);
|
||
// Further than `depth` from the edge nothing bends and no light falls.
|
||
let mut worst = 0;
|
||
for y in 55..105 {
|
||
for x in 95..185 {
|
||
let (p, q) = (px(&got, x, y), px(&want, x, y));
|
||
worst = worst.max(p.iter().zip(q).map(|(c, d)| c.abs_diff(d)).max().unwrap());
|
||
}
|
||
}
|
||
assert!(worst <= 3, "{worst}");
|
||
}
|
||
|
||
#[test]
|
||
fn glass_edges_bend_split_and_catch_the_light() {
|
||
let plain = Glass { frost: 0.0, refraction: 0.0, dispersion: 0.0, light_intensity: 0.0, depth: 16.0, ..Glass::default() };
|
||
let (d, f, _) = glass_scene(plain.clone());
|
||
let flat = render_node(&d, f, 1.0).unwrap();
|
||
// Without bend, frost or light the glass shows the backdrop as is.
|
||
let (d0, f0, p0) = glass_scene(plain.clone());
|
||
let mut none = d0.clone();
|
||
none.get_mut(p0).unwrap().visible = false;
|
||
let backdrop = render_node(&none, f0, 1.0).unwrap();
|
||
assert!(max_diff_in_pane(&flat, &backdrop, 1.0) <= 2);
|
||
|
||
// Refraction moves what shows near the edge (stripe edges within `depth` of the pane's
|
||
// border get pulled inwards), not in the middle.
|
||
let (d, f, _) = glass_scene(Glass { refraction: 1.0, ..plain.clone() });
|
||
let bent = render_node(&d, f, 1.0).unwrap();
|
||
// Pixels of the pane that differ between two renders, near the border (within 15 px) and in
|
||
// the middle (more than 17 px in).
|
||
let changed = |a: &Pixmap, b: &Pixmap| {
|
||
let (mut rim, mut middle) = (0, 0);
|
||
for y in 40..120u32 {
|
||
for x in 80..200u32 {
|
||
let edge = (x - 80).min(199 - x).min(y - 40).min(119 - y);
|
||
if px(a, x, y) != px(b, x, y) {
|
||
if edge < 15 {
|
||
rim += 1;
|
||
} else if edge > 17 {
|
||
middle += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
(rim, middle)
|
||
};
|
||
let (rim, middle) = changed(&bent, &flat);
|
||
assert!(rim > 10 && middle == 0, "rim {rim}, middle {middle}");
|
||
|
||
// Dispersion splits the channels along the rim (a fringe on the stripe edges).
|
||
let (d, f, _) = glass_scene(Glass { refraction: 1.0, dispersion: 1.0, ..plain.clone() });
|
||
let split = render_node(&d, f, 1.0).unwrap();
|
||
let (rim, middle) = changed(&split, &bent);
|
||
assert!(rim > 0 && middle == 0, "dispersion: rim {rim}, middle {middle}");
|
||
|
||
// The light (from the top left by default) is a line on the 1-unit bevel: the top-left corner
|
||
// catches it all, the bottom-right one (facing away) 0.6 as much, the top side (45° from it)
|
||
// 0.574 and the bottom side 0.6 × 0.574, and the top-right corner (side on) none. Further in
|
||
// only the faint gradient across the curve shows, and nothing past `depth`.
|
||
let (d, f, _) = glass_scene(Glass { light_intensity: 1.0, ..plain });
|
||
let lit = render_node(&d, f, 1.0).unwrap();
|
||
let gain = |x: u32, y: u32| i32::from(px(&lit, x, y)[1]) - i32::from(px(&flat, x, y)[1]);
|
||
let (top_left, bottom_right, top, bottom, top_right) = (gain(80, 40), gain(199, 119), gain(140, 40), gain(140, 119), gain(199, 40));
|
||
assert!(
|
||
top_left > bottom_right && bottom_right > top && top > bottom && bottom > top_right.abs(),
|
||
"{top_left} {bottom_right} {top} {bottom} {top_right}"
|
||
);
|
||
assert!(top_left > 120 && top_right.abs() <= 2, "{top_left} {top_right}");
|
||
assert!(gain(140, 44).abs() <= 13, "4 px in: the gradient only, {}", gain(140, 44));
|
||
assert_eq!(gain(140, 80), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn splay_turns_the_bend_towards_the_centre() {
|
||
// Refraction only, so the bend alone moves the stripes' edges.
|
||
let bend = Glass { frost: 0.0, refraction: 1.0, dispersion: 0.0, light_intensity: 0.0, depth: 16.0, ..Glass::default() };
|
||
let (d, f, _) = glass_scene(bend.clone());
|
||
let edge = render_node(&d, f, 1.0).unwrap();
|
||
let (d, f, _) = glass_scene(Glass { splay: 1.0, ..bend });
|
||
let centre = render_node(&d, f, 1.0).unwrap();
|
||
let diff = |xs: std::ops::Range<u32>, ys: std::ops::Range<u32>| {
|
||
let mut most = 0;
|
||
for y in ys {
|
||
for x in xs.clone() {
|
||
most = most.max(px(&edge, x, y).iter().zip(px(¢re, x, y)).map(|(a, b)| a.abs_diff(b)).max().unwrap());
|
||
}
|
||
}
|
||
most
|
||
};
|
||
// Halfway along the left side the edge's normal and the way from the centre (140, 80) agree:
|
||
// nearly the same pixels. In the top-left corner they part, and so does what shows there.
|
||
assert!(diff(80..96, 78..82) <= 8, "mid-left: {}", diff(80..96, 78..82));
|
||
assert!(diff(80..96, 40..56) > 40, "corner: {}", diff(80..96, 40..56));
|
||
}
|
||
|
||
/// Glass with nothing under it (an export of the pane alone): the light on the bevel still shows,
|
||
/// the alpha rising with it, and the flat inside and the outside stay clear.
|
||
#[test]
|
||
fn glass_over_nothing_still_catches_the_light() {
|
||
let mut d = Document::new("glass");
|
||
let p = d.first_page().unwrap();
|
||
let pane = d.create(p, NodeKind::Rectangle, "Glass", 40.0, 30.0, 120.0, 80.0).unwrap();
|
||
let n = d.get_mut(pane).unwrap();
|
||
n.fills.clear();
|
||
n.effects = vec![Effect { kind: EffectKind::Glass(Glass { light_intensity: 1.0, ..Glass::default() }), visible: true }];
|
||
let out = render_area(&d, &[pane], Rect::new(0.0, 0.0, 200.0, 140.0), 1.0).unwrap();
|
||
// The top-left corner faces the light (from the top left): white, as opaque as it is bright.
|
||
let corner = px(&out, 40, 30);
|
||
assert!(corner[3] > 100 && corner.iter().all(|v| v.abs_diff(corner[3]) <= 2), "{corner:?}");
|
||
assert!(out.data().chunks_exact(4).all(|q| q[0] <= q[3] && q[1] <= q[3] && q[2] <= q[3]), "premultiplied");
|
||
assert_eq!(px(&out, 100, 70), [0; 4]);
|
||
assert_eq!(px(&out, 10, 10), [0; 4]);
|
||
}
|
||
|
||
#[test]
|
||
fn glass_and_backdrop_blur_survive_bands_and_partial_renders() {
|
||
let (d, _, _) = glass_scene(Glass::default());
|
||
let p = d.first_page().unwrap();
|
||
let v = View { origin: (-20.0, -20.0), scale: 1.0, width: 280, height: 400 };
|
||
let one = render_view_single(&d, p, &v, &Options::default()).unwrap();
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
{
|
||
let banded = render_bands(&d, p, &v, &Options::default(), 4).unwrap();
|
||
assert!(max_diff(&one, &banded) <= 2);
|
||
}
|
||
// A pan: the retained render patches the uncovered strip and still matches a full render.
|
||
let doc = std::sync::Arc::new(d);
|
||
let mut r = Retained::default();
|
||
r.render(&doc, p, &v, &Options::default()).unwrap();
|
||
let moved = View { origin: (-12.0, -20.0), ..v.clone() };
|
||
let (snapped, partial, _) = r.render(&doc, p, &moved, &Options::default()).unwrap();
|
||
let partial = partial.clone();
|
||
let full = render_view_single(&doc, p, &snapped, &Options::default()).unwrap();
|
||
assert!(max_diff(&partial, &full) <= 2);
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_backdrop_values_do_not_crash() {
|
||
for g in [
|
||
Glass { depth: f64::NAN, frost: f64::INFINITY, refraction: -3.0, dispersion: 9.0, light_angle: f64::NAN, ..Glass::default() },
|
||
Glass { depth: 1e12, frost: 1e9, ..Glass::default() },
|
||
Glass { depth: 0.0, frost: 0.0, refraction: 0.0, light_intensity: 0.0, ..Glass::default() },
|
||
] {
|
||
let (d, f, _) = glass_scene(g);
|
||
assert!(render_node(&d, f, 1.0).is_some());
|
||
}
|
||
let (mut d, f, pane) = scene(Holder::ClipFrame);
|
||
d.get_mut(pane).unwrap().effects = vec![Effect { kind: EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(f64::NAN)), visible: true }];
|
||
assert!(render_node(&d, f, 1.0).is_some());
|
||
d.get_mut(pane).unwrap().effects = vec![Effect { kind: EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(1e12)), visible: true }];
|
||
assert!(render_node(&d, f, 0.5).is_some());
|
||
}
|