GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
droplet: attach-radius taper and belly-off — the oval dewdrop
The sheet's top corners get their own radius (DropletSpec::attach,
p_host.w): the meniscus taper that curves the silhouette into the
attach line instead of meeting it square. belly <= 0 now disables the
belly capsule outright (the shader skips the smin). Defaults move to
the oval dewdrop — no belly, no sag, attach 0.35 + sheet_r 0.55 — one
continuous curve from tapered contact to round bottom; the old
clinging-pool look stays reachable (belly > 0, sag > 0). When
attach + sheet_r overfill the sheet height the pair scales down
proportionally, so 0.5 + 0.5 is the exact egg. Legacy fallback keeps
silhouette parity via the same two radii.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 40 +++++++++++++++++++++++++++++-----------
src/scene/paint.rs | 33 +++++++++++++++++++++++----------
src/vk/shader2d.wgsl | 31 ++++++++++++++++++++-----------
3 files changed, 72 insertions(+), 32 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 39f51e5..2e1a160 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1985,11 +1985,27 @@ pub fn tessellate_display_list(
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);
+ // belly ≤ 0 disables the belly outright (the oval-dewdrop
+ // default) — the shader skips the smin when the radius is 0.
+ let (br, bw) = if spec.belly > 0.0 {
+ let br = (spec.belly.min(1.0) * rect.height).min(hy).min(hx);
+ (br, ((hx - br).max(0.0) * spec.belly_w.clamp(0.0, 1.0)).max(1.0))
+ } else {
+ (0.0, 0.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);
+ // Bottom (sheet_r) and top (attach) corner radii: when the
+ // pair overfills the sheet height, scale both down
+ // proportionally — 0.5 + 0.5 is the fully continuous egg.
+ let mut sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(hx);
+ let mut ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(hx);
+ let sheet_h = (2.0 * sheet_hy).max(0.0);
+ if sr + ar > sheet_h && sr + ar > 0.0 {
+ let f = sheet_h / (sr + ar);
+ sr *= f;
+ ar *= f;
+ }
let band = (spec.band.max(0.05) * rect.height).max(1.0);
plate = Some(crate::vk::PlatePush {
rect: [
@@ -2004,19 +2020,21 @@ pub fn tessellate_display_list(
// 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],
+ host: [sr * scale, spec.clarity.clamp(0.0, 1.0), spec.dome, ar * scale],
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);
+ Prim::Droplet { rect, color, spec } => {
+ // Legacy banded path: the flat drop outline — attach-tapered
+ // 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 cap = (rect.height * 0.5).min(rect.width * 0.5);
+ let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
+ let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
+ let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
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 => {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index eec3990..ebf3a75 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -66,7 +66,9 @@ pub struct DropletSpec {
/// How far the sheet's bottom lifts above the rect bottom (the waist the
/// sides pull up into), fraction of height. 0 = no waist (a capsule).
pub sag: f32,
- /// Belly capsule radius, fraction of height.
+ /// Belly capsule radius, fraction of height. **≤ 0 disables the belly**:
+ /// the drop is the sheet alone — with `attach` and `sheet_r` rounding its
+ /// top and bottom this is the oval dewdrop, and the default.
pub belly: f32,
/// Belly half-width, fraction of the half-width left after the belly
/// radius (1 = the belly spans the whole bottom).
@@ -76,6 +78,13 @@ pub struct DropletSpec {
pub blend: f32,
/// Sheet bottom-corner radius, fraction of height.
pub sheet_r: f32,
+ /// Sheet TOP-corner radius (the meniscus taper at the attach line),
+ /// fraction of height. 0 = the sides meet the attach edge square (the
+ /// clinging-pool look); larger values narrow the contact span so the
+ /// silhouette curves into the edge like a dewdrop. When `attach + sheet_r`
+ /// exceeds the sheet height the pair scales down proportionally, so 0.5 +
+ /// 0.5 is the fully continuous egg curve with no straight side segment.
+ pub attach: f32,
/// Tint opacity at the deep interior relative to the color's own alpha;
/// the rim falls toward `clarity` × that (thin water is clearer). 1 = flat.
pub clarity: f32,
@@ -93,18 +102,22 @@ pub struct DropletSpec {
impl Default for DropletSpec {
fn default() -> Self {
+ // The oval dewdrop: no belly, no sag — one continuous curve from a
+ // tapered attach line to a fully round bottom. The pendant-pool look
+ // is reachable by setting `belly` > 0 (and usually some `sag`).
Self {
- sag: 0.45,
- belly: 0.75,
- belly_w: 0.85,
+ sag: 0.0,
+ belly: 0.0,
+ belly_w: 0.5,
blend: 0.35,
- sheet_r: 0.3,
- clarity: 0.55,
- dome: 1.0,
+ sheet_r: 0.55,
+ attach: 0.35,
+ clarity: 0.5,
+ dome: 0.9,
band: 0.9,
- gleam: 1.2,
- shine: 24.0,
- rim: 0.35,
+ gleam: 1.4,
+ shine: 32.0,
+ rim: 0.5,
}
}
}
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 1027637..2214a13 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -352,7 +352,8 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
// 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_host = [sheet bottom-corner radius px, edge clarity 0-1, dome
+ // amplitude, attach (top-corner) radius px];
// 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
@@ -375,18 +376,26 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
let sr = rrect_clip.p_host.x;
let clarity = rrect_clip.p_host.y;
let dome = rrect_clip.p_host.z;
+ let ar = rrect_clip.p_host.w;
- // Sheet: bottom lifted by sag, top corners square (the attach line).
+ // Sheet: bottom lifted by sag; top corners carry the attach radius —
+ // the meniscus taper that curves the sides into the attach line (0 =
+ // the square-shouldered clinging-pool look).
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 ga = rr_sdf_grad(frag, a_rect, vec4f(ar, ar, sr, sr));
+ var d = ga.z;
+ var g = ga.xy;
+ // Belly (radius > 0 only): a horizontal capsule resting on the box
+ // bottom, joined by polynomial smooth union — one drop, smooth neck.
+ // The gradient is the same weighted mix as the distance, renormalized.
+ if (br > 0.5) {
+ let b_rect = vec4f(c.x, c.y + hy - br, bw, br);
+ let gb = rr_sdf_grad(frag, b_rect, vec4f(br));
+ let hm = clamp(0.5 + 0.5 * (gb.z - ga.z) / k, 0.0, 1.0);
+ d = mix(gb.z, ga.z, hm) - k * hm * (1.0 - hm);
+ g = mix(gb.xy, ga.xy, hm);
+ }
+ g = normalize(g);
let din = -d;
let aa2 = clamp(din + 0.5, 0.0, 1.0);