Skip to main content

cvkg_anim/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//!                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//!                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//!                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//!                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//!                      and move to unblocked work. Never silently accept a broken state.
20#![allow(
21    clippy::needless_range_loop,
22    clippy::too_many_arguments,
23    clippy::manual_range_contains
24)]
25
26//! # Sleipnir Animation Engine
27//!
28//! Provides high-fidelity physics-based animation and transition systems for CVKG.
29
30use std::sync::Arc;
31use std::time::Duration;
32pub mod advanced_particles;
33pub mod behavior;
34pub mod geometry;
35pub mod growth;
36pub mod particles;
37pub mod shader_anim;
38pub mod skeletal;
39pub use particles::*;
40
41pub mod momentum;
42pub mod morph;
43pub mod physics;
44
45pub mod spring_snap;
46
47pub mod verlet;
48
49/// Sleipnir spring parameters for the physics solver
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct SleipnirParams {
52    pub stiffness: f32,
53    pub damping: f32,
54    pub mass: f32,
55}
56
57impl SleipnirParams {
58    pub fn snappy() -> Self {
59        Self {
60            stiffness: 230.0,
61            damping: 22.0,
62            mass: 1.0,
63        }
64    }
65    pub fn fluid() -> Self {
66        Self {
67            stiffness: 170.0,
68            damping: 26.0,
69            mass: 1.0,
70        }
71    }
72    pub fn heavy() -> Self {
73        Self {
74            stiffness: 90.0,
75            damping: 20.0,
76            mass: 1.0,
77        }
78    }
79    pub fn bouncy() -> Self {
80        Self {
81            stiffness: 190.0,
82            damping: 14.0,
83            mass: 1.0,
84        }
85    }
86}
87
88impl Default for SleipnirParams {
89    fn default() -> Self {
90        Self::fluid()
91    }
92}
93
94/// A discrete keyframe in a hybrid animation path
95#[derive(Debug, Clone, PartialEq)]
96pub struct Keyframe {
97    pub value: f32,
98    pub time: Duration,
99    pub easing: Easing,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub enum Easing {
104    Linear,
105    EaseIn,
106    EaseOut,
107    EaseInOut,
108}
109
110impl Easing {
111    /// Evaluate the easing function for a parameter `t` in [0.0, 1.0].
112    pub fn evaluate(&self, t: f32) -> f32 {
113        let t = t.clamp(0.0, 1.0);
114        match self {
115            Easing::Linear => t,
116            Easing::EaseIn => t * t,
117            Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
118            Easing::EaseInOut => {
119                if t < 0.5 {
120                    2.0 * t * t
121                } else {
122                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
123                }
124            }
125        }
126    }
127}
128
129/// High-level Animation Primitive
130#[derive(Clone)]
131pub enum Animation {
132    /// No animation (instant)
133    Ginnungagap,
134    /// Linear animation
135    Linear { duration: Duration },
136    /// Organic spring animation
137    Sleipnir(SleipnirParams),
138    /// Hybrid: Keyframe path followed by a Spring settle
139    Hybrid {
140        keyframes: Vec<Keyframe>,
141        settle: SleipnirParams,
142    },
143    /// Coordination: Multiple animations in parallel
144    Parallel(Vec<Animation>),
145    /// Coordination: Multiple animations in sequence
146    Sequence(Vec<Animation>),
147    /// Coordination: Staggered start for multiple animations
148    Stagger {
149        animations: Vec<Animation>,
150        interval: Duration,
151    },
152    /// Bifrost transition (Glass-aware fade)
153    BifrostFade { duration: Duration },
154    /// Mjolnir transition (Geometric slice)
155    MjolnirSlice { duration: Duration },
156    /// Mjolnir transition (Physical shatter)
157    MjolnirShatter {
158        duration: Duration,
159        pieces: u32,
160        force: f32,
161    },
162    /// Inertial Momentum (Friction/Decay solver)
163    Momentum {
164        initial_velocity: f32,
165        friction: f32,
166    },
167}
168
169/// Abstract driver for animation progress (Time-based or Scroll/Scalar-based)
170#[derive(Debug, Clone, Copy, PartialEq)]
171pub enum ProgressDriver {
172    Time(Duration),
173    Scalar(f32),
174}
175
176impl ProgressDriver {
177    pub fn delta_time_secs(&self) -> f32 {
178        match self {
179            ProgressDriver::Time(dt) => dt.as_secs_f32(),
180            ProgressDriver::Scalar(ds) => *ds, // Use scalar diff as "dt" for generic progression
181        }
182    }
183}
184
185/// Tactile "Rubber Banding" utility for scroll/drag physics.
186/// Maps an unbounded input value to a bounded range with elastic resistance.
187pub struct RubberBand {
188    /// Minimum bound of the valid range
189    pub min: f32,
190    /// Maximum bound of the valid range
191    pub max: f32,
192    /// Resistance constant (higher = stiffer)
193    pub constant: f32,
194}
195
196impl RubberBand {
197    /// Create a new RubberBand solver with default resistance.
198    pub fn new(min: f32, max: f32) -> Self {
199        Self {
200            min,
201            max,
202            constant: 0.55,
203        }
204    }
205
206    /// Calculate the resisted value for an input that may exceed bounds.
207    pub fn solve(&self, input: f32) -> f32 {
208        if input < self.min {
209            self.min - self.apply_resistance(self.min - input)
210        } else if input > self.max {
211            self.max + self.apply_resistance(input - self.max)
212        } else {
213            input
214        }
215    }
216
217    fn apply_resistance(&self, delta: f32) -> f32 {
218        // Logarithmic resistance similar to iOS/WebKit
219        (delta * self.constant).atan() * (1.0 / self.constant)
220    }
221}
222
223/// Motion controller that handles lifecycle events and state transitions.
224pub struct Motion {
225    /// The target animation sequence or spring
226    pub animation: Animation,
227    /// Callback triggered when the animation starts
228    pub on_start: Option<Arc<dyn Fn() + Send + Sync>>,
229    /// Callback triggered when the physics settle at the target
230    pub on_settle: Option<Arc<dyn Fn() + Send + Sync>>,
231    /// Callback triggered if the animation is interrupted by a new target
232    pub on_interrupt: Option<Arc<dyn Fn() + Send + Sync>>,
233}
234
235impl Motion {
236    /// Create a new Motion controller for an animation.
237    pub fn new(animation: Animation) -> Self {
238        Self {
239            animation,
240            on_start: None,
241            on_settle: None,
242            on_interrupt: None,
243        }
244    }
245}
246
247/// SleipnirSolver implements a 4th-order Runge-Kutta (RK4) integration for springs.
248/// This provides superior stability for high-fidelity interactive motion.
249#[derive(Debug, Clone, Copy, PartialEq)]
250pub struct SleipnirSolver {
251    params: SleipnirParams,
252    target: f32,
253    state: SolverState,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq)]
257struct SolverState {
258    x: f32,
259    v: f32,
260}
261
262impl SleipnirSolver {
263    /// Create a new solver with a target value and starting state.
264    pub fn new(params: SleipnirParams, target: f32, current: f32) -> Self {
265        Self {
266            params,
267            target,
268            state: SolverState { x: current, v: 0.0 },
269        }
270    }
271
272    pub fn set_target(&mut self, target: f32) {
273        self.target = target;
274    }
275
276    /// Set the starting velocity for the solver.
277    pub fn with_velocity(mut self, velocity: f32) -> Self {
278        self.state.v = velocity;
279        self
280    }
281
282    /// Advance the simulation by dt seconds using RK4 integration.
283    pub fn tick(&mut self, dt: f32) -> f32 {
284        let a = self.evaluate(self.state, 0.0, SolverState { x: 0.0, v: 0.0 });
285        let b = self.evaluate(self.state, dt * 0.5, a);
286        let c = self.evaluate(self.state, dt * 0.5, b);
287        let d = self.evaluate(self.state, dt, c);
288
289        let dxdt = 1.0 / 6.0 * (a.x + 2.0 * (b.x + c.x) + d.x);
290        let dvdt = 1.0 / 6.0 * (a.v + 2.0 * (b.v + c.v) + d.v);
291
292        self.state.x += dxdt * dt;
293        self.state.v += dvdt * dt;
294        self.state.x
295    }
296
297    fn evaluate(&self, initial: SolverState, dt: f32, d: SolverState) -> SolverState {
298        let state = SolverState {
299            x: initial.x + d.x * dt,
300            v: initial.v + d.v * dt,
301        };
302        let force =
303            -self.params.stiffness * (state.x - self.target) - self.params.damping * state.v;
304        // Protect against division by zero; mass must be positive.
305        let mass = self.params.mass.max(0.001);
306        SolverState {
307            x: state.v,
308            v: force / mass,
309        }
310    }
311
312    pub fn is_settled(&self) -> bool {
313        (self.state.x - self.target).abs() < 0.001 && self.state.v.abs() < 0.001
314    }
315}
316
317/// Active animation state tracker
318pub struct ActiveAnimation {
319    pub animation: Animation,
320    pub elapsed: Duration,
321    pub is_finished: bool,
322    pub current_value: f32,
323
324    // Internal state for complex animations
325    solver: Option<SleipnirSolver>,
326    child_states: Vec<ActiveAnimation>,
327    current_index: usize,
328}
329
330impl ActiveAnimation {
331    pub fn new(animation: Animation) -> Self {
332        Self {
333            animation,
334            elapsed: Duration::ZERO,
335            is_finished: false,
336            current_value: 0.0,
337            solver: None,
338            child_states: Vec::new(),
339            current_index: 0,
340        }
341    }
342
343    pub fn update(&mut self, dt: ProgressDriver, start_val: f32, end_val: f32) -> f32 {
344        if self.is_finished {
345            return end_val;
346        }
347
348        match dt {
349            ProgressDriver::Time(duration) => {
350                self.elapsed += duration;
351            }
352            ProgressDriver::Scalar(t) => {
353                // Scalar directly controls absolute progress timeline instead of elapsed time.
354                // We'll map elapsed time strictly to the scalar seconds.
355
356                // RESTRICTION: Scroll Timelines (Scalar) only apply to Keyframe/Linear animations.
357                // If this is a physics animation (Sleipnir, Momentum, etc.), we ignore the scalar scrub.
358                match &self.animation {
359                    Animation::Linear { .. }
360                    | Animation::Hybrid { .. }
361                    | Animation::BifrostFade { .. }
362                    | Animation::MjolnirSlice { .. } => {
363                        self.elapsed = Duration::from_secs_f32(t);
364                    }
365                    _ => {
366                        return self.current_value; // Ignore scroll scrubbing on physics!
367                    }
368                }
369            }
370        }
371
372        let dt_secs = dt.delta_time_secs();
373        let t = self.elapsed.as_secs_f32();
374
375        match &self.animation {
376            Animation::Ginnungagap => {
377                self.is_finished = true;
378                self.current_value = end_val;
379            }
380            Animation::Linear { duration } => {
381                let d = duration.as_secs_f32();
382                if t >= d {
383                    self.is_finished = true;
384                    self.current_value = end_val;
385                } else {
386                    self.current_value = start_val + (end_val - start_val) * (t / d);
387                }
388            }
389            Animation::Sleipnir(params) => {
390                let solver = self
391                    .solver
392                    .get_or_insert_with(|| SleipnirSolver::new(*params, end_val, start_val));
393                self.current_value = solver.tick(dt_secs);
394                if solver.is_settled() {
395                    self.is_finished = true;
396                }
397            }
398            Animation::Sequence(anims) => {
399                if self.current_index >= anims.len() {
400                    self.is_finished = true;
401                    self.current_value = end_val;
402                } else {
403                    if self.child_states.is_empty() {
404                        self.child_states = anims
405                            .iter()
406                            .map(|a| ActiveAnimation::new(a.clone()))
407                            .collect();
408                    }
409
410                    let child = &mut self.child_states[self.current_index];
411                    self.current_value = child.update(dt, start_val, end_val);
412
413                    if child.is_finished {
414                        self.current_index += 1;
415                        if self.current_index >= anims.len() {
416                            self.is_finished = true;
417                        }
418                    }
419                }
420            }
421            Animation::Parallel(anims) => {
422                if self.child_states.is_empty() {
423                    self.child_states = anims
424                        .iter()
425                        .map(|a| ActiveAnimation::new(a.clone()))
426                        .collect();
427                }
428
429                let mut all_finished = true;
430                let mut sum_val = 0.0;
431                for child in &mut self.child_states {
432                    sum_val += child.update(dt, start_val, end_val);
433                    if !child.is_finished {
434                        all_finished = false;
435                    }
436                }
437
438                self.current_value = if !anims.is_empty() {
439                    sum_val / anims.len() as f32
440                } else {
441                    end_val
442                };
443                if all_finished {
444                    self.is_finished = true;
445                }
446            }
447            Animation::Hybrid { keyframes, settle } => {
448                // Phase 1: Walk through keyframes in order.
449                if self.current_index < keyframes.len() {
450                    let prev_value = if self.current_index == 0 {
451                        start_val
452                    } else {
453                        keyframes[self.current_index - 1].value
454                    };
455
456                    let kf = &keyframes[self.current_index];
457                    let kf_start_time = if self.current_index == 0 {
458                        0.0
459                    } else {
460                        keyframes[self.current_index - 1].time.as_secs_f32()
461                    };
462                    let kf_end_time = kf.time.as_secs_f32();
463                    let kf_duration = (kf_end_time - kf_start_time).max(0.001);
464                    let local_t = ((t - kf_start_time) / kf_duration).clamp(0.0, 1.0);
465                    let eased_t = kf.easing.evaluate(local_t);
466
467                    self.current_value = prev_value + (kf.value - prev_value) * eased_t;
468
469                    if local_t >= 1.0 {
470                        self.current_index += 1;
471                        if self.current_index >= keyframes.len() {
472                            self.solver =
473                                Some(SleipnirSolver::new(*settle, end_val, self.current_value));
474                        }
475                    }
476                } else {
477                    let solver = self.solver.get_or_insert_with(|| {
478                        SleipnirSolver::new(*settle, end_val, self.current_value)
479                    });
480                    self.current_value = solver.tick(dt_secs);
481                    if solver.is_settled() {
482                        self.is_finished = true;
483                    }
484                }
485            }
486            Animation::Stagger {
487                animations,
488                interval,
489            } => {
490                if self.child_states.is_empty() {
491                    self.child_states = animations
492                        .iter()
493                        .map(|a| ActiveAnimation::new(a.clone()))
494                        .collect();
495                }
496
497                let interval_secs = interval.as_secs_f32();
498                let mut all_finished = true;
499                let mut sum_val = 0.0;
500
501                for (i, child) in self.child_states.iter_mut().enumerate() {
502                    let delay = interval_secs * i as f32;
503                    if t > delay {
504                        sum_val += child.update(dt, start_val, end_val);
505                    } else {
506                        child.current_value = start_val;
507                    }
508                    if !child.is_finished {
509                        all_finished = false;
510                    }
511                }
512
513                self.current_value = if !animations.is_empty() {
514                    sum_val / animations.len() as f32
515                } else {
516                    end_val
517                };
518                if all_finished {
519                    self.is_finished = true;
520                }
521            }
522            Animation::BifrostFade { duration } => {
523                let d = duration.as_secs_f32();
524                if t >= d {
525                    self.is_finished = true;
526                    self.current_value = end_val;
527                } else {
528                    let progress = (t / d).clamp(0.0, 1.0);
529                    let base_t = Easing::EaseInOut.evaluate(progress);
530                    let fade_factor = if progress < 0.5 {
531                        1.0 - 2.0 * progress
532                    } else {
533                        2.0 * progress - 1.0
534                    };
535                    let opacity = 0.5 + 0.5 * fade_factor;
536                    self.current_value = start_val + (end_val - start_val) * base_t * opacity;
537                }
538            }
539            Animation::MjolnirSlice { duration } => {
540                let d = duration.as_secs_f32();
541                if t >= d {
542                    self.is_finished = true;
543                    self.current_value = end_val;
544                } else {
545                    let progress = Easing::EaseInOut.evaluate((t / d).clamp(0.0, 1.0));
546                    self.current_value = start_val + (end_val - start_val) * progress;
547                }
548            }
549            Animation::MjolnirShatter {
550                duration,
551                pieces,
552                force,
553            } => {
554                let piece_count = (*pieces as usize).max(1);
555                let force_val = *force;
556                let stiff = force_val.max(1.0) * 10.0;
557
558                if self.child_states.is_empty() {
559                    for i in 0..piece_count {
560                        let offset = ((i as f32 + 1.0) / piece_count as f32) * force_val * 0.1;
561                        let piece_start = end_val + offset * (if i % 2 == 0 { 1.0 } else { -1.0 });
562                        let params = SleipnirParams {
563                            stiffness: stiff,
564                            damping: 8.0,
565                            mass: 1.0,
566                        };
567                        let mut child = ActiveAnimation::new(Animation::Sleipnir(params));
568                        child.solver = Some(SleipnirSolver::new(params, end_val, piece_start));
569                        self.child_states.push(child);
570                    }
571                }
572
573                let total_d = duration.as_secs_f32();
574                if t >= total_d {
575                    self.is_finished = true;
576                    self.current_value = end_val;
577                    for child in &mut self.child_states {
578                        child.is_finished = true;
579                        child.current_value = end_val;
580                    }
581                } else {
582                    let mut sum_val = 0.0;
583                    let mut all_finished = true;
584                    for child in &mut self.child_states {
585                        let solver = child.solver.as_mut().unwrap();
586                        child.current_value = solver.tick(dt_secs);
587                        if !solver.is_settled() {
588                            all_finished = false;
589                        }
590                        sum_val += child.current_value;
591                    }
592                    self.current_value = if piece_count > 0 {
593                        sum_val / piece_count as f32
594                    } else {
595                        end_val
596                    };
597                    if all_finished {
598                        self.is_finished = true;
599                    }
600                }
601            }
602            Animation::Momentum {
603                initial_velocity,
604                friction,
605            } => {
606                // Advance the decay simulation using DecaySolver from momentum.rs
607                // to calculate inertial momentum progress.
608                let mut solver = crate::momentum::DecaySolver::new(
609                    *initial_velocity,
610                    *friction,
611                    self.current_value,
612                );
613                self.current_value = solver.tick(dt_secs);
614                if solver.velocity.abs() < 0.1 {
615                    self.is_finished = true;
616                }
617            }
618        }
619        self.current_value
620    }
621}
622
623pub trait AnimationValue: Sized + Clone + PartialEq {
624    fn lerp(&self, other: &Self, t: f32) -> Self;
625    fn distance(&self, other: &Self) -> f32;
626}
627
628impl AnimationValue for f32 {
629    fn lerp(&self, other: &Self, t: f32) -> Self {
630        self + (other - self) * t
631    }
632    fn distance(&self, other: &Self) -> f32 {
633        (self - other).abs()
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640
641    #[test]
642    fn test_rubber_band_solving() {
643        let rb = RubberBand::new(0.0, 100.0);
644
645        // Inside bounds
646        assert_eq!(rb.solve(50.0), 50.0);
647
648        // Above bounds
649        let over = rb.solve(150.0);
650        assert!(over > 100.0);
651        assert!(over < 150.0); // Resistance applied
652
653        // Below bounds
654        let under = rb.solve(-50.0);
655        assert!(under < 0.0);
656        assert!(under > -50.0); // Resistance applied
657    }
658
659    #[test]
660    fn test_sleipnir_solver_convergence() {
661        let params = SleipnirParams::snappy();
662        let mut solver = SleipnirSolver::new(params, 100.0, 0.0);
663
664        // Initial state
665        assert!(!solver.is_settled());
666
667        // Simulate some ticks
668        for _ in 0..100 {
669            solver.tick(0.016);
670        }
671
672        assert!(solver.is_settled());
673        assert!((solver.state.x - 100.0).abs() < 0.01);
674    }
675
676    #[test]
677    fn test_animation_sequence_execution() {
678        let anims = vec![
679            Animation::Linear {
680                duration: Duration::from_millis(100),
681            },
682            Animation::Linear {
683                duration: Duration::from_millis(100),
684            },
685        ];
686        let mut active = ActiveAnimation::new(Animation::Sequence(anims));
687
688        // Update first animation halfway
689        active.update(ProgressDriver::Time(Duration::from_millis(50)), 0.0, 100.0);
690        assert!(!active.is_finished);
691        assert_eq!(active.current_index, 0);
692
693        // Complete first animation
694        active.update(ProgressDriver::Time(Duration::from_millis(60)), 0.0, 100.0);
695        assert!(!active.is_finished);
696        assert_eq!(active.current_index, 1);
697
698        // Complete second animation
699        active.update(ProgressDriver::Time(Duration::from_millis(100)), 0.0, 100.0);
700        assert!(active.is_finished);
701    }
702}