Skip to main content

oxihuman_morph/
blink_control.rs

1//! Eye blink controller with natural variation.
2
3#[allow(dead_code)]
4pub struct BlinkParams {
5    /// Seconds for one complete blink.
6    pub blink_duration: f32,
7    /// Average blinks per second.
8    pub blink_rate_hz: f32,
9    /// Timing randomness factor.
10    pub variation: f32,
11    /// Eye close speed multiplier.
12    pub close_speed: f32,
13    /// Eye open speed multiplier.
14    pub open_speed: f32,
15}
16
17#[allow(dead_code)]
18#[derive(Clone, Copy, PartialEq, Debug)]
19pub enum BlinkPhase {
20    Open,
21    Closing,
22    Closed,
23    Opening,
24}
25
26#[allow(dead_code)]
27pub struct BlinkState {
28    pub phase: BlinkPhase,
29    pub phase_time: f32,
30    pub next_blink_time: f32,
31    /// 0 = closed, 1 = open.
32    pub left_eye_open: f32,
33    pub right_eye_open: f32,
34    pub synchronized: bool,
35    pub enabled: bool,
36}
37
38// ---------------------------------------------------------------------------
39// LCG RNG helper (no external deps)
40// ---------------------------------------------------------------------------
41
42/// Linear congruential generator. Advances `state` and returns a value in [0, 1).
43#[allow(dead_code)]
44pub fn lcg_next(state: &mut u64) -> f32 {
45    // Parameters from Knuth / Numerical Recipes (64-bit LCG).
46    *state = state
47        .wrapping_mul(6_364_136_223_846_793_005)
48        .wrapping_add(1_442_695_040_888_963_407);
49    // Use upper bits for better quality.
50    let bits = (*state >> 33) as u32;
51    (bits as f32) / (u32::MAX as f32 + 1.0)
52}
53
54// ---------------------------------------------------------------------------
55// Construction
56// ---------------------------------------------------------------------------
57
58#[allow(dead_code)]
59pub fn default_blink_params() -> BlinkParams {
60    BlinkParams {
61        blink_duration: 0.15,
62        blink_rate_hz: 0.25,
63        variation: 0.3,
64        close_speed: 1.0,
65        open_speed: 1.0,
66    }
67}
68
69#[allow(dead_code)]
70pub fn new_blink_state(sync: bool) -> BlinkState {
71    BlinkState {
72        phase: BlinkPhase::Open,
73        phase_time: 0.0,
74        next_blink_time: 3.0,
75        left_eye_open: 1.0,
76        right_eye_open: 1.0,
77        synchronized: sync,
78        enabled: true,
79    }
80}
81
82// ---------------------------------------------------------------------------
83// Update
84// ---------------------------------------------------------------------------
85
86#[allow(dead_code)]
87pub fn update_blink(state: &mut BlinkState, params: &BlinkParams, dt: f32, rng: &mut u64) {
88    if !state.enabled {
89        return;
90    }
91
92    state.phase_time += dt;
93
94    match state.phase {
95        BlinkPhase::Open => {
96            state.left_eye_open = 1.0;
97            state.right_eye_open = 1.0;
98            if state.phase_time >= state.next_blink_time {
99                state.phase = BlinkPhase::Closing;
100                state.phase_time = 0.0;
101            }
102        }
103        BlinkPhase::Closing => {
104            let half = params.blink_duration * 0.5 / params.close_speed.max(0.001);
105            let t = (state.phase_time / half).min(1.0);
106            let v = 1.0 - t;
107            state.left_eye_open = v;
108            if state.synchronized {
109                state.right_eye_open = v;
110            } else {
111                state.right_eye_open = (v + lcg_next(rng) * 0.1).min(1.0);
112            }
113            if state.phase_time >= half {
114                state.phase = BlinkPhase::Closed;
115                state.phase_time = 0.0;
116                state.left_eye_open = 0.0;
117                state.right_eye_open = 0.0;
118            }
119        }
120        BlinkPhase::Closed => {
121            let hold = params.blink_duration * 0.1;
122            if state.phase_time >= hold {
123                state.phase = BlinkPhase::Opening;
124                state.phase_time = 0.0;
125            }
126        }
127        BlinkPhase::Opening => {
128            let half = params.blink_duration * 0.5 / params.open_speed.max(0.001);
129            let t = (state.phase_time / half).min(1.0);
130            state.left_eye_open = t;
131            if state.synchronized {
132                state.right_eye_open = t;
133            } else {
134                state.right_eye_open = (t + lcg_next(rng) * 0.05).min(1.0);
135            }
136            if state.phase_time >= half {
137                state.phase = BlinkPhase::Open;
138                state.phase_time = 0.0;
139                state.left_eye_open = 1.0;
140                state.right_eye_open = 1.0;
141                // Schedule next blink with variation.
142                let base = 1.0 / params.blink_rate_hz.max(0.001);
143                let var = (lcg_next(rng) * 2.0 - 1.0) * params.variation * base;
144                state.next_blink_time = (base + var).max(0.1);
145            }
146        }
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Control
152// ---------------------------------------------------------------------------
153
154#[allow(dead_code)]
155pub fn trigger_manual_blink(state: &mut BlinkState) {
156    if state.phase == BlinkPhase::Open {
157        state.phase = BlinkPhase::Closing;
158        state.phase_time = 0.0;
159    }
160}
161
162#[allow(dead_code)]
163pub fn blink_value(state: &BlinkState) -> f32 {
164    (state.left_eye_open + state.right_eye_open) * 0.5
165}
166
167#[allow(dead_code)]
168pub fn set_blink_synchronized(state: &mut BlinkState, sync: bool) {
169    state.synchronized = sync;
170}
171
172#[allow(dead_code)]
173pub fn enable_blink(state: &mut BlinkState) {
174    state.enabled = true;
175}
176
177#[allow(dead_code)]
178pub fn disable_blink(state: &mut BlinkState) {
179    state.enabled = false;
180}
181
182#[allow(dead_code)]
183pub fn is_blinking(state: &BlinkState) -> bool {
184    state.phase != BlinkPhase::Open
185}
186
187#[allow(dead_code)]
188pub fn force_open_eyes(state: &mut BlinkState) {
189    state.phase = BlinkPhase::Open;
190    state.phase_time = 0.0;
191    state.left_eye_open = 1.0;
192    state.right_eye_open = 1.0;
193}
194
195#[allow(dead_code)]
196pub fn force_close_eyes(state: &mut BlinkState) {
197    state.phase = BlinkPhase::Closed;
198    state.phase_time = 0.0;
199    state.left_eye_open = 0.0;
200    state.right_eye_open = 0.0;
201}
202
203#[allow(dead_code)]
204pub fn blink_speed_for_emotion(emotion: &str) -> f32 {
205    match emotion {
206        "surprised" => 2.0,
207        "tired" => 0.5,
208        _ => 1.0,
209    }
210}
211
212// ---------------------------------------------------------------------------
213// Tests
214// ---------------------------------------------------------------------------
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_default_params() {
222        let p = default_blink_params();
223        assert!(p.blink_duration > 0.0);
224        assert!(p.blink_rate_hz > 0.0);
225        assert!(p.variation >= 0.0);
226        assert!(p.close_speed > 0.0);
227        assert!(p.open_speed > 0.0);
228    }
229
230    #[test]
231    fn test_new_state_defaults() {
232        let state = new_blink_state(true);
233        assert_eq!(state.phase, BlinkPhase::Open);
234        assert!((state.left_eye_open - 1.0).abs() < 1e-6);
235        assert!((state.right_eye_open - 1.0).abs() < 1e-6);
236        assert!(state.synchronized);
237        assert!(state.enabled);
238    }
239
240    #[test]
241    fn test_update_advances_phase() {
242        let params = default_blink_params();
243        let mut state = new_blink_state(true);
244        // Move past next_blink_time to trigger a blink.
245        let mut rng: u64 = 42;
246        let trigger_time = state.next_blink_time + 0.01;
247        update_blink(&mut state, &params, trigger_time, &mut rng);
248        assert_ne!(state.phase, BlinkPhase::Open);
249    }
250
251    #[test]
252    fn test_trigger_manual_blink() {
253        let mut state = new_blink_state(true);
254        assert_eq!(state.phase, BlinkPhase::Open);
255        trigger_manual_blink(&mut state);
256        assert_eq!(state.phase, BlinkPhase::Closing);
257    }
258
259    #[test]
260    fn test_blink_value_open() {
261        let state = new_blink_state(true);
262        assert!((blink_value(&state) - 1.0).abs() < 1e-6);
263    }
264
265    #[test]
266    fn test_blink_value_closed() {
267        let mut state = new_blink_state(true);
268        force_close_eyes(&mut state);
269        assert!((blink_value(&state)).abs() < 1e-6);
270    }
271
272    #[test]
273    fn test_enable_disable() {
274        let mut state = new_blink_state(false);
275        disable_blink(&mut state);
276        assert!(!state.enabled);
277        enable_blink(&mut state);
278        assert!(state.enabled);
279    }
280
281    #[test]
282    fn test_is_blinking_open() {
283        let state = new_blink_state(true);
284        assert!(!is_blinking(&state));
285    }
286
287    #[test]
288    fn test_is_blinking_closing() {
289        let mut state = new_blink_state(true);
290        trigger_manual_blink(&mut state);
291        assert!(is_blinking(&state));
292    }
293
294    #[test]
295    fn test_force_open_eyes() {
296        let mut state = new_blink_state(true);
297        force_close_eyes(&mut state);
298        force_open_eyes(&mut state);
299        assert_eq!(state.phase, BlinkPhase::Open);
300        assert!((state.left_eye_open - 1.0).abs() < 1e-6);
301    }
302
303    #[test]
304    fn test_force_close_eyes() {
305        let mut state = new_blink_state(true);
306        force_close_eyes(&mut state);
307        assert_eq!(state.phase, BlinkPhase::Closed);
308        assert!((state.left_eye_open).abs() < 1e-6);
309    }
310
311    #[test]
312    fn test_blink_speed_for_emotion() {
313        assert!((blink_speed_for_emotion("surprised") - 2.0).abs() < 1e-6);
314        assert!((blink_speed_for_emotion("tired") - 0.5).abs() < 1e-6);
315        assert!((blink_speed_for_emotion("neutral") - 1.0).abs() < 1e-6);
316    }
317
318    #[test]
319    fn test_lcg_next_range() {
320        let mut rng: u64 = 123456;
321        for _ in 0..1000 {
322            let v = lcg_next(&mut rng);
323            assert!((0.0..1.0).contains(&v), "lcg_next out of [0,1): {}", v);
324        }
325    }
326
327    #[test]
328    fn test_set_blink_synchronized() {
329        let mut state = new_blink_state(true);
330        set_blink_synchronized(&mut state, false);
331        assert!(!state.synchronized);
332        set_blink_synchronized(&mut state, true);
333        assert!(state.synchronized);
334    }
335
336    #[test]
337    fn test_full_blink_cycle() {
338        let params = default_blink_params();
339        let mut state = new_blink_state(true);
340        let mut rng: u64 = 999;
341        trigger_manual_blink(&mut state);
342        // Step through closing
343        for _ in 0..100 {
344            update_blink(&mut state, &params, 0.01, &mut rng);
345        }
346        // After enough time the cycle should complete and return to Open
347        assert_eq!(state.phase, BlinkPhase::Open);
348    }
349}