use bevy::math::Vec4;
use serde::{Deserialize, Deserializer};
use crate::animations::ValueKind;
use crate::protocol::units::Length;
pub const MAX_FILTER_PARAM_VECS: usize = 8;
pub const MAX_FILTER_OUTSET_PX: u32 = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParamSlot {
pub name: &'static str,
pub kind: ValueKind,
pub vec: usize,
pub comp: usize,
pub len: usize,
}
macro_rules! static_layout {
($($slot:expr),+ $(,)?) => {{
static LAYOUT: ::std::sync::LazyLock<
::std::sync::Arc<[crate::filters::ParamSlot]>,
> = ::std::sync::LazyLock::new(|| ::std::sync::Arc::from(vec![$($slot),+]));
::std::sync::Arc::clone(&LAYOUT)
}};
}
pub(crate) use static_layout;
pub(super) fn check_param_cap(name: &str, vecs: usize) -> Result<(), String> {
if vecs > MAX_FILTER_PARAM_VECS {
return Err(format!(
"filter {name:?} packs {vecs} param vec4s, over the cap of {MAX_FILTER_PARAM_VECS}"
));
}
Ok(())
}
pub fn length_logical_px(filter: &str, param: &str, len: Length) -> Result<f32, String> {
let unit = match len {
Length::Px(px) => return Ok(px),
Length::Auto => "auto",
Length::Percent(_) => "%",
Length::Vw(_) => "vw",
Length::Vh(_) => "vh",
Length::VMin(_) => "vmin",
Length::VMax(_) => "vmax",
};
Err(format!(
"filter {filter:?} {param} must be in px (a bare number or \"px\"), got a {unit:?} length"
))
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct FilterColor(pub [f32; 4]);
impl<'de> Deserialize<'de> for FilterColor {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
let srgba = crate::canvas::parse_css_color(&s)
.ok_or_else(|| serde::de::Error::custom(format!("invalid color {s:?}")))?;
let lin = bevy::color::LinearRgba::from(srgba);
Ok(Self([lin.red, lin.green, lin.blue, lin.alpha]))
}
}
impl ::ts_rs::TS for FilterColor {
type WithoutGenerics = Self;
fn name() -> String {
"string".to_owned()
}
fn inline() -> String {
Self::name()
}
fn inline_flattened() -> String {
panic!("FilterColor cannot be flattened")
}
fn decl() -> String {
panic!("FilterColor cannot be declared")
}
fn decl_concrete() -> String {
panic!("FilterColor cannot be declared")
}
}
pub fn lerp_angle(a: f32, b: f32, t: f32) -> f32 {
use std::f32::consts::{PI, TAU};
if t == 0.0 {
return a;
}
if t == 1.0 {
return b;
}
let mut delta = (b - a).rem_euclid(TAU);
if delta > PI {
delta -= TAU;
}
a + delta * t
}
pub fn lerp_packed_params(a: &[Vec4], b: &[Vec4], t: f32, layout: &[ParamSlot]) -> Vec<Vec4> {
debug_assert_eq!(
a.len(),
b.len(),
"lerp_packed_params: a/b length mismatch (the caller guarantees one shared layout)"
);
if a.len() != b.len() {
return b.to_vec();
}
if t == 0.0 {
return a.to_vec();
}
if t == 1.0 {
return b.to_vec();
}
let mut out = b.to_vec();
for slot in layout {
let Some((av, bv)) = a.get(slot.vec).zip(b.get(slot.vec)) else {
continue;
};
for comp in slot.comp..(slot.comp + slot.len).min(4) {
out[slot.vec][comp] = match slot.kind {
ValueKind::Angle => lerp_angle(av[comp], bv[comp], t),
_ => av[comp] + (bv[comp] - av[comp]) * t,
};
}
}
out
}
#[cfg(test)]
mod tests {
use std::f32::consts::PI;
use serde_json::json;
use super::*;
#[test]
fn lerp_angle_within_half_circle_matches_plain_lerp() {
let (a, b) = (0.2f32, 1.7f32); for t in [0.25f32, 0.5, 0.75] {
let plain = a + (b - a) * t;
assert!(
(lerp_angle(a, b, t) - plain).abs() < 1e-6,
"t={t}: {} vs {plain}",
lerp_angle(a, b, t)
);
}
assert!((lerp_angle(1.0, -1.0, 0.5)).abs() < 1e-6);
}
#[test]
fn lerp_angle_crosses_seam_the_short_way() {
let a = 170f32.to_radians();
let b = (-170f32).to_radians();
let mid = lerp_angle(a, b, 0.5);
assert!((mid.abs() - PI).abs() < 1e-5, "mid = {mid}");
assert!((lerp_angle(a, b, 0.25) - 175f32.to_radians()).abs() < 1e-5);
assert!(mid.abs() > 3.0);
}
#[test]
fn lerp_angle_endpoints_exact() {
let a = 0.1f32 + 0.7f32; let b = -3.041_7f32;
assert_eq!(lerp_angle(a, b, 0.0), a);
assert_eq!(lerp_angle(a, b, 1.0), b);
let (sa, sb) = (170f32.to_radians(), (-170f32).to_radians());
assert_eq!(lerp_angle(sa, sb, 0.0), sa);
assert_eq!(lerp_angle(sa, sb, 1.0), sb);
}
#[test]
fn lerp_angle_symmetric_in_its_arguments() {
use std::f32::consts::TAU;
let cases = [
(0.4f32, 2.9f32),
(170f32.to_radians(), (-170f32).to_radians()),
(-0.3f32, 0.9f32),
];
for (a, b) in cases {
for t in [0.25f32, 0.5, 0.75] {
let fwd = lerp_angle(a, b, t);
let rev = lerp_angle(b, a, 1.0 - t);
let diff = (fwd - rev).rem_euclid(TAU);
let dist = diff.min(TAU - diff);
assert!(dist < 1e-5, "a={a} b={b} t={t}: {fwd} vs {rev}");
}
}
}
#[test]
fn lerp_packed_params_lerps_each_slot_by_kind() {
let layout = [
ParamSlot {
name: "amount",
kind: ValueKind::Scalar,
vec: 0,
comp: 0,
len: 1,
},
ParamSlot {
name: "angle",
kind: ValueKind::Angle,
vec: 0,
comp: 1,
len: 1,
},
];
let a = [Vec4::new(0.0, 170f32.to_radians(), 0.0, 0.0)];
let b = [Vec4::new(10.0, (-170f32).to_radians(), 0.0, 0.0)];
let out = lerp_packed_params(&a, &b, 0.5, &layout);
assert_eq!(out.len(), 1);
assert_eq!(out[0].x, 5.0);
assert!((out[0].y.abs() - PI).abs() < 1e-5, "angle = {}", out[0].y);
}
#[test]
fn lerp_packed_params_color_slot_interpolates_linearly() {
let layout = [ParamSlot {
name: "color",
kind: ValueKind::Color,
vec: 0,
comp: 0,
len: 4,
}];
let a = [Vec4::new(0.0, 0.0, 0.0, 1.0)];
let b = [Vec4::new(1.0, 1.0, 1.0, 1.0)];
let mid = lerp_packed_params(&a, &b, 0.5, &layout)[0];
assert_eq!(mid, Vec4::new(0.5, 0.5, 0.5, 1.0));
assert!((mid.x - 0.214).abs() > 0.2, "must not be the sRGB midpoint");
}
#[test]
fn lerp_packed_params_length_slot_lerps() {
let layout = [ParamSlot {
name: "radius",
kind: ValueKind::Length,
vec: 0,
comp: 0,
len: 1,
}];
let a = [Vec4::new(4.0, 1.0, 0.0, 0.0)];
let b = [Vec4::new(8.0, 1.0, 0.0, 0.0)];
assert_eq!(lerp_packed_params(&a, &b, 0.25, &layout)[0].x, 5.0);
}
#[test]
fn lerp_packed_params_padding_copies_from_b() {
let layout = [ParamSlot {
name: "amount",
kind: ValueKind::Scalar,
vec: 0,
comp: 0,
len: 1,
}];
let a = [Vec4::new(0.0, 111.0, 0.0, 0.0), Vec4::splat(5.0)];
let b = [Vec4::new(2.0, 222.0, 0.0, 0.0), Vec4::splat(7.0)];
let out = lerp_packed_params(&a, &b, 0.5, &layout);
assert_eq!(out[0], Vec4::new(1.0, 222.0, 0.0, 0.0));
assert_eq!(out[1], Vec4::splat(7.0));
}
#[test]
fn lerp_packed_params_endpoints_exact() {
let layout = [ParamSlot {
name: "stuff",
kind: ValueKind::Scalar,
vec: 0,
comp: 0,
len: 4,
}];
let a = [Vec4::new(0.1f32 + 0.7f32, 1e-7, -3.333_333_3, 0.3)];
let b = [Vec4::new(0.2f32 + 0.1f32, 123_456.79, 2.718_281_7, -0.1)];
assert_eq!(lerp_packed_params(&a, &b, 0.0, &layout), a.to_vec());
assert_eq!(lerp_packed_params(&a, &b, 1.0, &layout), b.to_vec());
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "length mismatch")]
fn lerp_packed_params_mismatched_lengths_asserts_in_debug() {
let _ = lerp_packed_params(&[Vec4::ZERO], &[], 0.5, &[]);
}
#[cfg(not(debug_assertions))]
#[test]
fn lerp_packed_params_mismatched_lengths_returns_b() {
let b = [Vec4::splat(3.0)];
assert_eq!(lerp_packed_params(&[], &b, 0.5, &[]), b.to_vec());
}
fn from<T: serde::de::DeserializeOwned>(value: serde_json::Value) -> T {
serde_json::from_value(value).expect("params decode")
}
#[test]
fn filter_color_parses_css_strings_to_linear_rgba() {
let c: FilterColor = from(json!("#ff0000"));
assert_eq!(c.0, [1.0, 0.0, 0.0, 1.0]);
let c: FilterColor = from(json!("rgb(255 0 0)"));
assert_eq!(c.0, [1.0, 0.0, 0.0, 1.0]);
let c: FilterColor = from(json!("#808080"));
assert!((c.0[0] - 0.2158).abs() < 1e-3, "linear gray: {:?}", c.0);
assert_eq!(c.0[3], 1.0);
}
#[test]
fn filter_color_garbage_is_a_hard_error() {
assert!(serde_json::from_value::<FilterColor>(json!("notacolor")).is_err());
assert!(serde_json::from_value::<FilterColor>(json!(42)).is_err());
}
}