git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit405284b6bed2dac394d20de888eacb993bbab15b
parentf105d5c090
authorLucas Galante <[email protected]>
date2026-08-24 14:34
droplet: continuous bottom arc (DropletSpec::bow)

The drop's bottom boundary becomes one circular arc — smooth-INTERSECT
(smax, the smin blend sign-flipped) with a disc whose lowest point
touches the drop's bottom center. The radius derives per drop from a
fixed edge rise (R = hx²/2·rise), so a wide drop flattens toward its
middle on its own while a narrow one curves visibly. The rise rides
p_spec_tint.w — the one free slot left in the push block; 0 keeps the
flat bottom run. Default bow 0.12.

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

 src/backend/window_runner.rs |  5 ++++-
 src/scene/paint.rs           |  8 ++++++++
 src/vk/shader2d.wgsl         | 19 +++++++++++++++++++
 3 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 2e1a160..1bca0dc 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2007,6 +2007,9 @@ pub fn tessellate_display_list(
                     ar *= f;
                 }
                 let band = (spec.band.max(0.05) * rect.height).max(1.0);
+                // Bottom-bow edge rise; the shader derives the arc radius
+                // from it per drop (R = hx²/2·rise).
+                let bow = (spec.bow.clamp(0.0, 0.5) * rect.height).min(hy * 0.9);
                 plate = Some(crate::vk::PlatePush {
                     rect: [
                         (rect.x + rect.width * 0.5) * scale,
@@ -2021,7 +2024,7 @@ pub fn tessellate_display_list(
                     // DE material's (a drop is wetter than the DE's plates).
                     material: [plate_mat[0], spec.gleam, spec.shine, spec.rim],
                     host: [sr * scale, spec.clarity.clamp(0.0, 1.0), spec.dome, ar * scale],
-                    specular_tint: [1.0, 1.0, 1.0, 0.0],
+                    specular_tint: [1.0, 1.0, 1.0, bow * scale],
                     mode: 10.0,
                     shape: 2.0,
                 });
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index ebf3a75..18db1c4 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -98,6 +98,13 @@ pub struct DropletSpec {
     pub shine: f32,
     /// Fresnel rim crest amplitude (the glass-edge brightening).
     pub rim: f32,
+    /// Bottom bow: the drop's bottom boundary becomes ONE continuous circular
+    /// arc — lowest at center, rising by `bow` (fraction of height) at the
+    /// drop's side extents. The arc's radius is derived per drop from that
+    /// fixed edge rise, so a wide drop gets a huge radius and the curvature
+    /// stays subtle at the middle while a narrow drop curves visibly. 0
+    /// disables it (flat bottom run between the corner arcs).
+    pub bow: f32,
 }
 
 impl Default for DropletSpec {
@@ -118,6 +125,7 @@ impl Default for DropletSpec {
             gleam: 1.4,
             shine: 32.0,
             rim: 0.5,
+            bow: 0.12,
         }
     }
 }
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 2214a13..7da9948 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -354,6 +354,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     //   p_radii = [sag, belly radius, belly half-width, blend k] px;
     //   p_host  = [sheet bottom-corner radius px, edge clarity 0-1, dome
     //              amplitude, attach (top-corner) radius px];
+    //   p_spec_tint.w = bottom-bow edge rise px (0 = flat bottom run);
     //   p_mat.w = fresnel rim crest amplitude (droplets carve nothing, so the
     //             AO slot is free); p_light.w = shaded band width px.
     // The silhouette is the smooth union of a film SHEET attached to the top
@@ -395,6 +396,24 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
             d = mix(gb.z, ga.z, hm) - k * hm * (1.0 - hm);
             g = mix(gb.xy, ga.xy, hm);
         }
+        // Bottom bow (p_spec_tint.w = edge rise, px): smooth-INTERSECT the
+        // drop with a disc whose lowest point touches the drop's bottom
+        // center — the bottom becomes one continuous circular arc, rising by
+        // the given amount at x = ±hx. The radius follows from that fixed
+        // rise (R = hx²/2·rise), so wide drops flatten toward the middle on
+        // their own. smax = -smin(-a,-b): same polynomial blend, sign flipped.
+        let bow = rrect_clip.p_spec_tint.w;
+        if (bow > 0.25) {
+            let bigr = hx * hx / (2.0 * bow);
+            let cc = vec2f(c.x, c.y + hy - bigr);
+            let pc = frag - cc;
+            let dl = max(length(pc), 1e-3);
+            let dc = dl - bigr;
+            let gc = pc / dl;
+            let hm2 = clamp(0.5 + 0.5 * (d - dc) / k, 0.0, 1.0);
+            d = mix(dc, d, hm2) + k * hm2 * (1.0 - hm2);
+            g = mix(gc, g, hm2);
+        }
         g = normalize(g);
 
         let din = -d;