Skip to content

aarambh-darshan/spanda

spanda

Sanskrit: स्पन्द — vibration, pulse, the throb of motion.

A general-purpose animation library for Rust. Zero mandatory dependencies, no_std-ready, and designed to work anywhere: terminal UIs, web (WASM), game engines (Bevy), or native desktop apps.

Features

  • Tweening — animate any value from A to B with 38+ easing curves
  • Keyframe tracks — multi-stop animations with per-segment easing
  • Timeline & Sequence — compose animations concurrently or sequentially
  • Relative positioning — GSAP-style At::Start, At::End, At::Label, At::Offset
  • Stagger — offset N animations with a single call
  • Physics springs — damped harmonic oscillator with 4 presets + SpringN<T> for 2D/3D/4D
  • LoopingLoop::Once, Times(n), Forever, PingPong on tweens and keyframes
  • Time scale — speed up / slow down tweens and timelines at runtime
  • Callbackson_start, on_update, on_complete on tweens (std feature)
  • Value modifierssnap_to(grid), round_to(decimals), custom modifiers
  • Scroll-linked animationScrollDriver / ScrollClock for position-driven animations
  • Lenis-style smooth scrollSmoothScroll1D core + integrations::smooth_scroll::SmoothScroll (window scroll, exponential easing, wheel / keyboard / touch + InertiaN momentum; wasm-dom)
  • Motion paths — quadratic/cubic Bezier, CatmullRom splines, SVG path parsing, arc-length parameterization
  • Full motion path systemPolyPath, CompoundPath, SvgPathParser, auto-rotate, start/end offsets
  • CSS easingCubicBezier(x1,y1,x2,y2) and Steps(n) on the Easing enum
  • Colour animation — 9 palette types + InLab/InOklch/InLinear colour-space-aware wrappers (palette feature)
  • DrawSVGdraw_on / draw_on_reverse stroke-dashoffset helpers
  • Shape morphingMorphPath point-by-point morph with auto-resampling
  • Inertia physicsInertia / InertiaN<T> friction deceleration with presets
  • Advanced easingsRoughEase, SlowMo, ExpoScale, Wiggle, CustomBounce
  • Drag trackingDragState with velocity EMA, bounds, axis lock, grid snap → InertiaN on release
  • WASM-DOM plugins — FLIP animations, SplitText, ScrollSmoother, SmoothScroll, Draggable, Observer (wasm-dom feature)
  • Layout animation — automatic FLIP-style transitions with LayoutAnimator, shared element transitions
  • Gesture recognitionGestureRecognizer for tap, swipe, long press, pinch, rotation
  • GPU compute shadersGpuAnimationBatch for batch-evaluating 10,000+ tweens on the GPU (gpu feature)
  • Animation driver — manage multiple animations with auto-cleanup
  • Clock abstraction — wall clock, manual clock, scroll clock, and mock clock for testing

Getting Started

[dependencies]
spanda = "0.9.3"

Quick Example

use spanda::{Tween, Easing};
use spanda::traits::Update;

let mut tween = Tween::new(0.0_f32, 100.0)
    .duration(1.0)
    .easing(Easing::EaseOutCubic)
    .build();

// Simulate 10 frames:
for _ in 0..10 {
    tween.update(0.1);
}

assert!(tween.is_complete());
assert!((tween.value() - 100.0).abs() < 1e-6);

Looping Tween

use spanda::{Tween, Loop};
use spanda::traits::Update;

let mut tween = Tween::new(0.0_f32, 100.0)
    .duration(1.0)
    .looping(Loop::PingPong)
    .build();

// Runs forever, bouncing between 0 and 100
for _ in 0..600 {
    tween.update(1.0 / 60.0);
}

Spring Animation

use spanda::spring::{Spring, SpringConfig};
use spanda::traits::Update;

let mut spring = Spring::new(SpringConfig::wobbly());
spring.set_target(100.0);

for _ in 0..300 {
    spring.update(1.0 / 60.0);
}
assert!(spring.is_settled());

Multi-Dimensional Spring (SpringN)

use spanda::spring::{SpringN, SpringConfig};
use spanda::traits::Update;

let mut spring = SpringN::new(SpringConfig::wobbly(), [0.0_f32, 0.0]);
spring.set_target([100.0, 200.0]);

for _ in 0..1000 {
    spring.update(1.0 / 60.0);
}

let pos = spring.position(); // [f32; 2]
assert!(spring.is_settled());

Timeline with Relative Positioning

use spanda::timeline::{Timeline, At};
use spanda::tween::Tween;
use spanda::easing::Easing;

let mut tl = Timeline::new()
    .add("fade", Tween::new(0.0_f32, 1.0).duration(0.5).build(), 0.0);

// Place "slide" right after "fade" ends
tl.add_at("slide", Tween::new(0.0_f32, 100.0).duration(0.8).build(), 0.8, At::End);

// Place "glow" at the same time as "fade"
tl.add_at("glow", Tween::new(0.0_f32, 1.0).duration(0.3).build(), 0.3, At::Label("fade"));

tl.play();

Staggered Animations

use spanda::timeline::stagger;
use spanda::tween::Tween;
use spanda::traits::Update;

let tweens: Vec<_> = (0..5).map(|i| {
    let end = (i + 1) as f32 * 20.0;
    (Tween::new(0.0_f32, end).duration(0.5).build(), 0.5)
}).collect();

let mut timeline = stagger(tweens, 0.1);
timeline.play();
// Animations start at t=0.0, 0.1, 0.2, 0.3, 0.4

Scroll-Linked Animation

use spanda::scroll::ScrollDriver;
use spanda::tween::Tween;

let mut driver = ScrollDriver::new(0.0, 1000.0);
driver.add(Tween::new(0.0_f32, 100.0).duration(1.0).build());

// Drive animation from scroll position instead of time
driver.set_position(500.0); // 50% scroll

Motion Path Animation

use spanda::path::{MotionPath, MotionPathTween, PathEvaluate};
use spanda::easing::Easing;
use spanda::traits::Update;

let path = MotionPath::new()
    .cubic([0.0_f32, 0.0], [50.0, 100.0], [100.0, 100.0], [150.0, 0.0])
    .line([150.0, 0.0], [200.0, 0.0]);

let mut tween = MotionPathTween::new(path)
    .duration(2.0)
    .easing(Easing::EaseInOutCubic);

tween.update(1.0);
let pos = tween.value(); // position along the curve

Smooth Path Through Points (PolyPath)

use spanda::motion_path::PolyPath;

let path = PolyPath::from_points(vec![
    [0.0, 0.0],
    [100.0, 50.0],
    [200.0, 0.0],
    [300.0, 50.0],
]);

let pos = path.position(0.5);     // arc-length parameterized
let angle = path.rotation_deg(0.5); // auto-rotate angle

SVG Path Parsing

use spanda::svg_path::SvgPathParser;
use spanda::motion_path::CompoundPath;

let commands = SvgPathParser::parse("M 0 0 C 50 100 100 100 150 0 L 200 0");
let path = CompoundPath::new(commands)
    .start_offset(0.1)
    .end_offset(0.9);

let pos = path.position(0.5);

CSS Cubic-Bezier Easing

use spanda::{Tween, Easing};
use spanda::traits::Update;

let mut tween = Tween::new(0.0_f32, 100.0)
    .duration(1.0)
    .easing(Easing::CubicBezier(0.25, 0.1, 0.25, 1.0)) // CSS ease
    .build();

tween.update(0.5);

Colour Animation (palette feature)

use palette::Srgba;
use spanda::{Tween, Easing};
use spanda::colour::InLab;
use spanda::traits::Update;

// Interpolate in CIE L*a*b* for perceptually smooth gradients
let mut tween = Tween::new(
    InLab(Srgba::new(1.0, 0.0, 0.0, 1.0)),  // red
    InLab(Srgba::new(0.0, 0.0, 1.0, 1.0)),  // blue
)
    .duration(1.0)
    .easing(Easing::EaseInOutCubic)
    .build();

tween.update(0.5);
let colour = tween.value().0;  // Srgba

Sequence Composition

use spanda::timeline::Sequence;
use spanda::tween::Tween;
use spanda::traits::Update;

let mut timeline = Sequence::new()
    .then(Tween::new(0.0_f32, 100.0).duration(0.5).build(), 0.5)
    .gap(0.1)
    .then(Tween::new(100.0_f32, 0.0).duration(0.3).build(), 0.3)
    .build();

timeline.play();
timeline.update(0.9);

Layout Animation (FLIP)

use spanda::layout::{LayoutAnimator, Rect};
use spanda::easing::Easing;

let mut layout = LayoutAnimator::new();
layout.track("card-1", Rect::new(0.0, 0.0, 200.0, 100.0));
layout.track("card-2", Rect::new(0.0, 120.0, 200.0, 100.0));

// After layout mutation (e.g. items reordered)
let transitions = layout.compute_transitions(
    &[
        ("card-1", Rect::new(0.0, 120.0, 200.0, 100.0)),
        ("card-2", Rect::new(0.0, 0.0, 200.0, 100.0)),
    ],
    0.4,
    Easing::EaseOutCubic,
);

// In animation loop:
layout.update(dt);
if let Some(css) = layout.css_transform("card-1") {
    // Apply transform to DOM element
}

Gesture Recognition

use spanda::gesture::{GestureRecognizer, Gesture};
use spanda::drag::PointerData;

let mut recognizer = GestureRecognizer::new();

// Feed pointer events
recognizer.on_pointer_down(PointerData { x: 100.0, y: 100.0, pressure: 0.5, pointer_id: 0 });
recognizer.update(0.05);

if let Some(gesture) = recognizer.on_pointer_up(PointerData { x: 400.0, y: 105.0, pressure: 0.0, pointer_id: 0 }) {
    match gesture {
        Gesture::Swipe { direction, velocity, .. } => println!("Swipe {:?} at {} px/s", direction, velocity),
        Gesture::Tap { position } => println!("Tap at {:?}", position),
        _ => {}
    }
}

GPU Batch Animation

use spanda::gpu::GpuAnimationBatch;
use spanda::{Tween, Easing};

let mut batch = GpuAnimationBatch::new_auto(); // GPU with CPU fallback
for i in 0..10_000 {
    batch.push(Tween::new(0.0_f32, 1.0).duration(1.0).easing(Easing::EaseOutCubic).build());
}
batch.tick(1.0 / 60.0);
let positions: &[f32] = batch.read_back();

Feature Flags

Flag What it adds
std (default) wall-clock driver, thread-safe internals
serde Serialize/Deserialize on all public types
bevy SpandaPlugin for Bevy 0.18
wasm requestAnimationFrame driver
wasm-dom DOM plugins: FLIP, SplitText, ScrollSmoother, SmoothScroll, Draggable, Observer
palette Colour interpolation via the palette crate
tokio async / .await on timeline completion
gpu GPU compute shader batch animation via wgpu

Bevy Integration

use bevy::prelude::*;
use spanda::integrations::bevy::{SpandaPlugin, TweenCompleted, SpringSettled};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(SpandaPlugin)
        .run();
}

WASM Integration

use spanda::integrations::wasm::RafDriver;

let mut driver = RafDriver::new();
driver.pause();       // pause animations
driver.resume();      // resume
driver.set_time_scale(2.0); // 2x speed
// Call driver.tick(timestamp_ms) from your rAF callback.

Smooth window scroll (wasm-dom)

SmoothScroll applies Lenis-style smoothing to window scroll (scroll_to_with_x_and_y only). Feed current_scroll() into ScrollDriver::set_position for scroll-linked motion that matches what the user sees.

use spanda::integrations::smooth_scroll::{SmoothScroll, SmoothScrollOptions};

let mut smooth = SmoothScroll::new(SmoothScrollOptions::default());
smooth.attach().expect("attach");
// Each rAF: dt = seconds since last frame
smooth.tick(dt);

Benchmarks

cargo bench

Tests

cargo test                # unit + integration + doc tests
cargo test --tests        # integration tests only

Project Structure

src/
├── lib.rs           — crate root, re-exports
├── traits.rs        — Interpolate, Animatable, Update
├── easing.rs        — 38 easing functions + CubicBezier + Steps + 5 advanced
├── tween.rs         — Tween<T>, TweenBuilder, TweenState
├── keyframe.rs      — KeyframeTrack, Keyframe, Loop
├── timeline.rs      — Timeline, Sequence, At, stagger
├── spring.rs        — Spring, SpringConfig, SpringN, SpringAnimatable
├── clock.rs         — Clock, WallClock, ManualClock, MockClock
├── driver.rs        — AnimationDriver, AnimationId
├── scroll.rs        — ScrollClock, ScrollDriver
├── scroll_smooth.rs — SmoothScroll1D (exponential smooth scroll core)
├── path.rs          — BezierPath, MotionPath, MotionPathTween
├── bezier.rs        — CatmullRomSpline, PathEvaluate2D
├── motion_path.rs   — PolyPath, CompoundPath, PathCommand
├── svg_path.rs      — SvgPathParser (SVG d-attribute parser)
├── colour.rs        — colour interpolation (feature = "palette")
├── svg_draw.rs      — DrawSVG stroke-dashoffset helpers
├── morph.rs         — MorphPath shape morphing + resample
├── inertia.rs       — Inertia, InertiaN friction deceleration
├── drag.rs          — DragState, DragConstraints, PointerData
├── layout.rs        — LayoutAnimator, Rect, SharedElementTransition
├── gesture.rs       — GestureRecognizer, Gesture, GestureConfig
├── gpu.rs           — GpuAnimationBatch (feature = "gpu")
├── gpu_tween.wgsl   — WGSL compute shader
└── integrations/
    ├── mod.rs
    ├── bevy.rs      — SpandaPlugin  (feature = "bevy")
    ├── wasm.rs      — RafDriver     (feature = "wasm")
    ├── split_text.rs — SplitText character/word splitting
    ├── flip.rs      — FlipState, FlipAnimation (feature = "wasm-dom")
    ├── scroll_smoother.rs — ScrollSmoother (feature = "wasm-dom")
    ├── smooth_scroll.rs — SmoothScroll (feature = "wasm-dom")
    ├── draggable.rs — Draggable DOM binding (feature = "wasm-dom")
    └── observer.rs  — Observer unified input (feature = "wasm-dom")

License

Licensed under either of

at your option.

About

Spanda is a high-performance animation engine in Rust for web (WASM), native apps, games, and TUIs. It features easing curves, spring physics, timelines, motion paths, and advanced color interpolation—all with zero mandatory dependencies and no_std support.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors