git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitd133d9f52995c7f87ab8eab584e8ddf85e2972f3
parent1c73f7cdfa
authorLucas Galante <[email protected]>
date2026-08-30 10:08
feat(handles): pinch the band at the seams

The band's thickness minimum sat at the corner apex — a leftover from
before the pads, and invisible now that a pad covers the apex. The visible
consequence was a band that only thinned in one direction: away from each
gap toward the corner, while the gap itself cut through a partly-swollen
band.

The minimum now lands exactly on the seams. The profile swells away from
each gap in both directions — an edge bar to full band at its side's
midpoint, a corner arm back toward the apex, where the pad's smooth union
takes over — so every join is a waist between two thickening pieces, and
the pad reads as continuous with its two arms rather than sitting on a
thin ribbon. The corner run (cl, per-side clamped) is now shared by the
profile and the zone cut, which is what pins the minimum to the seam.

Measured along the top side: 12px at both seams (37%/63%), swelling to
27px into the corner arm and 31px at the midpoint.

Co-Authored-By: Claude Fable 5 <[email protected]>

 CLAUDE.md                                     | 14 +++++----
 scenefx/render/fx_renderer/shaders/frame.frag | 43 +++++++++++++++++----------
 2 files changed, 36 insertions(+), 21 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index b2e001a..10bc5b6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -322,12 +322,14 @@ where the old band sat outside them.
   to be spent on moving the way the outside band's did.
 - The ring is drawn by **one scenefx node**, `wlr_scene_frame`
   (`scenefx/render/fx_renderer/shaders/frame.frag`), not by rects. Its
-  two elements make the frame. The BAND runs thin at the corners and swells
-  to `band` at each side's midpoint; a round PAD (`bulge`, screen px, 0
-  disables) sits on each corner — a disc centred on the corner arc's centre,
-  smooth-unioned onto the ring so its inner boundary bows inward with a
-  fillet where it meets the moulding. The band deliberately thins toward the
-  corner so the pad reads as a bead on a slender moulding, not as more band.
+  two elements make the frame. The BAND is thinnest at the SEAMS — the gaps
+  between corner and edge pieces — and swells away from them both ways: an
+  edge bar to `band` at its side's midpoint, a corner arm back toward the
+  apex, where it flows into the round PAD (`bulge`, screen px, 0 disables) —
+  a disc centred on the corner arc's centre, smooth-unioned onto the ring so
+  its inner boundary bows inward with a fillet where it meets the moulding.
+  Each gap therefore separates two thin tips: the frame pinches at every
+  join and thickens toward every piece's middle.
   The gap notches sever only the band; a groove across a pad would read as
   damage. Both `get_border_zone` and the drawing treat a pad as its corner's
   zone — its tip reaches past the band, so the hit test carries a matching
diff --git a/scenefx/render/fx_renderer/shaders/frame.frag b/scenefx/render/fx_renderer/shaders/frame.frag
index 40edfdb..94f70e5 100644
--- a/scenefx/render/fx_renderer/shaders/frame.frag
+++ b/scenefx/render/fx_renderer/shaders/frame.frag
@@ -2,12 +2,14 @@
 // swells from the corners toward the middle of each side, like the moulding
 // of a picture frame.
 //
-// Two elements make the frame. The BAND runs `band_min` at the corners,
-// easing to `band` at each side's midpoint — the moulding. And a round BULGE
-// sits on each corner: a disc smooth-unioned onto the ring, so the inner
-// boundary bows inward there like a bead. The band deliberately thins toward
-// the corner so the bulge reads as its own rounded pad on a slender moulding,
-// not as more band. `corner_len` places the gaps and the corner zones.
+// Two elements make the frame. The BAND is thinnest at the SEAMS — the gaps
+// between corner and edge pieces — and swells away from them in both
+// directions: an edge bar rises to `band` at its side's midpoint, a corner
+// arm rises back toward the apex, where it flows into the round BULGE that
+// sits on each corner (a disc smooth-unioned onto the ring, its inner
+// boundary bowing inward like a bead). So each gap separates two thin tips,
+// and every piece thickens toward its own middle. `corner_len` places the
+// seams and the corner zones.
 //
 // Why a shader rather than rects: the swell is continuous along a side, and
 // a scene rect can only approximate it with a clipped region whose corner
@@ -97,11 +99,24 @@ void main() {
     // Distance from the nearer end of that side.
     float u = min(along, len - along);
 
-    // The band: thin at the corners, swelling to the side's midpoint, with
-    // swell_curve pulling the gain early (below 1) or late (above 1).
+    // The corner run, clamped against ITS OWN side (see the zone comment
+    // below); the band profile and the zone cut share it so the thickness
+    // minimum lands exactly on the seam.
+    float cl = min(corner_len, 0.45 * len);
+
+    // The band: band_min at the seams, swelling away from them both ways —
+    // an edge bar to `band` at the side's midpoint, a corner arm back toward
+    // the apex, where the pad's union takes over. v is 0 at a seam and 1 at
+    // a piece's peak; swell_curve pulls the gain early (below 1) or late.
     float half_side = max(0.5 * len, 1e-3);
-    float t = clamp(u / half_side, 0.0, 1.0);
-    float s = pow(smoothstep(0.0, 1.0, t), max(swell_curve, 0.01));
+    float v;
+    if (u <= cl) {
+        v = 1.0 - u / max(cl, 1e-3);
+    } else {
+        float run = max(half_side - cl - gap, 1e-3);
+        v = clamp((u - cl - gap) / run, 0.0, 1.0);
+    }
+    float s = pow(smoothstep(0.0, 1.0, v), max(swell_curve, 0.01));
     float thickness = mix(band_min, band, s);
     float f_ring = thickness - depth;
 
@@ -123,11 +138,9 @@ void main() {
     // Zones: anything on the pad is its corner's, then the band cut as
     // before. The gap only severs the BAND — a groove across the pad would
     // read as damage, so it skips fragments the disc owns.
-    // The corner run clamps against ITS OWN side: two corner zones on one
-    // side must never meet, or the side's midpoint would resize diagonally —
-    // and clamping per side rather than to the window's short side lets a
-    // long side carry the full configured run while a short one shortens.
-    float cl = min(corner_len, 0.45 * len);
+    // The corner run clamps per side because two corner zones on one side
+    // must never meet — past that, the side's midpoint would resize
+    // diagonally.
     float zone;
     if (u <= cl || f_disc > 0.0) {
         zone = top ? (left ? ZONE_TL : ZONE_TR) : (left ? ZONE_BL : ZONE_BR);