Skip to main content

rust_motion_animation/
random.rs

1//! Remotion-compatible deterministic pseudo-random number generator (`random()`).
2//!
3//! Provides Mulberry32 PRNG and 32-bit Java `hashCode` computation, matching
4//! Remotion's `random(seed)` function exactly.
5
6use std::fmt;
7
8/// A seed value for [`random`].
9///
10/// Can be constructed from strings (`&str`, `String`) or numbers (`f64`, `f32`, `i32`, `i64`, `u32`, `u64`, `usize`).
11#[derive(Clone, Debug, PartialEq)]
12pub enum RandomSeed {
13    /// Textual seed.
14    Str(String),
15    /// Numeric seed.
16    Num(f64),
17}
18
19impl fmt::Display for RandomSeed {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            RandomSeed::Str(s) => write!(f, "{}", s),
23            RandomSeed::Num(n) => write!(f, "{}", n),
24        }
25    }
26}
27
28impl From<&str> for RandomSeed {
29    fn from(s: &str) -> Self {
30        RandomSeed::Str(s.to_string())
31    }
32}
33
34impl From<String> for RandomSeed {
35    fn from(s: String) -> Self {
36        RandomSeed::Str(s)
37    }
38}
39
40impl From<&String> for RandomSeed {
41    fn from(s: &String) -> Self {
42        RandomSeed::Str(s.clone())
43    }
44}
45
46impl From<f64> for RandomSeed {
47    fn from(n: f64) -> Self {
48        RandomSeed::Num(n)
49    }
50}
51
52impl From<f32> for RandomSeed {
53    fn from(n: f32) -> Self {
54        RandomSeed::Num(n as f64)
55    }
56}
57
58impl From<i32> for RandomSeed {
59    fn from(n: i32) -> Self {
60        RandomSeed::Num(n as f64)
61    }
62}
63
64impl From<i64> for RandomSeed {
65    fn from(n: i64) -> Self {
66        RandomSeed::Num(n as f64)
67    }
68}
69
70impl From<u32> for RandomSeed {
71    fn from(n: u32) -> Self {
72        RandomSeed::Num(n as f64)
73    }
74}
75
76impl From<u64> for RandomSeed {
77    fn from(n: u64) -> Self {
78        RandomSeed::Num(n as f64)
79    }
80}
81
82impl From<usize> for RandomSeed {
83    fn from(n: usize) -> Self {
84        RandomSeed::Num(n as f64)
85    }
86}
87
88/// Computes 32-bit Java-compatible string hash code using UTF-16 code units.
89///
90/// Matches Remotion's `hashCode(str)` exactly:
91/// `hash = ((hash << 5) - hash + charCode) | 0`
92pub fn hash_code(s: &str) -> i32 {
93    let mut hash: i32 = 0;
94    for code_unit in s.encode_utf16() {
95        hash = hash.wrapping_mul(31).wrapping_add(code_unit as i32);
96    }
97    hash
98}
99
100/// Remotion Mulberry32 32-bit pseudo-random generator.
101///
102/// Takes a 32-bit integer seed and returns a deterministic floating-point number in `[0.0, 1.0)`.
103#[inline]
104pub fn mulberry32(a: i64) -> f64 {
105    let a_u32 = a as u32;
106    let t0 = a_u32.wrapping_add(0x6D2B79F5);
107    let t1 = (t0 ^ (t0 >> 15)).wrapping_mul(t0 | 1);
108    let t2 = t1 ^ t1.wrapping_add((t1 ^ (t1 >> 7)).wrapping_mul(t1 | 61));
109    let result = t2 ^ (t2 >> 14);
110    (result as f64) / 4294967296.0
111}
112
113/// Deterministic pseudo-random number generator matching Remotion's `random(seed)`.
114///
115/// - For string seeds: `mulberry32(hashCode(seed))`
116/// - For numeric seeds: `mulberry32(seed * 10_000_000_000)`
117///
118/// # Example
119/// ```rust
120/// use rust_motion_animation::random;
121///
122/// let r1 = random("seed-1");
123/// let r2 = random("seed-1");
124/// assert_eq!(r1, r2);
125/// assert!(r1 >= 0.0 && r1 < 1.0);
126/// ```
127#[inline]
128pub fn random(seed: impl Into<RandomSeed>) -> f64 {
129    match seed.into() {
130        RandomSeed::Str(s) => mulberry32(hash_code(&s) as i64),
131        RandomSeed::Num(n) => mulberry32((n * 10_000_000_000.0) as i64),
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_random_deterministic() {
141        let r1 = random("remotion-seed");
142        let r2 = random("remotion-seed");
143        assert_eq!(r1, r2);
144        assert!((0.0..1.0).contains(&r1));
145
146        let n1 = random(42);
147        let n2 = random(42);
148        assert_eq!(n1, n2);
149        assert!((0.0..1.0).contains(&n1));
150        assert_ne!(r1, n1);
151    }
152
153    #[test]
154    fn test_hash_code_known_values() {
155        assert_eq!(hash_code(""), 0);
156        assert_eq!(hash_code("a"), 97);
157        assert_eq!(hash_code("hello"), 99162322);
158    }
159}