use crate::fmt::num;
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct Props {
values: Vec<(&'static str, f32)>,
}
pub(crate) const PROP_NAMES: &[&str] = &[
"x",
"y",
"xPercent",
"yPercent",
"scale",
"scaleX",
"scaleY",
"rotation",
"rotationX",
"rotationY",
"skewX",
"skewY",
"opacity",
"autoAlpha",
];
macro_rules! prop_setters {
($($(#[$doc:meta])* $fn_name:ident => $gsap:literal;)*) => {
$(
$(#[$doc])*
pub fn $fn_name(self, value: f32) -> Self {
self.set($gsap, value)
}
)*
};
}
impl Props {
pub const fn new() -> Self {
Self { values: Vec::new() }
}
fn set(mut self, name: &'static str, value: f32) -> Self {
if !value.is_finite() {
self.values.retain(|(n, _)| *n != name);
} else if let Some(slot) = self.values.iter_mut().find(|(n, _)| *n == name) {
slot.1 = value;
} else {
self.values.push((name, value));
}
self
}
prop_setters! {
x => "x";
y => "y";
x_percent => "xPercent";
y_percent => "yPercent";
scale => "scale";
scale_x => "scaleX";
scale_y => "scaleY";
rotation => "rotation";
rotation_x => "rotationX";
rotation_y => "rotationY";
skew_x => "skewX";
skew_y => "skewY";
opacity => "opacity";
auto_alpha => "autoAlpha";
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[must_use]
pub fn to_json(&self) -> String {
let mut values = self.values.clone();
values.sort_by_key(|(name, _)| PROP_NAMES.iter().position(|n| n == name));
let body: Vec<String> = values
.iter()
.filter_map(|(name, value)| num(*value).map(|v| format!(r#""{name}":{v}"#)))
.collect();
format!("{{{}}}", body.join(","))
}
}
impl PartialEq for Props {
fn eq(&self, other: &Self) -> bool {
self.values.len() == other.values.len()
&& self.values.iter().all(|v| other.values.contains(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn every_setter_writes_its_gsap_name() {
let p = Props::new()
.x(1.0)
.y(2.0)
.x_percent(3.0)
.y_percent(4.0)
.scale(5.0)
.scale_x(6.0)
.scale_y(7.0)
.rotation(8.0)
.rotation_x(9.0)
.rotation_y(10.0)
.skew_x(11.0)
.skew_y(12.0)
.opacity(0.5)
.auto_alpha(0.25);
assert_eq!(
p.to_json(),
concat!(
r#"{"x":1,"y":2,"xPercent":3,"yPercent":4,"scale":5,"scaleX":6,"#,
r#""scaleY":7,"rotation":8,"rotationX":9,"rotationY":10,"skewX":11,"#,
r#""skewY":12,"opacity":0.5,"autoAlpha":0.25}"#
)
);
}
#[test]
fn init_js_accepts_the_same_names() {
let js = include_str!("../assets/init.js");
let start = js.find("var PROPS = [").expect("PROPS list");
let list = &js[start..start + js[start..].find("];").expect("end")];
let names: Vec<&str> = list.split('"').skip(1).step_by(2).collect();
assert_eq!(names, PROP_NAMES);
}
#[test]
fn equality_ignores_the_order() {
assert_eq!(Props::new().x(1.0).y(2.0), Props::new().y(2.0).x(1.0));
assert_eq!(
Props::new().opacity(0.0).x(1.0).to_json(),
Props::new().x(1.0).opacity(0.0).to_json(),
"equal sets write equal JSON"
);
assert_ne!(Props::new().x(1.0), Props::new().x(2.0));
assert_ne!(Props::new().x(1.0), Props::new().x(1.0).y(0.0));
}
#[test]
fn a_later_value_replaces_an_earlier_one() {
let p = Props::new().x(1.0).opacity(0.0).x(2.0);
assert_eq!(p.to_json(), r#"{"x":2,"opacity":0}"#);
}
#[test]
fn values_that_are_not_finite_are_not_written() {
let p = Props::new().x(f32::NAN).y(f32::INFINITY);
assert!(p.is_empty());
assert_eq!(p.to_json(), "{}");
let q = Props::new().x(1.0).x(f32::NAN);
assert_eq!(q.to_json(), "{}", "a bad value also clears the old one");
}
#[test]
fn json_is_valid_and_uses_only_known_names() {
let p = Props::new().rotation(-12.5).auto_alpha(0.0);
let v: serde_json::Value = serde_json::from_str(&p.to_json()).expect("json");
let obj = v.as_object().expect("object");
assert_eq!(obj.len(), 2);
for key in obj.keys() {
assert!(PROP_NAMES.contains(&key.as_str()), "{key}");
}
}
proptest! {
#[test]
fn json_values_match_the_js_grammar(v in proptest::num::f32::ANY) {
let json = Props::new().x(v).to_json();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("json");
if v.is_finite() {
let raw = json.trim_start_matches(r#"{"x":"#).trim_end_matches('}');
prop_assert!(crate::grammar::is_num(raw), "{json}");
prop_assert!(parsed["x"].is_number());
} else {
prop_assert_eq!(json, "{}");
}
}
}
}