use crate::{Color, RenderEffect, RuntimeShader};
pub const LIQUID_GLASS_WGSL: &str = include_str!("../shaders/liquid_glass.wgsl");
#[derive(Clone, Debug)]
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,
}
impl Default for LiquidGlassSpec {
fn default() -> Self {
Self {
corner_radius: 28.0,
bezel_width: 14.0,
displacement_scale: 44.0,
refractive_index: 1.8,
profile: 1.4,
highlight: 0.7,
tilt_angle: 0.0,
tilt_pitch: 0.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_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.abs().max(spec.tilt_pitch.abs());
let slope = liquid_glass_max_height_slope(spec.profile);
let displacement = tilt * bend * spec.displacement_scale.max(0.0) * slope;
if displacement > 0.0 {
displacement.ceil() + 2.0
} else {
0.0
}
}
fn liquid_glass_max_height_slope(profile: f32) -> f32 {
let p = profile.clamp(0.0, 1.0);
2.0 + 2.0 * p
}
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.displacement_scale, 44.0);
assert_eq!(spec.refractive_index, 1.8);
assert_eq!(spec.profile, 1.4);
assert_eq!(spec.highlight, 0.7);
}
#[test]
fn liquid_glass_rect_fields() {
let rect = LiquidGlassRect {
left: 50.0,
top: 30.0,
width: 200.0,
height: 100.0,
tint_color: Color(0.6, 0.8, 1.0, 0.15),
};
assert_eq!(rect.left, 50.0);
assert_eq!(rect.width, 200.0);
}
#[test]
fn liquid_glass_effect_single() {
let rect = LiquidGlassRect {
left: 50.0,
top: 30.0,
width: 200.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
};
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect(&rect, &spec, 800.0, 600.0);
assert!(matches!(effect, RenderEffect::Shader { .. }));
}
#[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::default();
let effect = liquid_glass_effect(&rect, &spec, 800.0, 600.0);
if let RenderEffect::Shader { 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[8], 44.0);
assert_eq!(u[9], 1.8);
assert_eq!(shader.input_padding(), 0.0);
} else {
panic!("expected Shader effect");
}
}
#[test]
fn liquid_glass_declares_backdrop_input_padding_for_tilted_refraction() {
let rect = LiquidGlassRect {
left: 0.0,
top: 0.0,
width: 140.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
};
let effect = liquid_glass_effect(
&rect,
&LiquidGlassSpec {
tilt_angle: 0.5,
tilt_pitch: 0.3,
..LiquidGlassSpec::default()
},
140.0,
100.0,
);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
assert!(
shader.input_padding() >= 11.0,
"tilted liquid glass must capture enough backdrop for displaced samples"
);
}
#[test]
fn liquid_glass_padding_covers_max_shader_displacement() {
let spec = LiquidGlassSpec {
tilt_angle: 0.5,
tilt_pitch: 0.3,
..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 max_shader_displacement = spec.tilt_angle.abs() * bend * spec.displacement_scale * 4.0;
assert!(
shader.input_padding() >= max_shader_displacement.ceil() + 2.0,
"backdrop capture must cover the shader's largest refracted sample"
);
}
#[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());
}
}