use crate::theme::LiquidColors;
use cranpose_ui::current_density;
use cranpose_ui::Modifier;
use cranpose_ui_graphics::{
Color, GraphicsLayer, LayerShape, RenderEffect, RoundedCornerShape, RuntimeShader,
GLASS_ACTIVITY_UNIFORM, GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM,
GLASS_EFFECT_DENSITY_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM, GLASS_MENISCUS_ABSORPTION_UNIFORM,
GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM, GLASS_RESTING_TINT_UNIFORM,
GLASS_TRANSMISSION_REFRACTION_UNIFORM, LIQUID_GLASS_WGSL,
};
use std::rc::Rc;
const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
const CAPSULE_SHADER_RADIUS: f32 = -1.0;
const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum LiquidShape {
#[default]
Capsule,
RoundedRect(f32),
Circle,
}
impl LiquidShape {
pub fn clip_shape(&self) -> RoundedCornerShape {
match self {
LiquidShape::Capsule | LiquidShape::Circle => {
RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
}
LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
}
}
pub fn layer_shape(&self) -> LayerShape {
LayerShape::Rounded(self.clip_shape())
}
fn shader_radius_px(&self, density: f32) -> f32 {
match self {
LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
LiquidShape::RoundedRect(radius) => radius * density,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GlassVariant {
#[default]
Regular,
Clear,
Lens,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlassShadow {
pub color: Color,
pub radius: f32,
pub offset_y: f32,
pub spread: f32,
}
impl GlassShadow {
pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
Self {
color,
radius: radius.max(0.0),
offset_y,
spread,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct GlassDynamics {
pub activity: Option<f32>,
pub resting_tint: Option<Color>,
pub highlight_boost: f32,
pub saturation_boost: f32,
pub tint_alpha_multiplier: Option<f32>,
pub morph: Option<GlassMorph>,
pub touch: Option<(f32, f32, f32)>,
}
fn foreground_is_dark(foreground: Color) -> bool {
let foreground_luma =
0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
foreground_luma < 0.5
}
fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
if boost.abs() <= f32::EPSILON {
return tint;
}
let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
let saturation = (1.0 + boost).max(0.0);
Color::rgba(
(luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
(luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
(luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
tint.a(),
)
}
pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
if foreground_is_dark(foreground) {
Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
} else {
Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
}
}
pub(crate) fn neutral_surface_lift(foreground: Color, light_lift: f32, dark_lift: f32) -> f32 {
if foreground_is_dark(foreground) {
light_lift
} else {
dark_lift
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct GlassMorph {
pub node_size: (f32, f32),
pub primary: (f32, f32, f32, f32, f32),
pub shapes: Vec<(f32, f32, f32, f32, f32)>,
pub glue: f32,
pub wobble_amplitude: f32,
pub wobble_phase: f32,
pub bulge_amplitude: f32,
pub bulge_direction: f32,
pub ellipse_blend: f32,
pub deformation: Option<GlassDeformation>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlassDeformation {
axis: (f32, f32),
along: f32,
}
impl GlassDeformation {
pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
let axis = if length > f32::EPSILON {
(axis.0 / length, axis.1 / length)
} else {
(1.0, 0.0)
};
Self {
axis,
along: along.max(f32::EPSILON),
}
}
pub fn axis(self) -> (f32, f32) {
self.axis
}
pub fn along(self) -> f32 {
self.along
}
pub fn across(self) -> f32 {
1.0 / self.along
}
}
impl GlassMorph {
pub const MAX_SHAPES: usize = 8;
}
#[derive(Clone, Debug, PartialEq)]
pub struct Glass {
pub variant: GlassVariant,
pub shape: LiquidShape,
pub tint: Option<Color>,
pub blur_radius: Option<f32>,
pub saturation: Option<f32>,
pub refraction_depth: f32,
pub refraction_curve: f32,
pub dispersion: f32,
pub transmission_refraction: f32,
pub meniscus_absorption: f32,
pub fold_depth: f32,
pub optical_zoom: f32,
pub rim_reflection: f32,
pub highlight: f32,
pub lift: Option<f32>,
pub shadow: bool,
pub shadow_style: Option<GlassShadow>,
pub clip: bool,
pub foreground: Option<Color>,
pub adaptive_frost: f32,
}
impl Glass {
pub fn regular() -> Self {
Self {
variant: GlassVariant::Regular,
shape: LiquidShape::Capsule,
tint: None,
blur_radius: None,
saturation: None,
refraction_depth: 0.34,
refraction_curve: 0.25,
dispersion: 0.0,
transmission_refraction: 1.0,
meniscus_absorption: 1.0,
fold_depth: 0.0,
optical_zoom: 1.0,
rim_reflection: 1.0,
highlight: 0.9,
lift: None,
shadow: true,
shadow_style: None,
clip: true,
foreground: None,
adaptive_frost: 0.65,
}
}
pub fn clear() -> Self {
Self {
variant: GlassVariant::Clear,
..Self::regular()
}
}
pub fn lens() -> Self {
Self {
variant: GlassVariant::Lens,
shape: LiquidShape::Capsule,
tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
blur_radius: None,
saturation: None,
refraction_depth: 0.34,
refraction_curve: 1.0,
dispersion: 0.30,
transmission_refraction: 1.0,
meniscus_absorption: 1.0,
fold_depth: 0.0,
optical_zoom: 1.0,
rim_reflection: 1.0,
highlight: 1.15,
lift: None,
shadow: true,
shadow_style: None,
clip: true,
foreground: None,
adaptive_frost: 0.0,
}
}
pub fn shape(mut self, shape: LiquidShape) -> Self {
self.shape = shape;
self
}
pub fn tint(mut self, tint: Color) -> Self {
self.tint = Some(tint);
self
}
pub fn blur_radius(mut self, radius_dp: f32) -> Self {
self.blur_radius = Some(radius_dp);
self
}
pub fn saturation(mut self, saturation: f32) -> Self {
self.saturation = Some(saturation);
self
}
pub fn refraction_depth(mut self, fraction: f32) -> Self {
self.refraction_depth = fraction.clamp(0.0, 2.0);
self
}
pub fn refraction_curve(mut self, curve: f32) -> Self {
self.refraction_curve = curve.clamp(0.05, 1.0);
self
}
pub fn dispersion(mut self, strength: f32) -> Self {
self.dispersion = strength.clamp(0.0, 1.0);
self
}
pub fn transmission_refraction(mut self, strength: f32) -> Self {
self.transmission_refraction = strength.clamp(0.0, 1.0);
self
}
pub fn meniscus_absorption(mut self, strength: f32) -> Self {
self.meniscus_absorption = strength.clamp(0.0, 1.0);
self
}
pub fn fold_depth(mut self, depth_dp: f32) -> Self {
self.fold_depth = depth_dp.max(0.0);
self
}
pub fn optical_zoom(mut self, zoom: f32) -> Self {
self.optical_zoom = zoom.max(1.0);
self
}
pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
self.rim_reflection = reflectivity.clamp(0.0, 2.0);
self
}
pub fn highlight(mut self, highlight: f32) -> Self {
self.highlight = highlight;
self
}
pub fn lift(mut self, lift: f32) -> Self {
self.lift = Some(lift);
self
}
pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
self.foreground = Some(foreground);
self.adaptive_frost = strength.clamp(0.0, 1.0);
self
}
pub fn shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
self.shadow_style = Some(shadow);
self
}
pub fn no_clip(mut self) -> Self {
self.clip = false;
self
}
fn default_blur_radius(&self) -> f32 {
match self.variant {
GlassVariant::Regular => 8.0,
GlassVariant::Clear => 3.0,
GlassVariant::Lens => 0.0,
}
}
fn default_saturation(&self) -> f32 {
match self.variant {
GlassVariant::Regular => 1.5,
GlassVariant::Clear => 1.25,
GlassVariant::Lens => 1.0,
}
}
pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
(GlassVariant::Regular, false) => 0.42,
(GlassVariant::Regular, true) => -0.38,
(GlassVariant::Clear, false) => 0.12,
(GlassVariant::Clear, true) => -0.12,
(GlassVariant::Lens, false) => 0.10,
(GlassVariant::Lens, true) => -0.08,
});
let foreground = self.foreground.unwrap_or(colors.label);
let shadow = self.shadow_style.unwrap_or_else(|| {
GlassShadow::new(
Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
(GlassVariant::Lens, false) => 0.14,
(GlassVariant::Lens, true) => 0.28,
(_, false) => 0.16,
(_, true) => 0.5,
}),
if self.variant == GlassVariant::Lens {
10.0
} else {
22.0
},
if self.variant == GlassVariant::Lens {
3.0
} else {
8.0
},
if self.variant == GlassVariant::Lens {
-6.0
} else {
-2.0
},
)
});
ResolvedGlass {
shape: self.shape,
tint: self.tint.unwrap_or(colors.glass_tint),
blur_radius_dp: self
.blur_radius
.unwrap_or_else(|| self.default_blur_radius()),
saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
refraction_depth: self.refraction_depth,
refraction_curve: self.refraction_curve,
dispersion: self.dispersion,
transmission_refraction: self.transmission_refraction,
meniscus_absorption: self.meniscus_absorption,
fold_depth: self.fold_depth,
optical_zoom: self.optical_zoom,
rim_reflection: self.rim_reflection,
highlight: self.highlight,
lift,
contrast: if self.variant == GlassVariant::Lens {
1.0
} else {
1.03
},
shadow: self.shadow,
clip: self.clip,
foreground_luma: 0.2126 * foreground.r()
+ 0.7152 * foreground.g()
+ 0.0722 * foreground.b(),
adaptive_frost: self.adaptive_frost,
rim_style: if self.variant == GlassVariant::Lens {
1.0
} else {
0.0
},
shadow_color: shadow.color,
shadow_radius: shadow.radius,
shadow_offset_y: shadow.offset_y,
shadow_spread: shadow.spread,
}
}
}
impl Default for Glass {
fn default() -> Self {
Self::regular()
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ResolvedGlass {
pub shape: LiquidShape,
pub tint: Color,
pub blur_radius_dp: f32,
pub saturation: f32,
pub refraction_depth: f32,
pub refraction_curve: f32,
pub dispersion: f32,
pub transmission_refraction: f32,
pub meniscus_absorption: f32,
pub fold_depth: f32,
pub optical_zoom: f32,
pub rim_reflection: f32,
pub highlight: f32,
pub lift: f32,
pub contrast: f32,
pub shadow: bool,
pub clip: bool,
pub rim_style: f32,
pub foreground_luma: f32,
pub adaptive_frost: f32,
pub shadow_color: Color,
pub shadow_radius: f32,
pub shadow_offset_y: f32,
pub shadow_spread: f32,
}
impl ResolvedGlass {
pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
self.runtime_effect(density, dynamics, false)
}
fn content_mask_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
self.runtime_effect(density, dynamics, true)
}
fn runtime_effect(
&self,
density: f32,
dynamics: GlassDynamics,
content_mask: bool,
) -> RenderEffect {
let density = density.max(f32::EPSILON);
let activity = dynamics
.activity
.filter(|value| value.is_finite())
.unwrap_or(1.0)
.clamp(0.0, 1.0);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
if let Some(morph) = dynamics.morph.as_ref() {
let (node_w, node_h) = morph.node_size;
let (cx, cy, w, h, radius) = morph.primary;
shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
shader.set_float2(2, cx, cy);
shader.set_float2(4, w, h);
shader.set_float(6, radius);
let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
shader.set_float(30, count as f32);
for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
let base = 36 + index * 5;
shader.set_float(base, *sx);
shader.set_float(base + 1, *sy);
shader.set_float(base + 2, *sw);
shader.set_float(base + 3, *sh);
shader.set_float(base + 4, *sr);
}
shader.set_float(31, morph.glue);
shader.set_float(32, morph.wobble_amplitude * activity);
shader.set_float(33, morph.wobble_phase);
shader.set_float(26, morph.bulge_amplitude * activity);
shader.set_float(27, morph.bulge_direction);
shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
if let Some(deformation) = morph.deformation {
let axis = deformation.axis();
let along = 1.0 + (deformation.along() - 1.0) * activity;
shader.set_float2(106, axis.0, axis.1);
shader.set_float(108, along);
shader.set_float(109, 1.0 / along);
} else {
shader.set_float2(106, 1.0, 0.0);
shader.set_float2(108, 1.0, 1.0);
}
} else {
shader.set_float2(0, 0.0, 0.0);
shader.set_float(6, self.shape.shader_radius_px(density));
}
shader.set_float(9, self.refraction_depth * activity);
shader.set_float(
GLASS_REFRACTION_CURVE_UNIFORM,
self.refraction_curve * activity,
);
shader.set_float(GLASS_DISPERSION_UNIFORM, self.dispersion * activity);
shader.set_float(
GLASS_TRANSMISSION_REFRACTION_UNIFORM,
self.transmission_refraction * activity,
);
shader.set_float(GLASS_MENISCUS_ABSORPTION_UNIFORM, self.meniscus_absorption);
shader.set_float(GLASS_FOLD_DEPTH_UNIFORM, self.fold_depth.max(0.0));
shader.set_float(
GLASS_OPTICAL_ZOOM_UNIFORM,
1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
);
shader.set_float(121, self.rim_reflection.max(0.001));
let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
shader.set_float(118, touch_x);
shader.set_float(119, touch_y);
shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
shader.set_float(
11,
(self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
);
let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
let dynamic_tint_alpha = (dynamic_tint.a()
* dynamics
.tint_alpha_multiplier
.unwrap_or(1.0)
.clamp(0.0, 2.0)
* activity)
.clamp(0.0, 1.0);
shader.set_float4(
14,
dynamic_tint.r(),
dynamic_tint.g(),
dynamic_tint.b(),
dynamic_tint_alpha,
);
let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
shader.set_float(20, self.lift * activity);
shader.set_float(21, 0.5 * activity);
shader.set_float2(22, 0.0, 1.0);
shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
shader.set_float(28, self.rim_style * activity);
let requested_blur_radius_px = if content_mask {
0.0
} else {
self.blur_radius_dp * density * activity
};
let wcksrd_blur_radius = requested_blur_radius_px.min(WCKSRD_OPTICAL_BLUR_RADIUS_PX);
let gaussian_blur_radius = (requested_blur_radius_px - wcksrd_blur_radius).max(0.0);
shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
shader.set_float4(
GLASS_RESTING_TINT_UNIFORM,
resting_tint.r(),
resting_tint.g(),
resting_tint.b(),
resting_tint.a(),
);
shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
shader.set_float(91, self.adaptive_frost * activity);
shader.set_float(97, self.foreground_luma);
let dynamic_shadow = !self.clip && self.shadow;
shader.set_float(
102,
if dynamic_shadow {
self.shadow_color.a() * 0.55 * activity
} else {
0.0
},
);
shader.set_float(103, self.shadow_radius);
shader.set_float(104, self.shadow_offset_y);
shader.set_float(105, self.shadow_spread);
let morph_pad = dynamics
.morph
.as_ref()
.map(|morph| {
let (px, py, pw, ph, _) = morph.primary;
let (left, top) = (px - pw * 0.5, py - ph * 0.5);
let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
let mut shape_reach = 0.0f32;
for (sx, sy, sw, sh, _) in &morph.shapes {
let reach_x = ((sx + sw * 0.5) - right)
.max(left - (sx - sw * 0.5))
.max(0.0);
let reach_y = ((sy + sh * 0.5) - bottom)
.max(top - (sy - sh * 0.5))
.max(0.0);
shape_reach = shape_reach.max(reach_x.max(reach_y));
}
let glue_pad = if morph.shapes.is_empty() {
0.0
} else {
morph.glue * 2.0
};
morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
})
.unwrap_or(0.0);
shader.set_input_padding(self.input_padding() + morph_pad + wcksrd_blur_radius / density);
if dynamics.morph.is_some() {
let shadow_reach = if dynamic_shadow {
self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
} else {
0.0
};
shader.set_output_padding(morph_pad + shadow_reach + 4.0);
}
let optical_effect = RenderEffect::runtime_shader(shader);
if gaussian_blur_radius > f32::EPSILON {
RenderEffect::blur(gaussian_blur_radius).then(optical_effect)
} else {
optical_effect
}
}
fn input_padding(&self) -> f32 {
2.0
}
}
pub trait LiquidModifierExt {
fn glass_effect(self, glass: Glass) -> Modifier;
fn glass_effect_with(
self,
glass: Glass,
dynamics: impl Fn() -> GlassDynamics + 'static,
) -> Modifier;
}
impl LiquidModifierExt for Modifier {
fn glass_effect(self, glass: Glass) -> Modifier {
self.glass_effect_with(glass, GlassDynamics::default)
}
fn glass_effect_with(
self,
glass: Glass,
dynamics: impl Fn() -> GlassDynamics + 'static,
) -> Modifier {
let colors = crate::theme::liquid_colors();
let resolved = Rc::new(glass.resolve(&colors));
let shape = resolved.shape;
let mut modifier = self;
if resolved.shadow && resolved.clip {
let shadow_color = resolved.shadow_color;
let (radius, offset_y, spread) = (
resolved.shadow_radius,
resolved.shadow_offset_y,
resolved.shadow_spread,
);
modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
scope.radius = radius;
scope.spread = spread;
scope.offset.y = offset_y;
scope.color = shadow_color;
scope.cutout = true;
});
}
let layer_resolved = Rc::clone(&resolved);
let clip = resolved.clip;
modifier.graphics_layer(move || {
let density = current_density();
let frame = dynamics();
let render_effect = (!clip && frame.morph.is_some())
.then(|| layer_resolved.content_mask_effect(density, frame.clone()));
GraphicsLayer {
backdrop_effect: Some(layer_resolved.backdrop_effect(density, frame)),
render_effect,
shape: shape.layer_shape(),
clip,
..Default::default()
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn light_colors() -> LiquidColors {
LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
}
fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
match effect {
RenderEffect::Shader { shader } => shader,
RenderEffect::Chain { second, .. } => terminal_shader(*second),
effect => panic!("expected runtime shader, got {effect:?}"),
}
}
#[test]
fn liquid_shape_builds_matching_clip_and_layer_shapes() {
for shape in [
LiquidShape::Capsule,
LiquidShape::Circle,
LiquidShape::RoundedRect(12.0),
] {
assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
}
}
#[test]
fn glass_shadow_clamps_negative_radius() {
let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
assert_eq!(shadow.radius, 0.0);
assert_eq!(shadow.offset_y, 3.0);
assert_eq!(shadow.spread, -1.0);
}
#[test]
fn incompressible_deformation_normalizes_axis_and_conserves_area() {
let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
assert_eq!(deformation.axis(), (0.6, 0.8));
assert_eq!(deformation.along(), 1.25);
assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
assert_eq!(
GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
(1.0, 0.0)
);
}
#[test]
fn glass_builders_clamp_physical_inputs() {
let glass = Glass::lens()
.shape(LiquidShape::Circle)
.tint(Color::BLACK)
.blur_radius(-2.0)
.saturation(1.2)
.refraction_depth(3.0)
.refraction_curve(2.0)
.dispersion(2.0)
.transmission_refraction(2.0)
.meniscus_absorption(2.0)
.highlight(0.4)
.lift(-0.2)
.adaptive_frost(Color::WHITE, 2.0)
.shadow(false)
.no_clip();
assert_eq!(glass.shape, LiquidShape::Circle);
assert_eq!(glass.tint, Some(Color::BLACK));
assert_eq!(glass.blur_radius, Some(-2.0));
assert_eq!(glass.saturation, Some(1.2));
assert_eq!(glass.refraction_depth, 2.0);
assert_eq!(glass.refraction_curve, 1.0);
assert_eq!(glass.dispersion, 1.0);
assert_eq!(glass.transmission_refraction, 1.0);
assert_eq!(glass.meniscus_absorption, 1.0);
assert_eq!(glass.highlight, 0.4);
assert_eq!(glass.lift, Some(-0.2));
assert_eq!(glass.adaptive_frost, 1.0);
assert!(!glass.shadow);
assert!(!glass.clip);
}
#[test]
fn material_variants_resolve_distinct_frost_levels() {
let regular = Glass::regular().resolve(&light_colors());
let clear = Glass::clear().resolve(&light_colors());
let lens = Glass::lens().resolve(&light_colors());
assert!(regular.blur_radius_dp > clear.blur_radius_dp);
assert!(clear.blur_radius_dp > lens.blur_radius_dp);
assert!(regular.saturation > clear.saturation);
assert_eq!(lens.rim_style, 1.0);
assert_eq!(regular.refraction_curve, 0.25);
assert_eq!(lens.refraction_curve, 1.0);
assert_eq!(regular.dispersion, 0.0);
assert_eq!(lens.dispersion, 0.30);
}
#[test]
fn neutral_surface_helpers_follow_foreground_polarity() {
assert_eq!(
neutral_surface_tint(Color::BLACK, 0.08, 0.10),
Color::BLACK.with_alpha(0.08)
);
assert_eq!(
neutral_surface_tint(Color::WHITE, 0.08, 0.10),
Color::WHITE.with_alpha(0.10)
);
assert_eq!(neutral_surface_lift(Color::BLACK, 0.7, -0.3), 0.7);
assert_eq!(neutral_surface_lift(Color::WHITE, 0.7, -0.3), -0.3);
}
#[test]
fn dynamic_saturation_reaches_the_material_tint() {
let resting = Color::from_rgb_u8(0, 199, 208);
let raised = boost_tint_saturation(resting, 0.55);
assert_eq!(raised.r(), 0.0);
assert!(raised.g() > resting.g());
assert!(raised.b() > resting.b());
assert_eq!(raised.a(), resting.a());
}
#[test]
fn resolved_material_packs_wcksrd_and_dynamic_tint() {
let resolved = Glass::lens()
.refraction_depth(0.72)
.refraction_curve(0.8)
.dispersion(0.42)
.transmission_refraction(0.35)
.meniscus_absorption(0.3)
.blur_radius(3.0)
.tint(Color::BLACK.with_alpha(0.8))
.resolve(&light_colors());
let effect = resolved.backdrop_effect(
2.0,
GlassDynamics {
highlight_boost: 0.2,
saturation_boost: 0.35,
tint_alpha_multiplier: Some(0.25),
..Default::default()
},
);
let RenderEffect::Chain { first, .. } = &effect else {
panic!("macroscopic frost must precede the wcKSRD optical pass");
};
assert!(matches!(
first.as_ref(),
RenderEffect::Blur {
radius_x: 4.0,
radius_y: 4.0,
..
}
));
let shader = terminal_shader(effect);
assert_eq!(shader.uniforms()[9], 0.72);
assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
assert_eq!(
shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
0.35
);
assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
assert_eq!(
shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
WCKSRD_OPTICAL_BLUR_RADIUS_PX
);
assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
}
#[test]
fn raised_tint_density_stays_premultiplied_alpha_safe() {
let effect = Glass::lens()
.tint(Color::WHITE.with_alpha(0.8))
.resolve(&light_colors())
.backdrop_effect(
1.0,
GlassDynamics {
tint_alpha_multiplier: Some(2.0),
..Default::default()
},
);
assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
}
#[test]
fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
let resolved = Glass::lens()
.refraction_depth(0.72)
.refraction_curve(0.8)
.dispersion(0.42)
.blur_radius(3.0)
.saturation(1.4)
.highlight(0.6)
.lift(0.2)
.tint(Color::BLACK.with_alpha(0.8))
.no_clip()
.resolve(&light_colors());
let morph = GlassMorph {
node_size: (120.0, 72.0),
primary: (60.0, 36.0, 96.0, 52.0, -1.0),
wobble_amplitude: 3.0,
bulge_amplitude: 4.0,
deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
..Default::default()
};
let RenderEffect::Shader { shader } = resolved.backdrop_effect(
2.0,
GlassDynamics {
activity: Some(0.0),
morph: Some(morph),
..Default::default()
},
) else {
panic!("material must resolve to the shared wcKSRD shader");
};
let uniforms = shader.uniforms();
assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
assert_eq!(uniforms[9], 0.0);
assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
assert_eq!(uniforms[11], 0.0);
assert_eq!(uniforms[17], 0.0);
assert_eq!(uniforms[18], 1.0);
assert_eq!(uniforms[20], 0.0);
assert_eq!(uniforms[21], 0.0);
assert_eq!(uniforms[24], 1.0);
assert_eq!(uniforms[28], 0.0);
assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
assert_eq!(uniforms[91], 0.0);
assert_eq!(uniforms[102], 0.0);
assert_eq!(
&uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
&[0.0, 0.0, 0.0, 0.0]
);
assert_eq!(uniforms[32], 0.0);
assert_eq!(uniforms[26], 0.0);
assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
}
#[test]
fn resting_surface_tint_survives_zero_optical_activity() {
let tint = Color::BLACK.with_alpha(0.11);
let RenderEffect::Shader { shader } = Glass::lens()
.no_clip()
.resolve(&light_colors())
.backdrop_effect(
1.0,
GlassDynamics {
activity: Some(0.0),
resting_tint: Some(tint),
..Default::default()
},
)
else {
panic!("resting surface must use the shared wcKSRD shader");
};
assert_eq!(
&shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
&[tint.r(), tint.g(), tint.b(), tint.a()]
);
assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
}
#[test]
fn full_optical_activity_preserves_the_resolved_material() {
let resolved = Glass::lens()
.refraction_depth(0.72)
.refraction_curve(0.8)
.dispersion(0.42)
.blur_radius(3.0)
.resolve(&light_colors());
let shader = terminal_shader(resolved.backdrop_effect(
2.0,
GlassDynamics {
activity: Some(1.0),
..Default::default()
},
));
let uniforms = shader.uniforms();
assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
assert_eq!(uniforms[9], 0.72);
assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
assert_eq!(
uniforms[GLASS_BLUR_RADIUS_UNIFORM],
WCKSRD_OPTICAL_BLUR_RADIUS_PX
);
}
#[test]
fn morph_geometry_and_incompressible_strain_are_packed() {
let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
let morph = GlassMorph {
node_size: (78.0, 59.0),
primary: (39.0, 29.5, 58.0, 39.0, -1.0),
shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
glue: 8.0,
wobble_amplitude: 1.0,
wobble_phase: 0.5,
bulge_amplitude: 2.0,
bulge_direction: 0.25,
ellipse_blend: 0.3,
deformation: Some(deformation),
};
let RenderEffect::Shader { shader } = Glass::lens()
.no_clip()
.resolve(&light_colors())
.backdrop_effect(
1.0,
GlassDynamics {
morph: Some(morph),
..Default::default()
},
)
else {
panic!("morph must use the shared wcKSRD shader");
};
let uniforms = shader.uniforms();
assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
assert_eq!(uniforms[30], 1.0);
assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
assert_eq!(uniforms[110], 0.3);
assert!(shader.output_padding() > 0.0);
}
}