GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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);