use crate::Rect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SdfShadowParams {
pub light_dir: [f32; 2],
pub shadow_color: [f32; 4],
pub max_distance: f32,
pub step_count: u32,
pub softness: f32,
pub offset: [f32; 2],
pub opacity: f32,
}
impl SdfShadowParams {
pub fn downward() -> Self {
Self {
light_dir: [0.0, 1.0],
shadow_color: [0.0, 0.0, 0.0, 0.35],
max_distance: 64.0,
step_count: 32,
softness: 4.0,
offset: [0.0, 2.0],
opacity: 1.0,
}
}
pub fn upward() -> Self {
Self {
light_dir: [0.0, -1.0],
shadow_color: [0.0, 0.0, 0.0, 0.35],
max_distance: 64.0,
step_count: 32,
softness: 4.0,
offset: [0.0, -2.0],
opacity: 1.0,
}
}
pub fn with_angle(angle: f32) -> Self {
Self {
light_dir: [angle.sin(), angle.cos()],
..Self::downward()
}
}
pub fn with_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.shadow_color = [r, g, b, a];
self
}
pub fn with_max_distance(mut self, d: f32) -> Self {
self.max_distance = d;
self
}
pub fn with_steps(mut self, n: u32) -> Self {
self.step_count = n;
self
}
pub fn with_softness(mut self, s: f32) -> Self {
self.softness = s;
self
}
pub fn with_offset(mut self, dx: f32, dy: f32) -> Self {
self.offset = [dx, dy];
self
}
pub fn with_opacity(mut self, o: f32) -> Self {
self.opacity = o.clamp(0.0, 1.0);
self
}
}
impl Default for SdfShadowParams {
fn default() -> Self {
Self::downward()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SdfShape {
Rect {
x: f32,
y: f32,
width: f32,
height: f32,
},
RoundedRect {
x: f32,
y: f32,
width: f32,
height: f32,
radius: f32,
},
Circle { cx: f32, cy: f32, radius: f32 },
}
impl SdfShape {
pub fn bounds(&self) -> Rect {
match *self {
SdfShape::Rect {
x,
y,
width,
height,
} => Rect::new(x, y, width, height),
SdfShape::RoundedRect {
x,
y,
width,
height,
..
} => Rect::new(x, y, width, height),
SdfShape::Circle { cx, cy, radius } => {
Rect::new(cx - radius, cy - radius, radius * 2.0, radius * 2.0)
}
}
}
pub fn from_rect(rect: Rect, radius: f32) -> Self {
SdfShape::RoundedRect {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
radius,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShadowInstance {
pub shape: SdfShape,
pub params: SdfShadowParams,
}
impl ShadowInstance {
pub fn new(shape: SdfShape, params: SdfShadowParams) -> Self {
Self { shape, params }
}
pub fn rect_shadow(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
shape: SdfShape::Rect {
x,
y,
width,
height,
},
params: SdfShadowParams::downward(),
}
}
pub fn rounded_rect_shadow(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Self {
Self {
shape: SdfShape::RoundedRect {
x,
y,
width,
height,
radius,
},
params: SdfShadowParams::downward(),
}
}
pub fn circle_shadow(cx: f32, cy: f32, radius: f32) -> Self {
Self {
shape: SdfShape::Circle { cx, cy, radius },
params: SdfShadowParams::downward(),
}
}
pub fn shadow_bounds(&self) -> Rect {
let bounds = self.shape.bounds();
let d = self.params.max_distance;
let ox = self.params.offset[0];
let oy = self.params.offset[1];
Rect::new(
bounds.x + ox - d,
bounds.y + oy - d,
bounds.width + d * 2.0,
bounds.height + d * 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShadowBatch {
pub instances: Vec<ShadowInstance>,
}
impl ShadowBatch {
pub fn new() -> Self {
Self {
instances: Vec::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
instances: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, instance: ShadowInstance) {
self.instances.push(instance);
}
pub fn len(&self) -> usize {
self.instances.len()
}
pub fn is_empty(&self) -> bool {
self.instances.is_empty()
}
pub fn clear(&mut self) {
self.instances.clear();
}
pub fn total_bounds(&self) -> Option<Rect> {
if self.instances.is_empty() {
return None;
}
let first = self.instances[0].shadow_bounds();
let mut min_x = first.x;
let mut min_y = first.y;
let mut max_x = first.x + first.width;
let mut max_y = first.y + first.height;
for inst in &self.instances[1..] {
let b = inst.shadow_bounds();
min_x = min_x.min(b.x);
min_y = min_y.min(b.y);
max_x = max_x.max(b.x + b.width);
max_y = max_y.max(b.y + b.height);
}
Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
}
}
impl Default for ShadowBatch {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdf_shadow_params_default() {
let p = SdfShadowParams::default();
assert_eq!(p.light_dir, [0.0, 1.0]);
assert_eq!(p.step_count, 32);
}
#[test]
fn sdf_shadow_params_with_angle() {
let p = SdfShadowParams::with_angle(std::f32::consts::PI / 4.0);
assert!((p.light_dir[0] - std::f32::consts::PI / 4.0.sin()).abs() < 0.001);
assert!((p.light_dir[1] - std::f32::consts::PI / 4.0.cos()).abs() < 0.001);
}
#[test]
fn sdf_shape_bounds() {
let rect = SdfShape::Rect {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
};
let b = rect.bounds();
assert_eq!(b.x, 10.0);
assert_eq!(b.y, 20.0);
assert_eq!(b.width, 100.0);
assert_eq!(b.height, 50.0);
}
#[test]
fn sdf_shape_circle_bounds() {
let circle = SdfShape::Circle {
cx: 50.0,
cy: 50.0,
radius: 20.0,
};
let b = circle.bounds();
assert_eq!(b.x, 30.0);
assert_eq!(b.y, 30.0);
assert_eq!(b.width, 40.0);
assert_eq!(b.height, 40.0);
}
#[test]
fn shadow_instance_bounds() {
let inst = ShadowInstance::rect_shadow(0.0, 0.0, 100.0, 50.0);
let b = inst.shadow_bounds();
assert!(b.x < 0.0);
assert!(b.y < 0.0);
assert!(b.width > 100.0);
assert!(b.height > 50.0);
}
#[test]
fn shadow_batch_total_bounds() {
let mut batch = ShadowBatch::new();
assert!(batch.total_bounds().is_none());
batch.push(ShadowInstance::rect_shadow(0.0, 0.0, 100.0, 50.0));
batch.push(ShadowInstance::rect_shadow(200.0, 200.0, 50.0, 50.0));
let bounds = batch.total_bounds().unwrap();
assert!(bounds.x <= 0.0);
assert!(bounds.y <= 0.0);
assert!(bounds.width >= 250.0);
assert!(bounds.height >= 250.0);
}
#[test]
fn shadow_batch_clear() {
let mut batch = ShadowBatch::new();
batch.push(ShadowInstance::rect_shadow(0.0, 0.0, 10.0, 10.0));
assert_eq!(batch.len(), 1);
batch.clear();
assert!(batch.is_empty());
}
}