Skip to main content

solium_animation/
ffi.rs

1//! A C ABI over the engine, so it can be called from outside Rust.
2//!
3//! This exists for the preview: the engine is built for `wasm32` and driven
4//! from the page, which means what you tune in a browser is *this* code and not
5//! a copy of it in JavaScript. A copy would drift the first time either
6//! changed, and the drift would be invisible — the page would still animate
7//! plausibly.
8//!
9//! Only `f64` and integers cross the boundary, so there is nothing to allocate,
10//! free or get wrong about ownership.
11
12use std::time::Duration;
13
14use crate::{Animation, Curve, Spring};
15
16/// The curve at `index` in [`Curve::all`], or the default if out of range.
17fn curve(index: u32) -> Curve {
18    Curve::all()
19        .get(index as usize)
20        .map_or_else(Curve::default, |(_, curve)| *curve)
21}
22
23/// How many curves there are, so a caller can enumerate them.
24#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
25#[unsafe(no_mangle)]
26pub extern "C" fn solium_curve_count() -> u32 {
27    Curve::all().len() as u32
28}
29
30/// A curve's value at normalised time `t`.
31#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
32#[unsafe(no_mangle)]
33pub extern "C" fn solium_curve_at(index: u32, t: f64) -> f64 {
34    curve(index).at(t)
35}
36
37/// A spring's value `seconds` after it was released.
38#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
39#[unsafe(no_mangle)]
40pub extern "C" fn solium_spring_at(
41    stiffness: f64,
42    damping: f64,
43    mass: f64,
44    initial_velocity: f64,
45    seconds: f64,
46) -> f64 {
47    spring(stiffness, damping, mass, initial_velocity).value_at(seconds)
48}
49
50/// How long a spring takes to settle, in milliseconds.
51#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
52#[unsafe(no_mangle)]
53pub extern "C" fn solium_spring_settle_ms(
54    stiffness: f64,
55    damping: f64,
56    mass: f64,
57    initial_velocity: f64,
58) -> f64 {
59    spring(stiffness, damping, mass, initial_velocity)
60        .settle_time()
61        .as_secs_f64()
62        * 1000.0
63}
64
65/// Progress of a fixed-duration animation, for callers that would rather not
66/// divide by the duration themselves.
67#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
68#[unsafe(no_mangle)]
69pub extern "C" fn solium_progress(index: u32, duration_ms: f64, elapsed_ms: f64) -> f64 {
70    let animation = Animation::new(
71        Duration::ZERO,
72        Duration::from_secs_f64(duration_ms.max(0.0) / 1000.0),
73        curve(index),
74    );
75    animation.progress(Duration::from_secs_f64(elapsed_ms.max(0.0) / 1000.0))
76}
77
78fn spring(stiffness: f64, damping: f64, mass: f64, initial_velocity: f64) -> Spring {
79    Spring {
80        stiffness,
81        damping,
82        mass,
83        initial_velocity,
84        ..Spring::default()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn the_exported_surface_agrees_with_the_engine() {
94        // The preview must not be able to see a different answer than the
95        // compositor does.
96        for index in 0..solium_curve_count() {
97            for step in 0..=10 {
98                let t = f64::from(step) / 10.0;
99                assert!((solium_curve_at(index, t) - curve(index).at(t)).abs() < f64::EPSILON);
100            }
101        }
102    }
103
104    #[test]
105    fn an_out_of_range_curve_falls_back_rather_than_panicking() {
106        // Reachable from a page that is out of date with the engine.
107        assert!((solium_curve_at(9999, 1.0) - Curve::default().at(1.0)).abs() < f64::EPSILON);
108    }
109
110    #[test]
111    fn settling_is_reported_in_milliseconds() {
112        let millis = solium_spring_settle_ms(300.0, 25.0, 1.0, 0.0);
113        assert!(millis > 50.0 && millis < 3000.0, "got {millis}ms");
114    }
115}