75faf3efaa
Appearance: acrylic material with popup and modal backing, corner radius, UI font (bundled DM Sans, Figtree, Fredoka, Geist, Nunito, Quicksand, Varela Round), accent colour. Icons on buttons, code targets and agent logos; redesigned fill/gradient and auto-layout rows; compact topbar. Chat: agent logo in the header, history popup with search and recency groups, new-chat button, issue outcomes saved with the chat and issue bodies shown as fields, question cards with one-click answers, number keys, an inline other-answer field and an answered state. Layers: right-click context menu. Performance: virtualised layer reveal, pruned hit-testing, glyph cache. Settings > Performance: FPS, frame time, RAM, CPU and GPU overlay with a slow-frame warning. MCP: compact spec writes bound strokes as variables (#23), lint contrast averages gradient and translucent backgrounds (#24), lint flags light/dark theme copies (#25). Closes #23, closes #24, closes #25 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.1 KiB
Rust
75 lines
2.1 KiB
Rust
//! `box-shadow` falloff matches CSS (Gaussian, sigma = blur / 2) and is cut out under the box.
|
|
|
|
use papercraft::geom::{Rect, V2, v2};
|
|
use papercraft::model::{Color, Node, NodeId, NodeKind, Shadow};
|
|
use papercraft::render::draw::{DrawOp, PaintSpec, node_draw};
|
|
|
|
/// Composited shadow alpha at `q` from the node's fill ops (shadow color only).
|
|
fn alpha_at(ops: &[DrawOp], q: V2) -> f64 {
|
|
let clear: f64 = ops
|
|
.iter()
|
|
.filter_map(|op| match op {
|
|
DrawOp::Fill {
|
|
path,
|
|
paint: PaintSpec::Solid(c),
|
|
even_odd,
|
|
} if c[0] == 1.0 => path
|
|
.contains(q, *even_odd, 0.1)
|
|
.then_some(1.0 - c[3] as f64),
|
|
_ => None,
|
|
})
|
|
.product();
|
|
1.0 - clear
|
|
}
|
|
|
|
fn glow(x: f64, y: f64) -> Vec<DrawOp> {
|
|
let mut n = Node::new(NodeId(1), NodeKind::Rect, Rect::new(0.0, 0.0, 200.0, 100.0));
|
|
n.style.corner_radius = 16.0;
|
|
// White glow, so the box's own (absent) fill can't be mistaken for shadow.
|
|
n.style.shadow = Some(Shadow {
|
|
x,
|
|
y,
|
|
blur: 40.0,
|
|
spread: 0.0,
|
|
color: Color::rgba(255, 255, 255, 128),
|
|
});
|
|
node_draw(&n, n.rect, 1.0).ops
|
|
}
|
|
|
|
#[test]
|
|
fn centered_glow_follows_css_falloff_and_skips_the_box() {
|
|
let ops = glow(0.0, 0.0);
|
|
let a = |d: f64| alpha_at(&ops, v2(100.0, 100.0 + d));
|
|
let full = 128.0 / 255.0;
|
|
assert!(
|
|
(a(0.5) - full * 0.5).abs() < 0.06,
|
|
"half the alpha at the edge: {}",
|
|
a(0.5)
|
|
);
|
|
assert!(a(20.0) < full * 0.2, "one sigma out is faint: {}", a(20.0));
|
|
assert!(
|
|
a(10.0) > a(20.0) && a(20.0) > a(30.0),
|
|
"fades monotonically"
|
|
);
|
|
assert!(a(42.0) < 0.01, "gone past the blur radius: {}", a(42.0));
|
|
assert_eq!(
|
|
alpha_at(&ops, v2(100.0, 50.0)),
|
|
0.0,
|
|
"nothing under the box"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn offset_shadow_is_cut_out_under_the_box() {
|
|
let ops = glow(0.0, 24.0);
|
|
assert_eq!(
|
|
alpha_at(&ops, v2(100.0, 90.0)),
|
|
0.0,
|
|
"nothing under the box"
|
|
);
|
|
assert!(
|
|
alpha_at(&ops, v2(100.0, 110.0)) > 0.2,
|
|
"shadow below the box"
|
|
);
|
|
}
|