GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
paint: Prim::Glow — a per-vertex-alpha feathered aura
A rounded rect filling at the color's alpha, fading to zero across
`reach` px outside the boundary. Tessellated as concentric outline rings
whose alphas ride the vertices on a quadratic ease-out, so the GPU
interpolates a per-pixel-smooth vignette — stacked translucent layers
band visibly; interpolation cannot. Circular corner arcs (at a soft edge
the superellipse difference is invisible) and one shared sampler keep
every ring seam-free. No relief shading, no shader mode, no banded
fallback needed: pure vertex geometry works identically on both paths.
First caller: the designer's drop-target glow.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 93 +++++++++++++++++++++++++++++++++++++++++++-
src/scene/paint.rs | 17 ++++++++
2 files changed, 109 insertions(+), 1 deletion(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 7dbbda7..085dc85 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -660,6 +660,93 @@ fn superellipse_pt(theta: f32, e: f32) -> (f32, f32) {
(c.signum() * c.abs().powf(e), s.signum() * s.abs().powf(e))
}
+/// Feathered glow ([`Prim::Glow`]): the rounded rect's interior fills at the
+/// color's alpha and concentric outline rings fade it to zero across `reach`
+/// px outside the boundary. Alpha rides the VERTICES, so the GPU interpolates
+/// a per-pixel-smooth falloff between rings — stacked translucent layers band
+/// visibly; this cannot. Ring alphas sit on a quadratic ease-out, giving the
+/// vignette profile piecewise-linearly with kinks below visibility at glow
+/// alphas. Circular corner arcs (not the DE superellipse): at a soft edge the
+/// difference is invisible, and one sampler keeps every ring seam-free.
+pub fn push_glow_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ radius: f32, reach: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ if ww <= 0.0 || h <= 0.0 || color[3].abs() <= 0.0005 || sw <= 0.0 || sh <= 0.0 {
+ return;
+ }
+ let r0 = radius.clamp(0.0, ww.min(h) * 0.5);
+ let ctl = (x + r0, y + r0);
+ let ctr = (x + ww - r0, y + r0);
+ let cbr = (x + ww - r0, y + h - r0);
+ let cbl = (x + r0, y + h - r0);
+ const K: usize = 10;
+ use std::f32::consts::PI;
+ // One outline ring `off` px outside the boundary, clockwise from the
+ // top-left arc; every ring shares the layout, so strips never twist.
+ let ring = |off: f32| -> Vec<[f32; 2]> {
+ let r = (r0 + off).max(0.0);
+ let mut pts = Vec::with_capacity(4 * (K + 1));
+ let corners = [
+ (ctl, PI, 1.5 * PI),
+ (ctr, 1.5 * PI, 2.0 * PI),
+ (cbr, 0.0, 0.5 * PI),
+ (cbl, 0.5 * PI, PI),
+ ];
+ for ((cx, cy), a0, a1) in corners {
+ for k in 0..=K {
+ let a = a0 + (a1 - a0) * (k as f32 / K as f32);
+ pts.push([cx + r * a.cos(), cy + r * a.sin()]);
+ }
+ }
+ pts
+ };
+ let to_v = |p: [f32; 2], a: f32| Vertex {
+ position: [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0],
+ color: [color[0], color[1], color[2], a],
+ clip_circle,
+ };
+
+ let rings: Vec<(Vec<[f32; 2]>, f32)> = [0.0f32, 0.35, 0.7, 1.0]
+ .iter()
+ .map(|&t| (ring(reach * t), color[3] * (1.0 - t) * (1.0 - t)))
+ .collect();
+ let n = rings[0].0.len();
+
+ // Interior: a fan from the rect center over the innermost ring (a rounded
+ // rect is convex, so the fan covers it exactly), uniform core alpha.
+ let center = [x + ww * 0.5, y + h * 0.5];
+ for i in 0..n {
+ let p1 = rings[0].0[i];
+ let p2 = rings[0].0[(i + 1) % n];
+ out.push(to_v(center, color[3]));
+ out.push(to_v(p1, color[3]));
+ out.push(to_v(p2, color[3]));
+ }
+ // The feather: strips between consecutive rings, each vertex carrying its
+ // ring's alpha.
+ for w in rings.windows(2) {
+ let (inner, ia) = (&w[0].0, w[0].1);
+ let (outer, oa) = (&w[1].0, w[1].1);
+ for i in 0..n {
+ let a1 = inner[i];
+ let a2 = inner[(i + 1) % n];
+ let b1 = outer[i];
+ let b2 = outer[(i + 1) % n];
+ out.push(to_v(a1, ia));
+ out.push(to_v(b1, oa));
+ out.push(to_v(a2, ia));
+ out.push(to_v(a2, ia));
+ out.push(to_v(b1, oa));
+ out.push(to_v(b2, oa));
+ }
+ }
+}
+
pub fn push_rounded_rect_vertices_corners(
x: f32, y: f32, ww: f32, h: f32,
radii: crate::widget::CornerRadii,
@@ -1481,7 +1568,8 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
P::ConcaveFillet { .. } => "ConcaveFillet",
- P::Groove { .. } => "Groove", P::Text { .. } => "Text", P::Image { .. } => "Image",
+ P::Groove { .. } => "Groove", P::Glow { .. } => "Glow",
+ P::Text { .. } => "Text", P::Image { .. } => "Image",
}
}
@@ -1593,6 +1681,9 @@ pub fn tessellate_display_list(
push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
}
+ Prim::Glow { rect, radius, reach, color } => {
+ push_glow_vertices(rect.x, rect.y, rect.width, rect.height, *radius, *reach, sw, sh, *color, no, &mut verts);
+ }
Prim::Bevel { rect, radii, color, depth, tint } if shader_plates => {
// SDF-lit raised plate: one cover quad; the shader owns fill,
// roll shading, corners, and silhouette AA. Nominal corner
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 7247df7..4c2ad01 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -416,6 +416,14 @@ pub enum Prim {
ArcShaded { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, inner: [f32; 4], crest: [f32; 4], outer: [f32; 4] },
Vector { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap },
Circle { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
+ /// A feathered aura around (and over) a rounded rect: the interior fills
+ /// at the color's full alpha, and outside the boundary the alpha falls
+ /// off smoothly to zero across `reach` px. Tessellated as concentric
+ /// per-vertex-alpha rings the GPU interpolates, so the gradient is
+ /// per-pixel smooth — no stacked-layer banding. Highlights and soft
+ /// focus auras (the designer's drop-target glow) are the intended use;
+ /// no relief shading, no light involvement.
+ Glow { rect: Rect, radius: f32, reach: f32, color: [f32; 4] },
/// A `Circle` lit as a ball: the disc is shaded per pixel as a hemisphere
/// under the DE's plate light (same ambient/diffuse/specular model), so it
/// reads as a sphere sitting on the surface — the slider thumb's look. The
@@ -735,6 +743,14 @@ impl PaintCtx {
self.push(Prim::RoundedRect { rect, radius, corners, color });
}
+ /// Feathered aura over a rounded rect (see [`Prim::Glow`]): interior at
+ /// the color's alpha, smooth per-pixel falloff to zero across `reach` px
+ /// outside the boundary.
+ pub fn glow(&mut self, rect: Rect, radius: f32, reach: f32, color: [f32; 4]) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Glow { rect, radius, reach, color });
+ }
+
pub fn vector(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap) {
let (ox, oy) = self.offset;
self.push(Prim::Vector { x1: x1 + ox, y1: y1 + oy, x2: x2 + ox, y2: y2 + oy, thickness, color, cap });
@@ -814,6 +830,7 @@ impl PaintCtx {
}
Prim::Circle { cx, cy, radius, color } => self.circle(cx, cy, radius, color),
Prim::Sphere { cx, cy, radius, color } => self.sphere(cx, cy, radius, color),
+ Prim::Glow { rect, radius, reach, color } => self.glow(rect, radius, reach, color),
Prim::Droplet { rect, color, spec } => self.droplet(rect, color, spec),
Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
self.concave_fillet(cx, cy, radius, depth, start, raised)