use std::sync::Arc;
use bevy::asset::load_embedded_asset;
use bevy::prelude::*;
use bevy::shader::Shader;
use serde::Deserialize;
use serde_json::Value;
use crate::animations::ValueKind;
use crate::filters::builtin::BlurParams;
use crate::filters::params::{FilterColor, ParamSlot, length_logical_px, static_layout};
use crate::filters::registry::{ReactFilter, ResolvedFilterPass};
use crate::protocol::units::Length;
const MODE_PREP: f32 = 0.0;
const MODE_COMBINE: f32 = 1.0;
fn default_offset_y() -> Length {
Length::Px(4.0)
}
fn default_spread() -> Length {
Length::Px(6.0)
}
fn default_shadow_color() -> FilterColor {
FilterColor([0.0, 0.0, 0.0, 0.6])
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, ts_rs::TS)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ShadowParams {
#[serde(default = "default_shadow_color")]
pub color: FilterColor,
#[serde(default)]
#[ts(type = "number | string")]
pub offset_x: Length,
#[serde(default = "default_offset_y")]
#[ts(type = "number | string")]
pub offset_y: Length,
#[serde(default = "default_spread")]
#[ts(type = "number | string")]
pub spread: Length,
}
impl Default for ShadowParams {
fn default() -> Self {
Self {
color: default_shadow_color(),
offset_x: Length::Px(0.0),
offset_y: default_offset_y(),
spread: default_spread(),
}
}
}
fn shadow_layout() -> Arc<[ParamSlot]> {
static_layout![
ParamSlot {
name: "spread",
kind: ValueKind::Length,
vec: 0,
comp: 0,
len: 1,
},
ParamSlot {
name: "offsetX",
kind: ValueKind::Length,
vec: 1,
comp: 0,
len: 1,
},
ParamSlot {
name: "offsetY",
kind: ValueKind::Length,
vec: 1,
comp: 1,
len: 1,
},
ParamSlot {
name: "color",
kind: ValueKind::Color,
vec: 2,
comp: 0,
len: 4,
},
]
}
impl ShadowParams {
fn lengths_px(&self) -> Result<(f32, f32, f32), String> {
Ok((
length_logical_px(Self::NAME, "offsetX", self.offset_x)?,
length_logical_px(Self::NAME, "offsetY", self.offset_y)?,
length_logical_px(Self::NAME, "spread", self.spread)?,
))
}
}
impl ReactFilter for ShadowParams {
const NAME: &'static str = "shadow";
fn shader(assets: &AssetServer) -> Handle<Shader> {
load_embedded_asset!(assets, "shadow.wgsl")
}
fn identity_params() -> Option<Value> {
Some(serde_json::json!({ "color": "transparent" }))
}
fn outset(&self) -> Result<f32, String> {
let (offset_x, offset_y, spread) = self.lengths_px()?;
Ok(offset_x.abs().max(offset_y.abs()) + 3.0 * spread)
}
fn pack(&self) -> (Vec<Vec4>, Arc<[ParamSlot]>) {
let (offset_x, offset_y, spread) = self.lengths_px().unwrap_or((0.0, 0.0, 0.0));
(
vec![
Vec4::new(spread, 0.0, 0.0, MODE_PREP),
Vec4::new(offset_x, offset_y, 0.0, 0.0),
Vec4::from_array(self.color.0),
],
shadow_layout(),
)
}
fn resolve(&self, assets: &AssetServer) -> Result<Vec<ResolvedFilterPass>, String> {
let (offset_x, offset_y, spread) = self.lengths_px()?;
let shadow_shader = Self::shader(assets);
let blur_shader = BlurParams::shader(assets);
let offsets = Vec4::new(offset_x, offset_y, 0.0, 0.0);
let color = Vec4::from_array(self.color.0);
let pass = |shader: &Handle<Shader>, internal: (f32, f32, f32)| ResolvedFilterPass {
shader: shader.clone(),
params: vec![
Vec4::new(spread, internal.0, internal.1, internal.2),
offsets,
color,
],
layout: shadow_layout(),
wire_index: 0,
};
Ok(vec![
pass(&shadow_shader, (0.0, 0.0, MODE_PREP)),
pass(&blur_shader, (1.0, 0.0, 0.0)),
pass(&blur_shader, (0.0, 1.0, 0.0)),
pass(&shadow_shader, (0.0, 0.0, MODE_COMBINE)),
])
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::filters::registry::FilterRegistry;
use crate::filters::test_util::{asset_app, params};
#[test]
fn shadow_resolves_to_four_passes() {
let app = asset_app();
let assets = app.world().resource::<AssetServer>();
let passes = params::<ShadowParams>(json!({
"offsetX": 3, "offsetY": -2, "spread": 5, "color": "#ff0000",
}))
.resolve(assets)
.expect("shadow resolves");
assert_eq!(passes.len(), 4);
let offsets = Vec4::new(3.0, -2.0, 0.0, 0.0);
let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
assert_eq!(
passes[0].params,
vec![Vec4::new(5.0, 0.0, 0.0, 0.0), offsets, color]
);
assert_eq!(
passes[1].params,
vec![Vec4::new(5.0, 1.0, 0.0, 0.0), offsets, color]
);
assert_eq!(
passes[2].params,
vec![Vec4::new(5.0, 0.0, 1.0, 0.0), offsets, color]
);
assert_eq!(
passes[3].params,
vec![Vec4::new(5.0, 0.0, 0.0, 1.0), offsets, color]
);
assert!(passes.iter().all(|p| p.wire_index == 0));
assert_eq!(passes[0].layout[0].name, "spread");
assert_eq!(passes[0].layout[0].kind, ValueKind::Length);
assert_eq!(passes[0].layout[3].kind, ValueKind::Color);
assert_eq!(passes[0].shader, passes[3].shader);
assert_eq!(passes[1].shader, passes[2].shader);
assert_eq!(passes[1].shader, BlurParams::shader(assets));
assert_ne!(passes[0].shader, passes[1].shader);
}
#[test]
fn shadow_outset_is_offset_plus_three_spreads() {
assert_eq!(
params::<ShadowParams>(json!({ "offsetX": -8, "offsetY": 2, "spread": 4 })).outset(),
Ok(20.0)
);
assert_eq!(params::<ShadowParams>(json!({})).outset(), Ok(22.0));
}
#[test]
fn shadow_empty_params_default_to_a_soft_shadow() {
let p = params::<ShadowParams>(json!({}));
assert_eq!(p, ShadowParams::default());
assert_eq!(p.offset_x, Length::Px(0.0));
assert_eq!(p.offset_y, Length::Px(4.0));
assert_eq!(p.spread, Length::Px(6.0));
assert_eq!(p.color.0, [0.0, 0.0, 0.0, 0.6]);
}
#[test]
fn identity_is_a_transparent_shadow() {
let identity = ShadowParams::identity_params().expect("has identity");
let p: ShadowParams = serde_json::from_value(identity).expect("identity decodes");
assert_eq!(p.color.0, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(p.pack().0[2], Vec4::ZERO);
}
#[test]
fn non_px_lengths_reject_from_registry() {
let app = asset_app();
let assets = app.world().resource::<AssetServer>();
let mut registry = FilterRegistry::default();
registry.register::<ShadowParams>();
let entry = ®istry.entries["shadow"];
for value in [
json!({ "offsetX": "50%" }),
json!({ "offsetY": "2vw" }),
json!({ "spread": "1vh" }),
] {
let err = (entry.resolve)(&value, assets).expect_err("non-px must reject resolve");
assert!(err.contains("px"), "names the unit: {err}");
let err = (entry.outset)(&value).expect_err("non-px must reject outset");
assert!(err.contains("px"), "names the unit: {err}");
}
assert!((entry.resolve)(&json!({ "offsetX": -6, "spread": "3px" }), assets).is_ok());
assert_eq!(
(entry.outset)(&json!({ "offsetY": -10, "spread": 2 })),
Ok(16.0)
);
}
#[test]
fn unknown_shadow_param_rejects() {
assert!(serde_json::from_value::<ShadowParams>(json!({ "offsetx": 2 })).is_err());
}
}