devalang_wasm/engine/curves/
mod.rs1use crate::language::syntax::ast::Value;
4use std::f32::consts::PI;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum CurveType {
9 Linear,
11 EaseIn,
12 EaseOut,
13 EaseInOut,
14
15 Swing(f32), Bounce(f32), Elastic(f32), Bezier(f32, f32, f32, f32), Random,
23 Perlin, StepFunction(f32), }
28
29pub fn evaluate_curve(curve: CurveType, progress: f32) -> f32 {
31 let p = progress.clamp(0.0, 1.0);
32
33 match curve {
34 CurveType::Linear => p,
35
36 CurveType::EaseIn => p * p,
37 CurveType::EaseOut => 1.0 - (1.0 - p) * (1.0 - p),
38 CurveType::EaseInOut => {
39 if p < 0.5 {
40 2.0 * p * p
41 } else {
42 1.0 - (-2.0 * p + 2.0).powi(2) / 2.0
43 }
44 }
45
46 CurveType::Swing(intensity) => {
47 let i = intensity.clamp(0.0, 1.0);
48 let swing_amount = 0.5 * i;
49 if p < 0.5 {
50 let local_p = p * 2.0;
51 (1.0 + swing_amount) * local_p - swing_amount * local_p * local_p
52 } else {
53 let local_p = (p - 0.5) * 2.0;
54 1.0 - ((1.0 + swing_amount) * (1.0 - local_p)
55 - swing_amount * (1.0 - local_p) * (1.0 - local_p))
56 }
57 }
58
59 CurveType::Bounce(height) => {
60 let h = height.clamp(0.0, 1.0);
61 let bounce_height = 0.5 * h;
62
63 if p < 0.5 {
65 let local_p = p * 2.0;
66 local_p / 2.0 + bounce_height * (PI * local_p).sin()
67 } else {
68 let local_p = (p - 0.5) * 2.0;
69 0.5 + local_p / 2.0 + bounce_height * (PI * (1.0 - local_p)).sin()
70 }
71 }
72
73 CurveType::Elastic(intensity) => {
74 let i = intensity.clamp(0.0, 1.0);
75 let n = 5.0 * i; p * ((n * PI * p).sin() * 0.5 + 1.0)
77 }
78
79 CurveType::Bezier(x1, y1, x2, y2) => {
80 bezier(p, x1, y1, x2, y2)
82 }
83
84 CurveType::Random => {
85 ((p * 12.9898).sin() * 43758.5453_f32).fract()
87 }
88
89 CurveType::Perlin => {
90 let t = p * 3.0;
92 let i = t.floor();
93 let f = t - i;
94 let u = f * f * (3.0 - 2.0 * f); let g0 = ((i * 12.9898).sin() * 43758.5453_f32).fract();
98 let g1 = (((i + 1.0) * 12.9898).sin() * 43758.5453_f32).fract();
99
100 g0.lerp(g1, u)
101 }
102
103 CurveType::StepFunction(steps) => {
104 let n = steps.max(1.0);
105 (p * n).floor() / n
106 }
107 }
108}
109
110fn bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
112 let mut u = t;
114 for _ in 0..4 {
115 let cu = (1.0 - u).powi(3)
116 + 3.0 * (1.0 - u).powi(2) * u * x1
117 + 3.0 * (1.0 - u) * u.powi(2) * x2
118 + u.powi(3);
119 let der = 3.0 * (1.0 - u).powi(2) * (x1 - 1.0)
120 + 6.0 * (1.0 - u) * u * (x2 - x1)
121 + 3.0 * u.powi(2) * (1.0 - x2);
122
123 u -= (cu - t) / der;
124 }
125
126 (1.0 - u).powi(3)
128 + 3.0 * (1.0 - u).powi(2) * u * y1
129 + 3.0 * (1.0 - u) * u.powi(2) * y2
130 + u.powi(3)
131}
132
133pub fn parse_curve(name: &str) -> Option<CurveType> {
140 if name.starts_with("$curve.") {
141 let curve_name = &name[7..]; parse_curve_name(curve_name)
143 } else if name.starts_with("$ease.") {
144 let ease_name = &name[6..]; parse_ease_name(ease_name)
146 } else {
147 None
148 }
149}
150
151fn parse_curve_name(name: &str) -> Option<CurveType> {
152 match name {
153 "linear" => Some(CurveType::Linear),
154 "in" => Some(CurveType::EaseIn),
155 "out" => Some(CurveType::EaseOut),
156 "inOut" => Some(CurveType::EaseInOut),
157 "random" => Some(CurveType::Random),
158 "perlin" => Some(CurveType::Perlin),
159
160 _ if name.starts_with("swing(") => {
162 let intensity = extract_param(name, "swing")?;
163 Some(CurveType::Swing(intensity))
164 }
165 _ if name.starts_with("bounce(") => {
166 let height = extract_param(name, "bounce")?;
167 Some(CurveType::Bounce(height))
168 }
169 _ if name.starts_with("elastic(") => {
170 let intensity = extract_param(name, "elastic")?;
171 Some(CurveType::Elastic(intensity))
172 }
173 _ if name.starts_with("step(") => {
174 let steps = extract_param(name, "step")?;
175 Some(CurveType::StepFunction(steps))
176 }
177
178 _ => None,
179 }
180}
181
182fn parse_ease_name(name: &str) -> Option<CurveType> {
183 match name {
184 "linear" => Some(CurveType::Linear),
185 "in" => Some(CurveType::EaseIn),
186 "out" => Some(CurveType::EaseOut),
187 "inOut" => Some(CurveType::EaseInOut),
188
189 _ if name.starts_with("bezier(") => extract_bezier_params(name),
191
192 _ => None,
193 }
194}
195
196fn extract_param(name: &str, func_name: &str) -> Option<f32> {
199 let start = format!("{}(", func_name);
200 let start_idx = name.find(&start)? + start.len();
201 let end_idx = name.rfind(')')?;
202 let content = &name[start_idx..end_idx];
203 content.trim().parse::<f32>().ok()
204}
205
206fn extract_bezier_params(name: &str) -> Option<CurveType> {
208 let start_idx = name.find('(')? + 1;
209 let end_idx = name.rfind(')')?;
210 let content = &name[start_idx..end_idx];
211
212 let parts: Vec<f32> = content
213 .split(',')
214 .filter_map(|s| s.trim().parse::<f32>().ok())
215 .collect();
216
217 if parts.len() == 4 {
218 Some(CurveType::Bezier(parts[0], parts[1], parts[2], parts[3]))
219 } else {
220 None
221 }
222}
223
224trait Lerp {
226 fn lerp(&self, other: Self, t: f32) -> Self;
227}
228
229impl Lerp for f32 {
230 fn lerp(&self, other: f32, t: f32) -> f32 {
231 self + (other - self) * t
232 }
233}
234
235pub fn curve_to_value(curve: CurveType) -> Value {
237 let repr = match curve {
240 CurveType::Linear => "$curve.linear".to_string(),
241 CurveType::EaseIn => "$curve.in".to_string(),
242 CurveType::EaseOut => "$curve.out".to_string(),
243 CurveType::EaseInOut => "$curve.inOut".to_string(),
244 CurveType::Swing(i) => format!("$curve.swing({})", i),
245 CurveType::Bounce(h) => format!("$curve.bounce({})", h),
246 CurveType::Elastic(i) => format!("$curve.elastic({})", i),
247 CurveType::Random => "$curve.random".to_string(),
248 CurveType::Perlin => "$curve.perlin".to_string(),
249 CurveType::Bezier(x1, y1, x2, y2) => {
250 format!("$ease.bezier({}, {}, {}, {})", x1, y1, x2, y2)
251 }
252 CurveType::StepFunction(s) => format!("$curve.step({})", s),
253 };
254
255 Value::String(repr)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_linear_curve() {
264 assert_eq!(evaluate_curve(CurveType::Linear, 0.0), 0.0);
265 assert_eq!(evaluate_curve(CurveType::Linear, 0.5), 0.5);
266 assert_eq!(evaluate_curve(CurveType::Linear, 1.0), 1.0);
267 }
268
269 #[test]
270 fn test_ease_in_curve() {
271 let v = evaluate_curve(CurveType::EaseIn, 0.5);
272 assert!(v > 0.0 && v < 0.5);
273 }
274
275 #[test]
276 fn test_parse_curve() {
277 let curve = parse_curve("$curve.linear");
278 assert!(matches!(curve, Some(CurveType::Linear)));
279
280 let curve = parse_curve("$curve.swing(0.5)");
281 assert!(matches!(curve, Some(CurveType::Swing(_))));
282
283 let curve = parse_curve("$ease.bezier(0.25, 0.1, 0.25, 1.0)");
284 assert!(matches!(curve, Some(CurveType::Bezier(_, _, _, _))));
285 }
286
287 #[test]
288 fn test_swing_curve() {
289 let v = evaluate_curve(CurveType::Swing(0.5), 0.5);
290 assert!(v >= 0.0 && v <= 1.0);
291 }
292}