devalang_wasm/engine/curves/
mod.rs

1/// Curves system for Devalang
2/// Provides easing functions and animation curves for automations
3use crate::language::syntax::ast::Value;
4use std::f32::consts::PI;
5
6/// Easing curve types
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum CurveType {
9    // Basic easing
10    Linear,
11    EaseIn,
12    EaseOut,
13    EaseInOut,
14
15    // Advanced easing
16    Swing(f32),                 // Swing with optional intensity (0.0-1.0)
17    Bounce(f32),                // Bounce with optional height (0.0-1.0)
18    Elastic(f32),               // Elastic with optional intensity
19    Bezier(f32, f32, f32, f32), // Custom bezier with control points
20
21    // Noise-based
22    Random,
23    Perlin, // Perlin noise-based curve
24
25    // Special
26    StepFunction(f32), // Number of steps
27}
28
29/// Evaluate a curve at progress (0.0 to 1.0)
30pub 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            // Simplified bounce curve
64            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; // Number of bounces
76            p * ((n * PI * p).sin() * 0.5 + 1.0)
77        }
78
79        CurveType::Bezier(x1, y1, x2, y2) => {
80            // Simplified cubic bezier evaluation
81            bezier(p, x1, y1, x2, y2)
82        }
83
84        CurveType::Random => {
85            // Deterministic "random" based on input (using sine for pseudo-randomness)
86            ((p * 12.9898).sin() * 43758.5453_f32).fract()
87        }
88
89        CurveType::Perlin => {
90            // Simplified Perlin-like noise
91            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); // Smooth step
95
96            // Pseudo-random gradients
97            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
110/// Cubic bezier evaluation using Newton's method
111fn bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
112    // Find X value for given t
113    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    // Calculate Y from U
127    (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
133/// Parse a curve definition from special variable syntax
134/// Examples:
135/// - $curve.linear
136/// - $curve.easeIn
137/// - $curve.swing(0.5)
138/// - $ease.bezier(0.25, 0.1, 0.25, 1.0)
139pub fn parse_curve(name: &str) -> Option<CurveType> {
140    if name.starts_with("$curve.") {
141        let curve_name = &name[7..]; // Remove "$curve."
142        parse_curve_name(curve_name)
143    } else if name.starts_with("$ease.") {
144        let ease_name = &name[6..]; // Remove "$ease."
145        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        // Parameterized curves
161        _ 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        // Bezier curve: bezier(x1, y1, x2, y2)
190        _ if name.starts_with("bezier(") => extract_bezier_params(name),
191
192        _ => None,
193    }
194}
195
196/// Extract single parameter from function string
197/// Example: "swing(0.5)" -> 0.5
198fn 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
206/// Extract bezier parameters: bezier(x1, y1, x2, y2)
207fn 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
224/// Helper trait for lerp
225trait 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
235/// Get a curve as a Value (for use with automation)
236pub fn curve_to_value(curve: CurveType) -> Value {
237    // Store curve as a string representation for now
238    // Later could use a custom Value variant
239    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}