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/// Seconds the gaze takes to sweep side to side and back.
12const SCAN_EVERY: f32 = 1.2;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Motion {
16    /// Radians per second the lobe phases travel.
17    pub wobble: f32,
18    /// How much the whole body breathes, as a fraction of its radius.
19    pub breathe: f32,
20    /// How far the gaze wanders, in units of the body radius.
21    pub drift: f32,
22    pub blink: bool,
23    /// How far the gaze sweeps side to side, in units of the body radius. A
24    /// sweep of a quarter radius moves the eyes a cell in a [`mascot`].
25    ///
26    /// [`mascot`]: crate::avatar::mascot
27    pub scan: f32,
28    /// How shut the eyes rest, 0 open to 1 closed. A blink shuts them further
29    /// and never opens them past this.
30    pub lid: f32,
31}
32
33impl Default for Motion {
34    fn default() -> Self {
35        Self::ALIVE
36    }
37}
38
39impl Motion {
40    pub const STILL: Self = Self {
41        wobble: 0.0,
42        breathe: 0.0,
43        drift: 0.0,
44        blink: false,
45        scan: 0.0,
46        lid: 0.0,
47    };
48    pub const ALIVE: Self = Self {
49        wobble: 0.34,
50        breathe: 0.014,
51        drift: 0.03,
52        blink: true,
53        scan: 0.0,
54        lid: 0.0,
55    };
56    /// Alive, its gaze sweeping side to side.
57    pub const BUSY: Self = Self {
58        scan: 0.3,
59        ..Self::ALIVE
60    };
61    /// Eyes shut, the body still but for its breath.
62    pub const ASLEEP: Self = Self {
63        wobble: 0.0,
64        breathe: 0.014,
65        drift: 0.0,
66        blink: false,
67        scan: 0.0,
68        lid: 1.0,
69    };
70
71    /// How far the gaze can wander from rest, drift and sweep together.
72    pub fn reach(&self) -> f32 {
73        self.drift + self.scan
74    }
75
76    /// The gaze's side-to-side offset at `t`.
77    pub fn sweep(&self, t: f32) -> f32 {
78        self.scan * (2.0 * PI * t / SCAN_EVERY).sin()
79    }
80
81    /// The outline at `t`. Each harmonic travels at its own rate, which is what
82    /// keeps the wobble from reading as the whole body rotating.
83    pub fn shape(&self, mut shape: Shape, t: f32) -> Shape {
84        if self.wobble == 0.0 {
85            return shape;
86        }
87        for l in &mut shape.lobes {
88            l.phase += t * self.wobble * (0.7 + 0.13 * l.k as f32);
89        }
90        shape
91    }
92
93    /// Everything that moves without changing the outline.
94    pub fn beat(&self, t: f32) -> Beat {
95        let blink = if self.blink {
96            let p = (t / BLINK_EVERY).fract() * BLINK_EVERY;
97            if p < BLINK_FOR {
98                (PI * p / BLINK_FOR).sin()
99            } else {
100                0.0
101            }
102        } else {
103            0.0
104        };
105        let lid = blink.max(self.lid.clamp(0.0, 1.0));
106        Beat {
107            scale: 1.0 + self.breathe * (0.9 * t).sin(),
108            gaze: (
109                self.drift * (0.6 * (0.31 * t).sin() + 0.4 * (0.17 * t + 1.3).sin())
110                    + self.sweep(t),
111                self.drift * (0.5 * (0.23 * t + 0.7).sin() + 0.5 * (0.13 * t).sin()),
112            ),
113            lid,
114        }
115    }
116}
117
118/// The moving parts at one instant.
119#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct Beat {
121    pub scale: f32,
122    pub gaze: (f32, f32),
123    /// How shut the eyes are: 0 open, 1 closed.
124    pub lid: f32,
125}