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    /// How shut the eyes rest, 0 open to 1 closed. A blink shuts them further
22    /// and never opens them past this.
23    pub lid: f32,
24}
25
26impl Default for Motion {
27    fn default() -> Self {
28        Self::ALIVE
29    }
30}
31
32impl Motion {
33    pub const STILL: Self = Self {
34        wobble: 0.0,
35        breathe: 0.0,
36        drift: 0.0,
37        blink: false,
38        lid: 0.0,
39    };
40    pub const ALIVE: Self = Self {
41        wobble: 0.34,
42        breathe: 0.014,
43        drift: 0.03,
44        blink: true,
45        lid: 0.0,
46    };
47    /// Eyes shut, the body still but for its breath.
48    pub const ASLEEP: Self = Self {
49        wobble: 0.0,
50        breathe: 0.014,
51        drift: 0.0,
52        blink: false,
53        lid: 1.0,
54    };
55
56    /// The outline at `t`. Each harmonic travels at its own rate, which is what
57    /// keeps the wobble from reading as the whole body rotating.
58    pub fn shape(&self, mut shape: Shape, t: f32) -> Shape {
59        if self.wobble == 0.0 {
60            return shape;
61        }
62        for l in &mut shape.lobes {
63            l.phase += t * self.wobble * (0.7 + 0.13 * l.k as f32);
64        }
65        shape
66    }
67
68    /// Everything that moves without changing the outline.
69    pub fn beat(&self, t: f32) -> Beat {
70        let blink = if self.blink {
71            let p = (t / BLINK_EVERY).fract() * BLINK_EVERY;
72            if p < BLINK_FOR {
73                (PI * p / BLINK_FOR).sin()
74            } else {
75                0.0
76            }
77        } else {
78            0.0
79        };
80        let lid = blink.max(self.lid.clamp(0.0, 1.0));
81        Beat {
82            scale: 1.0 + self.breathe * (0.9 * t).sin(),
83            gaze: (
84                self.drift * (0.6 * (0.31 * t).sin() + 0.4 * (0.17 * t + 1.3).sin()),
85                self.drift * (0.5 * (0.23 * t + 0.7).sin() + 0.5 * (0.13 * t).sin()),
86            ),
87            lid,
88        }
89    }
90}
91
92/// The moving parts at one instant.
93#[derive(Clone, Copy, Debug, PartialEq)]
94pub struct Beat {
95    pub scale: f32,
96    pub gaze: (f32, f32),
97    /// How shut the eyes are: 0 open, 1 closed.
98    pub lid: f32,
99}