use crate::{Color, RenderEffect, RuntimeShader};
pub const LIQUID_GLASS_WGSL: &str = include_str!("../shaders/liquid_glass.wgsl");
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidGlassSpec {
pub corner_radius: f32,
pub bezel_width: f32,
pub displacement_scale: f32,
pub refractive_index: f32,
pub profile: f32,
pub highlight: f32,
pub tilt_angle: f32,
pub tilt_pitch: f32,
pub saturation: f32,
pub chromatic_aberration: f32,
pub lift: f32,
pub contrast: f32,
pub edge_band: f32,
pub dither: f32,
pub light_direction: (f32, f32),
}
impl Default for LiquidGlassSpec {
fn default() -> Self {
Self {
corner_radius: 28.0,
bezel_width: 14.0,
displacement_scale: 24.0,
refractive_index: 1.5,
profile: 0.6,
highlight: 0.7,
tilt_angle: 0.0,
tilt_pitch: 0.0,
saturation: 1.0,
chromatic_aberration: 0.0,
lift: 0.0,
contrast: 1.0,
edge_band: 0.5,
dither: 0.5,
light_direction: (0.0, 1.0),
}
}
}
#[derive(Clone, Debug)]
pub struct LiquidGlassRect {
pub left: f32,
pub top: f32,
pub width: f32,
pub height: f32,
pub tint_color: Color,
}
pub fn liquid_glass_effect(
rect: &LiquidGlassRect,
spec: &LiquidGlassSpec,
area_width: f32,
area_height: f32,
) -> RenderEffect {
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
let cx = rect.left + rect.width * 0.5;
let cy = rect.top + rect.height * 0.5;
shader.set_float2(0, area_width, area_height); shader.set_float2(2, cx, cy); shader.set_float2(4, rect.width, rect.height); shader.set_float(6, spec.corner_radius);
shader.set_float(7, spec.bezel_width);
shader.set_float(8, spec.displacement_scale);
shader.set_float(9, spec.refractive_index);
shader.set_float(10, spec.profile);
shader.set_float(11, spec.highlight);
shader.set_float2(12, spec.tilt_angle, spec.tilt_pitch);
shader.set_float4(
14,
rect.tint_color.r(),
rect.tint_color.g(),
rect.tint_color.b(),
rect.tint_color.a(),
);
shader.set_float(18, spec.saturation);
shader.set_float(19, spec.chromatic_aberration);
shader.set_float(20, spec.lift);
shader.set_float(21, spec.dither);
shader.set_float2(22, spec.light_direction.0, spec.light_direction.1);
shader.set_float(24, spec.contrast);
shader.set_float(25, spec.edge_band);
shader.set_input_padding(liquid_glass_input_padding(spec));
RenderEffect::runtime_shader(shader)
}
fn liquid_glass_input_padding(spec: &LiquidGlassSpec) -> f32 {
let bend = 1.0 - 1.0 / spec.refractive_index.max(1.0001);
let tilt = (spec.tilt_angle * spec.tilt_angle + spec.tilt_pitch * spec.tilt_pitch).sqrt();
let reach = 1.0 + tilt;
let slope = liquid_glass_max_lens_slope(spec.profile);
let spread = 1.0 + spec.chromatic_aberration.max(0.0) * 0.5;
let displacement = reach * bend * spec.displacement_scale.max(0.0) * slope * spread;
if displacement > 0.0 {
displacement.ceil() + 2.0
} else {
0.0
}
}
fn liquid_glass_max_lens_slope(profile: f32) -> f32 {
let p = profile.clamp(0.0, 1.0);
4.0 + 0.35 * (2.0 + 2.0 * p)
}
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidLoupeSpec {
pub magnification: f32,
pub focus_offset: (f32, f32),
pub band_start: f32,
pub fold_peak: f32,
pub dispersion: f32,
pub seam_lift: f32,
pub highlight: f32,
pub progress: f32,
pub corner_radius: f32,
}
impl Default for LiquidLoupeSpec {
fn default() -> Self {
Self {
magnification: 1.25,
focus_offset: (0.0, 75.0),
band_start: 0.78,
fold_peak: 0.80,
dispersion: 0.35,
seam_lift: 26.0,
highlight: 4.4,
progress: 1.0,
corner_radius: 0.0,
}
}
}
pub fn liquid_loupe_effect(node_size: (f32, f32), spec: &LiquidLoupeSpec) -> RenderEffect {
let (w, h) = (node_size.0.max(1.0), node_size.1.max(1.0));
let alpha = spec.progress.clamp(0.0, 1.0);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
shader.set_float2(0, w, h); shader.set_float2(2, w * 0.5, h * 0.5); shader.set_float2(4, w, h);
if spec.corner_radius > 0.0 {
shader.set_float(6, spec.corner_radius.min(0.5 * h.min(w)));
} else {
shader.set_float(6, -1.0); }
shader.set_float(7, 0.5 * h.min(w)); shader.set_float(11, spec.highlight);
shader.set_float4(14, 1.0, 1.0, 1.0, 0.0); shader.set_float(18, 1.0); shader.set_float(20, 0.0); shader.set_float(21, 0.5); shader.set_float2(22, 0.0, 1.0);
shader.set_float(24, 1.0); shader.set_float(28, 1.0); shader.set_float(29, 0.45); shader.set_float(80, 1.0); shader.set_float2(81, spec.focus_offset.0, spec.focus_offset.1);
shader.set_float(83, spec.magnification);
shader.set_float(84, spec.band_start);
shader.set_float(85, spec.fold_peak);
shader.set_float(86, spec.dispersion);
shader.set_float(87, spec.seam_lift);
shader.set_float(90, alpha.max(1.0e-3));
let r_in = 0.5 * w.min(h);
let focus_reach = (spec.focus_offset.0.powi(2) + spec.focus_offset.1.powi(2)).sqrt();
let fold_reach = (spec.fold_peak.max(1.0) - 1.0) * r_in;
shader.set_input_padding((focus_reach + fold_reach + 8.0).ceil());
RenderEffect::runtime_shader(shader)
}
pub fn liquid_menu_glass_effect(
node_size: (f32, f32),
blur_radius_px: f32,
progress: f32,
) -> RenderEffect {
let (w, h) = (node_size.0.max(1.0), node_size.1.max(1.0));
let p = progress.clamp(0.0, 1.0);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
shader.set_float2(0, w, h); shader.set_float2(2, w * 0.5, h * 0.5);
shader.set_float2(4, w, h);
shader.set_float(6, -1.0); shader.set_float(7, 8.0); shader.set_float(8, 2.5 * p); shader.set_float(9, 1.4);
shader.set_float(10, 0.6);
shader.set_float(11, 0.19 * p); shader.set_float4(14, 0.0, 0.0, 0.0, 0.04 * p);
shader.set_float(18, 1.0 + 0.10 * p); shader.set_float(19, 0.0); shader.set_float(20, -0.06 * p);
shader.set_float(24, 1.0 + 0.05 * p); shader.set_float(88, 0.7); shader.set_float(89, 0.45); shader.set_float(21, 0.5);
shader.set_float2(22, 0.0, 1.0);
shader.set_float(25, 0.5);
shader.set_input_padding(12.0);
let lens = RenderEffect::runtime_shader(shader);
if blur_radius_px > 0.5 {
let radius = (blur_radius_px * (1.0 - p)).max(blur_radius_px * 0.08);
RenderEffect::blur(radius).then(lens)
} else {
lens
}
}
pub fn liquid_glass_effect_multi(
rects: &[LiquidGlassRect],
spec: &LiquidGlassSpec,
area_width: f32,
area_height: f32,
) -> Option<RenderEffect> {
let mut result: Option<RenderEffect> = None;
for rect in rects {
let effect = liquid_glass_effect(rect, spec, area_width, area_height);
result = Some(match result {
Some(existing) => existing.then(effect),
None => effect,
});
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn liquid_glass_spec_defaults() {
let spec = LiquidGlassSpec::default();
assert_eq!(spec.corner_radius, 28.0);
assert_eq!(spec.bezel_width, 14.0);
assert_eq!(spec.refractive_index, 1.5);
assert_eq!(spec.saturation, 1.0);
assert_eq!(spec.chromatic_aberration, 0.0);
assert_eq!(spec.lift, 0.0);
assert_eq!(spec.contrast, 1.0);
assert_eq!(spec.light_direction, (0.0, 1.0));
}
#[test]
fn liquid_glass_effect_uniforms() {
let rect = LiquidGlassRect {
left: 100.0,
top: 50.0,
width: 200.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
};
let spec = LiquidGlassSpec {
saturation: 1.6,
chromatic_aberration: 0.4,
lift: 0.12,
contrast: 1.05,
edge_band: 0.4,
dither: 1.0,
light_direction: (0.3, 0.7),
..LiquidGlassSpec::default()
};
let effect = liquid_glass_effect(&rect, &spec, 800.0, 600.0);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let u = shader.uniforms();
assert_eq!(u[0], 800.0);
assert_eq!(u[1], 600.0);
assert_eq!(u[2], 200.0);
assert_eq!(u[3], 100.0);
assert_eq!(u[4], 200.0);
assert_eq!(u[5], 100.0);
assert_eq!(u[6], 28.0);
assert_eq!(u[18], 1.6);
assert_eq!(u[19], 0.4);
assert_eq!(u[20], 0.12);
assert_eq!(u[21], 1.0);
assert_eq!(u[22], 0.3);
assert_eq!(u[23], 0.7);
assert_eq!(u[24], 1.05);
assert_eq!(u[25], 0.4);
}
#[test]
fn liquid_glass_declares_padding_for_static_lensing() {
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect(
&LiquidGlassRect {
left: 0.0,
top: 0.0,
width: 140.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
},
&spec,
140.0,
100.0,
);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let bend = 1.0 - 1.0 / spec.refractive_index;
let min_padding = bend * spec.displacement_scale;
assert!(
shader.input_padding() >= min_padding,
"static lensing must capture backdrop beyond the rect: {} < {min_padding}",
shader.input_padding()
);
}
#[test]
fn liquid_glass_padding_covers_max_shader_displacement() {
let spec = LiquidGlassSpec {
tilt_angle: 0.5,
tilt_pitch: 0.3,
chromatic_aberration: 0.6,
..LiquidGlassSpec::default()
};
let effect = liquid_glass_effect(
&LiquidGlassRect {
left: 0.0,
top: 0.0,
width: 140.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
},
&spec,
140.0,
100.0,
);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let bend = 1.0 - 1.0 / spec.refractive_index.max(1.0001);
let tilt = (spec.tilt_angle * spec.tilt_angle + spec.tilt_pitch * spec.tilt_pitch).sqrt();
let slope = 2.0 + 2.0 * spec.profile.clamp(0.0, 1.0);
let spread = 1.0 + spec.chromatic_aberration * 0.5;
let max_displacement = (1.0 + tilt) * bend * spec.displacement_scale * slope * spread;
assert!(
shader.input_padding() >= max_displacement,
"backdrop capture must cover the largest refracted sample: {} < {max_displacement}",
shader.input_padding()
);
}
#[test]
fn liquid_glass_effect_multi_chains() {
let rects = vec![
LiquidGlassRect {
left: 10.0,
top: 10.0,
width: 100.0,
height: 100.0,
tint_color: Color(1.0, 0.0, 0.0, 0.1),
},
LiquidGlassRect {
left: 200.0,
top: 200.0,
width: 100.0,
height: 100.0,
tint_color: Color(0.0, 0.0, 1.0, 0.1),
},
];
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect_multi(&rects, &spec, 800.0, 600.0);
assert!(effect.is_some());
assert!(matches!(effect.unwrap(), RenderEffect::Chain { .. }));
}
#[test]
fn liquid_glass_effect_multi_empty() {
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect_multi(&[], &spec, 800.0, 600.0);
assert!(effect.is_none());
}
}