1use crate::error::{JsResult, js_error};
4use crate::types::{normalize_name, string_array};
5use animato_core::Easing;
6use js_sys::Array;
7use wasm_bindgen::prelude::*;
8
9pub const EASING_NAMES: &[&str] = &[
11 "linear",
12 "easeInQuad",
13 "easeOutQuad",
14 "easeInOutQuad",
15 "easeInCubic",
16 "easeOutCubic",
17 "easeInOutCubic",
18 "easeInQuart",
19 "easeOutQuart",
20 "easeInOutQuart",
21 "easeInQuint",
22 "easeOutQuint",
23 "easeInOutQuint",
24 "easeInSine",
25 "easeOutSine",
26 "easeInOutSine",
27 "easeInExpo",
28 "easeOutExpo",
29 "easeInOutExpo",
30 "easeInCirc",
31 "easeOutCirc",
32 "easeInOutCirc",
33 "easeInBack",
34 "easeOutBack",
35 "easeInOutBack",
36 "easeInElastic",
37 "easeOutElastic",
38 "easeInOutElastic",
39 "easeInBounce",
40 "easeOutBounce",
41 "easeInOutBounce",
42 "steps(n)",
43 "cubicBezier(x1,y1,x2,y2)",
44 "roughEase(strength,points)",
45 "slowMo(linearRatio,power)",
46 "wiggle(wiggles)",
47 "customBounce(strength)",
48 "expoScale(start,end)",
49];
50
51pub fn parse_easing(name: &str) -> JsResult<Easing> {
53 let trimmed = name.trim();
54 if let Some(args) = call_args(trimmed, "steps") {
55 let values = parse_numbers(args)?;
56 if values.len() != 1 {
57 return Err(js_error("steps() expects one numeric argument"));
58 }
59 return Ok(Easing::Steps(values[0].max(1.0).round() as u32));
60 }
61 if let Some(args) =
62 call_args(trimmed, "cubicBezier").or_else(|| call_args(trimmed, "cubic-bezier"))
63 {
64 let values = parse_numbers(args)?;
65 if values.len() != 4 {
66 return Err(js_error("cubicBezier() expects four numeric arguments"));
67 }
68 return Ok(Easing::CubicBezier(
69 values[0], values[1], values[2], values[3],
70 ));
71 }
72 if let Some(args) = call_args(trimmed, "roughEase") {
73 let values = parse_numbers(args)?;
74 if values.len() != 2 {
75 return Err(js_error("roughEase() expects strength and points"));
76 }
77 return Ok(Easing::RoughEase {
78 strength: values[0],
79 points: values[1].round().max(1.0) as u32,
80 });
81 }
82 if let Some(args) = call_args(trimmed, "slowMo") {
83 let values = parse_numbers(args)?;
84 if values.len() != 2 {
85 return Err(js_error("slowMo() expects linearRatio and power"));
86 }
87 return Ok(Easing::SlowMo {
88 linear_ratio: values[0],
89 power: values[1],
90 });
91 }
92 if let Some(args) = call_args(trimmed, "wiggle") {
93 let values = parse_numbers(args)?;
94 if values.len() != 1 {
95 return Err(js_error("wiggle() expects one numeric argument"));
96 }
97 return Ok(Easing::Wiggle {
98 wiggles: values[0].round().max(1.0) as u32,
99 });
100 }
101 if let Some(args) = call_args(trimmed, "customBounce") {
102 let values = parse_numbers(args)?;
103 if values.len() != 1 {
104 return Err(js_error("customBounce() expects one numeric argument"));
105 }
106 return Ok(Easing::CustomBounce {
107 strength: values[0],
108 });
109 }
110 if let Some(args) = call_args(trimmed, "expoScale") {
111 let values = parse_numbers(args)?;
112 if values.len() != 2 {
113 return Err(js_error("expoScale() expects start and end"));
114 }
115 return Ok(Easing::ExpoScale {
116 start: values[0],
117 end: values[1],
118 });
119 }
120
121 match normalize_name(trimmed).as_str() {
122 "linear" => Ok(Easing::Linear),
123 "easeinquad" => Ok(Easing::EaseInQuad),
124 "easeoutquad" => Ok(Easing::EaseOutQuad),
125 "easeinoutquad" => Ok(Easing::EaseInOutQuad),
126 "easeincubic" => Ok(Easing::EaseInCubic),
127 "easeoutcubic" => Ok(Easing::EaseOutCubic),
128 "easeinoutcubic" => Ok(Easing::EaseInOutCubic),
129 "easeinquart" => Ok(Easing::EaseInQuart),
130 "easeoutquart" => Ok(Easing::EaseOutQuart),
131 "easeinoutquart" => Ok(Easing::EaseInOutQuart),
132 "easeinquint" => Ok(Easing::EaseInQuint),
133 "easeoutquint" => Ok(Easing::EaseOutQuint),
134 "easeinoutquint" => Ok(Easing::EaseInOutQuint),
135 "easeinsine" => Ok(Easing::EaseInSine),
136 "easeoutsine" => Ok(Easing::EaseOutSine),
137 "easeinoutsine" => Ok(Easing::EaseInOutSine),
138 "easeinexpo" => Ok(Easing::EaseInExpo),
139 "easeoutexpo" => Ok(Easing::EaseOutExpo),
140 "easeinoutexpo" => Ok(Easing::EaseInOutExpo),
141 "easeincirc" => Ok(Easing::EaseInCirc),
142 "easeoutcirc" => Ok(Easing::EaseOutCirc),
143 "easeinoutcirc" => Ok(Easing::EaseInOutCirc),
144 "easeinback" => Ok(Easing::EaseInBack),
145 "easeoutback" => Ok(Easing::EaseOutBack),
146 "easeinoutback" => Ok(Easing::EaseInOutBack),
147 "easeinelastic" => Ok(Easing::EaseInElastic),
148 "easeoutelastic" => Ok(Easing::EaseOutElastic),
149 "easeinoutelastic" => Ok(Easing::EaseInOutElastic),
150 "easeinbounce" => Ok(Easing::EaseInBounce),
151 "easeoutbounce" => Ok(Easing::EaseOutBounce),
152 "easeinoutbounce" => Ok(Easing::EaseInOutBounce),
153 _ => Err(js_error(format!("unknown easing `{name}`"))),
154 }
155}
156
157pub fn easing_name(easing: &Easing) -> &'static str {
159 match easing {
160 Easing::Linear => "linear",
161 Easing::EaseInQuad => "easeInQuad",
162 Easing::EaseOutQuad => "easeOutQuad",
163 Easing::EaseInOutQuad => "easeInOutQuad",
164 Easing::EaseInCubic => "easeInCubic",
165 Easing::EaseOutCubic => "easeOutCubic",
166 Easing::EaseInOutCubic => "easeInOutCubic",
167 Easing::EaseInQuart => "easeInQuart",
168 Easing::EaseOutQuart => "easeOutQuart",
169 Easing::EaseInOutQuart => "easeInOutQuart",
170 Easing::EaseInQuint => "easeInQuint",
171 Easing::EaseOutQuint => "easeOutQuint",
172 Easing::EaseInOutQuint => "easeInOutQuint",
173 Easing::EaseInSine => "easeInSine",
174 Easing::EaseOutSine => "easeOutSine",
175 Easing::EaseInOutSine => "easeInOutSine",
176 Easing::EaseInExpo => "easeInExpo",
177 Easing::EaseOutExpo => "easeOutExpo",
178 Easing::EaseInOutExpo => "easeInOutExpo",
179 Easing::EaseInCirc => "easeInCirc",
180 Easing::EaseOutCirc => "easeOutCirc",
181 Easing::EaseInOutCirc => "easeInOutCirc",
182 Easing::EaseInBack => "easeInBack",
183 Easing::EaseOutBack => "easeOutBack",
184 Easing::EaseInOutBack => "easeInOutBack",
185 Easing::EaseInElastic => "easeInElastic",
186 Easing::EaseOutElastic => "easeOutElastic",
187 Easing::EaseInOutElastic => "easeInOutElastic",
188 Easing::EaseInBounce => "easeInBounce",
189 Easing::EaseOutBounce => "easeOutBounce",
190 Easing::EaseInOutBounce => "easeInOutBounce",
191 Easing::CubicBezier(..) => "cubicBezier",
192 Easing::Steps(_) => "steps",
193 Easing::RoughEase { .. } => "roughEase",
194 Easing::SlowMo { .. } => "slowMo",
195 Easing::Wiggle { .. } => "wiggle",
196 Easing::CustomBounce { .. } => "customBounce",
197 Easing::ExpoScale { .. } => "expoScale",
198 Easing::Custom(_) => "custom",
199 }
200}
201
202#[wasm_bindgen(js_name = parseEasing)]
204pub fn parse_easing_for_js(name: &str) -> Result<String, JsValue> {
205 Ok(easing_name(&parse_easing(name)?).to_owned())
206}
207
208#[wasm_bindgen]
210pub fn ease(name: &str, t: f32) -> Result<f32, JsValue> {
211 Ok(parse_easing(name)?.apply(t))
212}
213
214#[wasm_bindgen(js_name = availableEasings)]
216pub fn available_easings() -> Array {
217 string_array(EASING_NAMES)
218}
219
220fn call_args<'a>(input: &'a str, function_name: &str) -> Option<&'a str> {
221 let input_normalized = normalize_name(input);
222 let name_normalized = normalize_name(function_name);
223 if !input_normalized.starts_with(&name_normalized) {
224 return None;
225 }
226 let open = input.find('(')?;
227 let close = input.rfind(')')?;
228 if close <= open {
229 return None;
230 }
231 Some(&input[open + 1..close])
232}
233
234fn parse_numbers(input: &str) -> JsResult<Vec<f32>> {
235 if input.trim().is_empty() {
236 return Ok(Vec::new());
237 }
238 input
239 .split(',')
240 .map(|part| {
241 let value = part
242 .trim()
243 .parse::<f32>()
244 .map_err(|_| js_error(format!("invalid numeric argument `{}`", part.trim())))?;
245 if value.is_finite() {
246 Ok(value)
247 } else {
248 Err(js_error("numeric arguments must be finite"))
249 }
250 })
251 .collect()
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn parses_named_easing() {
260 assert_eq!(
261 parse_easing("ease-out-cubic").unwrap(),
262 Easing::EaseOutCubic
263 );
264 assert_eq!(
265 parse_easing("easeInOutBack").unwrap(),
266 Easing::EaseInOutBack
267 );
268 }
269
270 #[test]
271 fn parses_parameterized_easing() {
272 assert_eq!(parse_easing("steps(5)").unwrap(), Easing::Steps(5));
273 assert_eq!(
274 parse_easing("cubicBezier(0.4, 0, 0.2, 1)").unwrap(),
275 Easing::CubicBezier(0.4, 0.0, 0.2, 1.0)
276 );
277 assert!(matches!(
278 parse_easing("roughEase(0.4, 8)").unwrap(),
279 Easing::RoughEase { points: 8, .. }
280 ));
281 }
282
283 #[test]
284 fn rejects_bad_easing() {
285 assert!(parse_easing("nope").is_err());
286 assert!(parse_easing("steps(1, 2)").is_err());
287 }
288}