pub type ShadowLayer = [f32; 4];
pub const SHADOW_ANCHORS: [(f32, [ShadowLayer; 2]); 6] = [
(0.0, [[0.0; 4], [0.0; 4]]),
(2.0, [[1.0, 2.0, 0.0, 0.05], [0.0; 4]]),
(4.0, [[1.0, 3.0, 0.0, 0.10], [1.0, 2.0, -1.0, 0.10]]),
(12.0, [[4.0, 6.0, -1.0, 0.10], [2.0, 4.0, -2.0, 0.10]]),
(24.0, [[10.0, 15.0, -3.0, 0.10], [4.0, 6.0, -4.0, 0.10]]),
(40.0, [[20.0, 25.0, -5.0, 0.10], [8.0, 10.0, -6.0, 0.10]]),
];
pub fn shadow_recipe(level: f32) -> [ShadowLayer; 2] {
if level <= 0.0 {
return [[0.0; 4], [0.0; 4]];
}
let (top_level, top_layers) = SHADOW_ANCHORS[SHADOW_ANCHORS.len() - 1];
if level >= top_level {
let s = level / top_level;
return top_layers.map(|l| [l[0] * s, l[1] * s, l[2] * s, l[3]]);
}
let mut lo = SHADOW_ANCHORS[0];
for hi in SHADOW_ANCHORS[1..].iter() {
if level <= hi.0 {
let t = (level - lo.0) / (hi.0 - lo.0);
let lerp = |a: f32, b: f32| a + (b - a) * t;
return [
std::array::from_fn(|i| lerp(lo.1[0][i], hi.1[0][i])),
std::array::from_fn(|i| lerp(lo.1[1][i], hi.1[1][i])),
];
}
lo = *hi;
}
unreachable!("level < top anchor is always bracketed");
}
pub fn shadow_extents(level: f32) -> (f32, f32, f32) {
let layers = shadow_recipe(level);
let mut sides = 0.0_f32;
let mut top = 0.0_f32;
let mut bottom = 0.0_f32;
for l in layers {
if l[3] <= 0.0 {
continue;
}
let reach = l[1] + l[2]; sides = sides.max(reach);
top = top.max(reach - l[0]);
bottom = bottom.max(reach + l[0]);
}
(sides.max(0.0), top.max(0.0), bottom.max(0.0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tokens;
#[test]
fn anchors_reproduce_tailwind_recipes_exactly() {
assert_eq!(
shadow_recipe(tokens::SHADOW_XS),
[[1.0, 2.0, 0.0, 0.05], [0.0; 4]]
);
assert_eq!(
shadow_recipe(tokens::SHADOW_SM),
[[1.0, 3.0, 0.0, 0.10], [1.0, 2.0, -1.0, 0.10]]
);
assert_eq!(
shadow_recipe(tokens::SHADOW_MD),
[[4.0, 6.0, -1.0, 0.10], [2.0, 4.0, -2.0, 0.10]]
);
assert_eq!(
shadow_recipe(tokens::SHADOW_LG),
[[10.0, 15.0, -3.0, 0.10], [4.0, 6.0, -4.0, 0.10]]
);
}
#[test]
fn zero_and_negative_levels_have_no_shadow() {
assert_eq!(shadow_recipe(0.0), [[0.0; 4], [0.0; 4]]);
assert_eq!(shadow_recipe(-3.0), [[0.0; 4], [0.0; 4]]);
assert_eq!(shadow_extents(0.0), (0.0, 0.0, 0.0));
}
#[test]
fn interpolation_is_smooth_and_monotone() {
let a = shadow_recipe(5.0);
let b = shadow_recipe(11.0);
assert!(a[0][1] < b[0][1], "blur grows with level");
assert!(a[0][0] < b[0][0], "dy grows with level");
let xl = shadow_recipe(40.0);
let xxl = shadow_recipe(80.0);
assert_eq!(xxl[0][1], xl[0][1] * 2.0);
assert_eq!(xxl[0][3], xl[0][3]);
}
#[test]
fn extents_cover_the_softened_silhouette() {
assert_eq!(shadow_extents(tokens::SHADOW_LG), (12.0, 2.0, 22.0));
assert_eq!(shadow_extents(tokens::SHADOW_XS), (2.0, 1.0, 3.0));
}
#[test]
fn wgsl_table_matches() {
let wgsl = include_str!("../../shaders/rounded_rect.wgsl");
for (level, layers) in SHADOW_ANCHORS[1..].iter() {
for (li, l) in layers.iter().enumerate() {
if l[3] <= 0.0 {
continue;
}
let row = format!("vec4<f32>({:?}, {:?}, {:?}, {:?})", l[0], l[1], l[2], l[3]);
assert!(
wgsl.contains(&row),
"rounded_rect.wgsl is missing anchor level {level} layer {li}: {row}"
);
}
}
}
}