Skip to main content

agent/avatar/
motion.rs

1//! What makes it alive — a wobble in the outline, a breath in the body, a
2//! drift in the gaze, a blink. Every one is a function of `t` and nothing is
3//! stored between frames, so a paused page is an honest still.
4
5use crate::avatar::shape::Shape;
6use std::f32::consts::PI;
7
8/// Seconds between blinks, and how long one takes.
9const BLINK_EVERY: f32 = 4.4;
10const BLINK_FOR: f32 = 0.16;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Motion {
14    /// Radians per second the lobe phases travel.
15    pub wobble: f32,
16    /// How much the whole body breathes, as a fraction of its radius.
17    pub breathe: f32,
18    /// How far the gaze wanders, in units of the body radius.
19    pub drift: f32,
20    pub blink: bool,
21}
22
23impl Default for Motion {
24    fn default() -> Self {
25        Self::ALIVE
26    }
27}
28
29impl Motion {
30    pub const STILL: Self = Self {
31        wobble: 0.0,
32        breathe: 0.0,
33        drift: 0.0,
34        blink: false,
35    };
36    pub const ALIVE: Self = Self {
37        wobble: 0.34,
38        breathe: 0.014,
39        drift: 0.03,
40        blink: true,
41    };
42
43    /// The outline at `t`. Each harmonic travels at its own rate, which is what
44    /// keeps the wobble from reading as the whole body rotating.
45    pub fn shape(&self, mut shape: Shape, t: f32) -> Shape {
46        if self.wobble == 0.0 {
47            return shape;
48        }
49        for l in &mut shape.lobes {
50            l.phase += t * self.wobble * (0.7 + 0.13 * l.k as f32);
51        }
52        shape
53    }
54
55    /// Everything that moves without changing the outline.
56    pub fn beat(&self, t: f32) -> Beat {
57        let lid = if self.blink {
58            let p = (t / BLINK_EVERY).fract() * BLINK_EVERY;
59            if p < BLINK_FOR {
60                (PI * p / BLINK_FOR).sin()
61            } else {
62                0.0
63            }
64        } else {
65            0.0
66        };
67        Beat {
68            scale: 1.0 + self.breathe * (0.9 * t).sin(),
69            gaze: (
70                self.drift * (0.6 * (0.31 * t).sin() + 0.4 * (0.17 * t + 1.3).sin()),
71                self.drift * (0.5 * (0.23 * t + 0.7).sin() + 0.5 * (0.13 * t).sin()),
72            ),
73            lid,
74        }
75    }
76}
77
78/// The moving parts at one instant.
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct Beat {
81    pub scale: f32,
82    pub gaze: (f32, f32),
83    /// How shut the eyes are: 0 open, 1 closed.
84    pub lid: f32,
85}