use crate::{Color, RenderEffect, RuntimeShader};
pub const GLASS_BLUR_RADIUS_UNIFORM: usize = 93;
pub const GLASS_REFRACTION_CURVE_UNIFORM: usize = 94;
pub const GLASS_DISPERSION_UNIFORM: usize = 95;
pub const GLASS_TRANSMISSION_REFRACTION_UNIFORM: usize = 96;
pub const GLASS_EFFECT_DENSITY_UNIFORM: usize = 99;
pub const GLASS_MENISCUS_ABSORPTION_UNIFORM: usize = 100;
pub const GLASS_FOLD_DEPTH_UNIFORM: usize = 88;
pub const GLASS_OPTICAL_ZOOM_UNIFORM: usize = 89;
pub const GLASS_ACTIVITY_UNIFORM: usize = 111;
pub const GLASS_RESTING_TINT_UNIFORM: usize = 113;
pub const LIQUID_GLASS_WGSL: &str = include_str!("../shaders/liquid_glass.wgsl");
pub const GLASS_LIGHT_DIRECTION_UNIFORM: usize = 122;
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidGlassSpec {
pub corner_radius: f32,
pub refraction_depth: f32,
pub refraction_curve: f32,
pub blur_radius: f32,
pub highlight: f32,
pub saturation: f32,
pub lift: f32,
pub contrast: f32,
pub meniscus_absorption: f32,
pub dither: f32,
}
impl Default for LiquidGlassSpec {
fn default() -> Self {
Self {
corner_radius: 28.0,
refraction_depth: 0.34,
refraction_curve: 0.25,
blur_radius: 0.0,
highlight: 0.7,
saturation: 1.0,
lift: 0.0,
contrast: 1.0,
meniscus_absorption: 1.0,
dither: 0.5,
}
}
}
#[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(9, spec.refraction_depth.clamp(0.0, 2.0));
shader.set_float(
GLASS_REFRACTION_CURVE_UNIFORM,
spec.refraction_curve.clamp(0.05, 1.0),
);
shader.set_float(11, spec.highlight);
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(20, spec.lift);
shader.set_float(21, spec.dither);
shader.set_float(24, spec.contrast);
shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, spec.blur_radius.max(0.0));
shader.set_float(GLASS_TRANSMISSION_REFRACTION_UNIFORM, 1.0);
shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, 1.0);
shader.set_float(
GLASS_MENISCUS_ABSORPTION_UNIFORM,
spec.meniscus_absorption.clamp(0.0, 1.0),
);
shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
shader.set_input_padding(liquid_glass_input_padding(spec));
RenderEffect::runtime_shader(shader)
}
fn liquid_glass_input_padding(spec: &LiquidGlassSpec) -> f32 {
spec.blur_radius.max(2.0).ceil()
}
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidLoupeSpec {
pub magnification: f32,
pub focus_offset: (f32, f32),
pub dispersion: f32,
pub highlight: f32,
pub activity: f32,
pub corner_radius: f32,
}
impl Default for LiquidLoupeSpec {
fn default() -> Self {
Self {
magnification: 1.25,
focus_offset: (0.0, 75.0),
dispersion: 0.36,
highlight: 0.42,
activity: 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 activity = spec.activity.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(9, 0.34 * activity);
shader.set_float(GLASS_REFRACTION_CURVE_UNIFORM, 0.25);
shader.set_float(
GLASS_DISPERSION_UNIFORM,
spec.dispersion.clamp(0.0, 1.0) * activity,
);
shader.set_float(GLASS_TRANSMISSION_REFRACTION_UNIFORM, 1.0);
shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, 1.0);
shader.set_float(GLASS_MENISCUS_ABSORPTION_UNIFORM, 1.0);
shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
shader.set_float(11, spec.highlight * activity);
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_float(24, 1.0); shader.set_float(28, activity); shader.set_float(80, 1.0); shader.set_float2(81, spec.focus_offset.0, spec.focus_offset.1);
shader.set_float(83, 1.0 + (spec.magnification.max(0.2) - 1.0) * activity);
shader.set_float(90, activity);
let focus_reach = (spec.focus_offset.0.powi(2) + spec.focus_offset.1.powi(2)).sqrt();
shader.set_input_padding((focus_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(9, 0.10 * p);
shader.set_float(GLASS_REFRACTION_CURVE_UNIFORM, 0.25);
shader.set_float(GLASS_TRANSMISSION_REFRACTION_UNIFORM, 1.0);
shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, 1.0);
shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
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(20, -0.06 * p);
shader.set_float(24, 1.0 + 0.05 * p); shader.set_float(21, 0.5);
let blur_radius = if blur_radius_px > 0.5 {
(blur_radius_px * (1.0 - p)).max(blur_radius_px * 0.08)
} else {
0.0
};
shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, blur_radius);
shader.set_input_padding(12.0 + blur_radius);
RenderEffect::runtime_shader(shader)
}
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::*;
fn rect() -> LiquidGlassRect {
LiquidGlassRect {
left: 100.0,
top: 50.0,
width: 200.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
}
}
#[test]
fn liquid_glass_spec_defaults_match_wcksrd() {
let spec = LiquidGlassSpec::default();
assert_eq!(spec.corner_radius, 28.0);
assert_eq!(spec.refraction_depth, 0.34);
assert_eq!(spec.refraction_curve, 0.25);
assert_eq!(spec.blur_radius, 0.0);
assert_eq!(spec.saturation, 1.0);
assert_eq!(spec.lift, 0.0);
assert_eq!(spec.contrast, 1.0);
assert_eq!(spec.meniscus_absorption, 1.0);
}
#[test]
fn liquid_glass_effect_packs_the_single_optical_program() {
let spec = LiquidGlassSpec {
refraction_depth: 0.72,
refraction_curve: 0.8,
blur_radius: 6.0,
saturation: 1.6,
lift: 0.12,
contrast: 1.05,
meniscus_absorption: 0.3,
dither: 1.0,
..LiquidGlassSpec::default()
};
let RenderEffect::Shader { shader } = liquid_glass_effect(&rect(), &spec, 800.0, 600.0)
else {
panic!("liquid glass must be one runtime shader");
};
let uniforms = shader.uniforms();
assert_eq!(&uniforms[0..6], &[800.0, 600.0, 200.0, 100.0, 200.0, 100.0]);
assert_eq!(uniforms[9], 0.72);
assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
assert_eq!(uniforms[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
assert_eq!(uniforms[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
assert_eq!(uniforms[18], 1.6);
assert_eq!(uniforms[20], 0.12);
assert_eq!(uniforms[21], 1.0);
assert_eq!(uniforms[24], 1.05);
assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 6.0);
assert_eq!(shader.input_padding(), 6.0);
}
#[test]
fn refraction_depth_is_clamped_at_the_shader_boundary() {
for (input, expected) in [(-1.0, 0.0), (0.8, 0.8), (3.0, 2.0)] {
let spec = LiquidGlassSpec {
refraction_depth: input,
..LiquidGlassSpec::default()
};
let RenderEffect::Shader { shader } = liquid_glass_effect(&rect(), &spec, 800.0, 600.0)
else {
panic!("liquid glass must be one runtime shader");
};
assert_eq!(shader.uniforms()[9], expected);
}
}
#[test]
fn refraction_curve_is_clamped_at_the_shader_boundary() {
for (input, expected) in [(-1.0, 0.05), (0.8, 0.8), (3.0, 1.0)] {
let spec = LiquidGlassSpec {
refraction_curve: input,
..LiquidGlassSpec::default()
};
let RenderEffect::Shader { shader } = liquid_glass_effect(&rect(), &spec, 800.0, 600.0)
else {
panic!("liquid glass must be one runtime shader");
};
assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], expected);
}
}
#[test]
fn loupe_and_menu_use_the_same_wcksrd_program() {
let loupe_spec = LiquidLoupeSpec::default();
let RenderEffect::Shader { shader: loupe } =
liquid_loupe_effect((117.0, 82.0), &loupe_spec)
else {
panic!("loupe must use the shared runtime shader");
};
assert_eq!(loupe.uniforms()[9], 0.34);
assert_eq!(loupe.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.25);
assert_eq!(
loupe.uniforms()[GLASS_DISPERSION_UNIFORM],
loupe_spec.dispersion
);
assert_eq!(loupe.uniforms()[80], 1.0);
assert!(loupe.input_padding() >= 75.0);
let RenderEffect::Shader { shader: menu } =
liquid_menu_glass_effect((240.0, 44.0), 8.0, 1.0)
else {
panic!("menu must use the shared runtime shader");
};
assert_eq!(menu.uniforms()[9], 0.10);
assert_eq!(menu.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.25);
assert!((menu.uniforms()[GLASS_BLUR_RADIUS_UNIFORM] - 0.64).abs() < 1.0e-6);
}
#[test]
fn liquid_glass_effect_multi_handles_empty_and_multiple_rects() {
let spec = LiquidGlassSpec::default();
assert!(liquid_glass_effect_multi(&[], &spec, 800.0, 600.0).is_none());
let rects = [rect(), rect()];
assert!(matches!(
liquid_glass_effect_multi(&rects, &spec, 800.0, 600.0),
Some(RenderEffect::Chain { .. })
));
}
}