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)
}
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());
}
}