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

commit13f4cf1cb473029b9681812365ff10c96887f3ce
parenta5a2d6ee83
authorLucas Galante <[email protected]>
date2026-08-24 13:14
scene: Prim::Droplet — a hanging water-drop material (shader mode 10)

A droplet clinging to the top edge of its rect: the silhouette is a
smooth union (polynomial smin) of a film sheet attached to the attach
line and a belly capsule on the rect bottom, so a waist forms like
surface tension pulls one. Shaded per pixel in the plate vocabulary —
roll_slope dome tilt, ambient/diffuse, decoupled specular — plus two
water terms: a tunable fresnel rim crest and a thin-edge clarity
falloff on the tint alpha, so the frosted backdrop reads clearer at
the rim. Shape/material knobs ride DropletSpec as height fractions;
the tessellator resolves and clamps them per rect. Mode 10 reuses the
existing push-constant fields (radii/host/material.w) — the block
cannot grow. Legacy banded path degrades to the flat hanging capsule
rather than vanishing.

The paint.rs half (DropletSpec, the variant, PaintCtx::droplet, the
replay arm) landed inside a5a2d6e when the relief session completed
the then-dangling variant to unbreak the tree; this adds the shader
branch, the tessellation arms, and the PlatePush mode doc.

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

 src/backend/window_runner.rs | 51 +++++++++++++++++++++++++++++++--
 src/vk/renderer.rs           |  5 +++-
 src/vk/shader2d.wgsl         | 67 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 120 insertions(+), 3 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 5171c4b..39f51e5 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1478,7 +1478,8 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
         P::Ridge { .. } => "Ridge", P::Trough { .. } => "Trough", P::Plate { .. } => "Plate",
         P::Arc { .. } => "Arc", P::ArcShaded { .. } => "ArcShaded",
         P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
-        P::Sphere { .. } => "Sphere", P::ConcaveFillet { .. } => "ConcaveFillet",
+        P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
+        P::ConcaveFillet { .. } => "ConcaveFillet",
         P::Groove { .. } => "Groove", P::Text { .. } => "Text", P::Image { .. } => "Image",
     }
 }
@@ -1548,7 +1549,8 @@ pub fn tessellate_display_list(
         let blur_behind = matches!(
             &item.prim,
             crate::scene::paint::Prim::Bevel { color, .. }
-            | crate::scene::paint::Prim::Plate { color, .. } if color[3] < 0.0
+            | crate::scene::paint::Prim::Plate { color, .. }
+            | crate::scene::paint::Prim::Droplet { color, .. } if color[3] < 0.0
         );
         // Logical [cx, cy, r] → the physical-pixel triple the vertex attribute carries.
         let no = item
@@ -1972,6 +1974,51 @@ pub fn tessellate_display_list(
                 // Legacy path: the flat disc, exactly a Circle.
                 verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
             }
+            Prim::Droplet { rect, color, spec } if shader_plates => {
+                // A water droplet lit by shader mode 10: one cover quad; the
+                // shader owns silhouette (sheet ∪smin belly), dome shading,
+                // fresnel rim and thin-edge clarity. The spec's height
+                // fractions resolve against the concrete rect here, clamped so
+                // small or narrow boxes stay well-formed (a belly wider than
+                // the box would turn the SDF interior inside out).
+                verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+                let hx = rect.width * 0.5;
+                let hy = rect.height * 0.5;
+                let sag = spec.sag.clamp(0.0, 0.9) * rect.height;
+                let br = (spec.belly.clamp(0.05, 1.0) * rect.height).min(hy).min(hx);
+                let bw = ((hx - br).max(0.0) * spec.belly_w.clamp(0.0, 1.0)).max(1.0);
+                let k = (spec.blend.max(0.0) * rect.height).max(1.0);
+                let sheet_hy = hy - sag * 0.5;
+                let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(sheet_hy.max(0.0)).min(hx);
+                let band = (spec.band.max(0.05) * rect.height).max(1.0);
+                plate = Some(crate::vk::PlatePush {
+                    rect: [
+                        (rect.x + rect.width * 0.5) * scale,
+                        (rect.y + rect.height * 0.5) * scale,
+                        hx * scale,
+                        hy * scale,
+                    ],
+                    radii: [sag * scale, br * scale, bw * scale, k * scale],
+                    light: [plate_light[0], plate_light[1], plate_light[2], band * scale],
+                    // Slots y/z/w feed roll_spec and the rim term directly:
+                    // droplets carry their own gleam/shine/rim instead of the
+                    // 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, 0.0],
+                    specular_tint: [1.0, 1.0, 1.0, 0.0],
+                    mode: 10.0,
+                    shape: 2.0,
+                });
+            }
+            Prim::Droplet { rect, color, .. } => {
+                // Legacy banded path: the flat hanging capsule — square top,
+                // round bottom. Degrades the material but keeps the silhouette
+                // (a prim with no arm here VANISHES, it doesn't degrade — see
+                // Ridge/Groove above).
+                let r = (rect.height * 0.5).min(rect.width * 0.5);
+                let radii = crate::widget::CornerRadii::new(0.0, 0.0, r, r);
+                push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, *color, no, None, &mut verts);
+            }
             Prim::ConcaveFillet { cx, cy, radius, depth, start: a0, raised } if shader_plates => {
                 // A quarter-arc carve wall (shader mode 6/7): one cover quad
                 // over the wedge's reach; the wall straddles the arc by ±t/2
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index cbbd25a..6b480e5 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -95,7 +95,10 @@ pub struct PlatePush {
     /// 1.0 = raised lit plate, 2.0 = recess overlay, 3.0 = boss, 4.0 = ridge,
     /// 5.0 = sphere, 6.0/7.0 = concave fillet (recessed/raised), 8.0 = groove
     /// (slab carve about a line: `rect` = [cx, cy, half-width, _], `radii.xy` =
-    /// the line's unit normal, `host` = the surface it is engraved into).
+    /// the line's unit normal, `host` = the surface it is engraved into),
+    /// 9.0 = trough, 10.0 = droplet (`radii` = [sag, belly r, belly half-w,
+    /// blend k] px, `host` = [sheet corner r px, clarity, dome amplitude, _],
+    /// `material.w` = fresnel rim — see shader2d's MODE_DROPLET).
     pub mode: f32,
     /// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
     /// (continuous-curvature) corners — see shader2d's `plate_sdf_grad`.
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 47bdd48..1027637 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -129,6 +129,7 @@ const MODE_FILLET_DOWN: i32 = 6;  // concave inside-corner wall, recessed
 const MODE_FILLET_UP: i32 = 7;    // concave inside-corner wall, raised
 const MODE_GROOVE: i32 = 8;       // slab carve about an arbitrary line
 const MODE_TROUGH: i32 = 9;       // sunken valley straddling the boundary
+const MODE_DROPLET: i32 = 10;     // hanging water droplet clinging to the box top
 // Fillet modes rejoin the shared free-carve path as their flat equivalents.
 const FILLET_TO_STEP: i32 = 4;    // 6 -> RECESS, 7 -> BOSS
 
@@ -347,6 +348,72 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         return vec4f(vcol.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), vcol.a * aa);
     }
 
+    // MODE_DROPLET: a hanging water droplet clinging to the box's TOP edge.
+    // Field reinterpretation (the push block cannot grow):
+    //   p_rect  = the droplet box, center + half-extents (like a plate);
+    //   p_radii = [sag, belly radius, belly half-width, blend k] px;
+    //   p_host  = [sheet corner radius px, edge clarity 0-1, dome amplitude, _];
+    //   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
+    // edge (square top corners — the attach line; bottom lifted by sag) and a
+    // BELLY capsule resting on the box bottom: the polynomial smin forms the
+    // waist/neck a real drop's surface tension pulls in. Shading reuses the
+    // plate vocabulary — roll_slope tilt over the band, ambient/diffuse,
+    // decoupled roll specular — plus two water terms: a fresnel rim crest
+    // (f³, like PLATE_CREST but tunable) and a thin-edge clarity falloff on
+    // the tint alpha, so the (compositor- or resolve_blur-) frosted backdrop
+    // shows through clearer at the rim.
+    if (mode == MODE_DROPLET) {
+        let c = rrect_clip.p_rect.xy;
+        let hx = rrect_clip.p_rect.z;
+        let hy = rrect_clip.p_rect.w;
+        let sag = rrect_clip.p_radii.x;
+        let br = rrect_clip.p_radii.y;
+        let bw = rrect_clip.p_radii.z;
+        let k = max(rrect_clip.p_radii.w, 1.0);
+        let sr = rrect_clip.p_host.x;
+        let clarity = rrect_clip.p_host.y;
+        let dome = rrect_clip.p_host.z;
+
+        // Sheet: bottom lifted by sag, top corners square (the attach line).
+        let a_rect = vec4f(c.x, c.y - sag * 0.5, hx, hy - sag * 0.5);
+        let ga = rr_sdf_grad(frag, a_rect, vec4f(0.0, 0.0, sr, sr));
+        // Belly: a horizontal capsule resting on the box bottom.
+        let b_rect = vec4f(c.x, c.y + hy - br, bw, br);
+        let gb = rr_sdf_grad(frag, b_rect, vec4f(br));
+        // Polynomial smooth union — one drop, smooth neck. The gradient is the
+        // same weighted mix as the distance, renormalized.
+        let hm = clamp(0.5 + 0.5 * (gb.z - ga.z) / k, 0.0, 1.0);
+        let d = mix(gb.z, ga.z, hm) - k * hm * (1.0 - hm);
+        let g = normalize(mix(gb.xy, ga.xy, hm));
+
+        let din = -d;
+        let aa2 = clamp(din + 0.5, 0.0, 1.0);
+        if (aa2 <= 0.0) {
+            discard;
+        }
+        var base = vcol;
+        if (vcol.a < 0.0) {
+            base = resolve_blur(frag, vcol);
+        }
+        let t2 = max(rrect_clip.p_light.w, 0.001);
+        let u = clamp(din / t2, 0.0, 1.0);
+        let f = 1.0 - u;
+        let sv = g * roll_slope(f) * dome;
+        let n = normalize(vec3f(sv, 1.0));
+        let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
+        let extra = rrect_clip.p_mat.w * f * f * f;
+        let shade = 1.0 + (diff / flat_shade - 1.0 + extra) * strength;
+        let spec = roll_spec(sv);
+        // Thin edges are clearer water: the tint opacity falls toward the rim.
+        let body = mix(clarity, 1.0, u);
+        return vec4f(
+            base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength),
+            abs(base.a) * body * aa2,
+        );
+    }
+
     let gd = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
     let d = -gd.z; // positive inside the plate, in px
     let t = max(rrect_clip.p_light.w, 0.001);