Skip to main content

agent/avatar/
eyes.rs

1//! The eye rig, in units of the body radius, and the one solve that keeps it
2//! inside whatever silhouette it was handed.
3
4use crate::avatar::shape::Shape;
5
6/// Clearance the fit aims for: the eye cluster's furthest corner sits this far
7/// along the outline's reach, never at it.
8const MARGIN: f32 = 0.86;
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Eyes {
12    /// Half-width of one eye.
13    pub rx: f32,
14    /// Height against width. Past ~1.5 the eye reads as a capsule.
15    pub ratio: f32,
16    /// Half the centre-to-centre distance.
17    pub gap: f32,
18    /// How high on the body the pair sits.
19    pub rise: f32,
20    /// Degrees, mirrored between the two.
21    pub lean: f32,
22    /// Superellipse exponent — 2 is an ellipse, 5 a rounded slab.
23    pub n: f32,
24}
25
26impl Default for Eyes {
27    fn default() -> Self {
28        Self::CALM
29    }
30}
31
32impl Eyes {
33    pub const CALM: Self = Self {
34        rx: 0.15,
35        ratio: 1.9,
36        gap: 0.34,
37        rise: 0.04,
38        lean: 0.0,
39        n: 4.0,
40    };
41    pub const WIDE: Self = Self {
42        rx: 0.2,
43        ratio: 1.15,
44        gap: 0.4,
45        ..Self::CALM
46    };
47    pub const KEEN: Self = Self {
48        rx: 0.12,
49        ratio: 2.8,
50        lean: 12.0,
51        ..Self::CALM
52    };
53    pub const SLEEPY: Self = Self {
54        rx: 0.19,
55        ratio: 0.42,
56        rise: 0.0,
57        n: 5.0,
58        ..Self::CALM
59    };
60    pub const TALL: Self = Self {
61        rx: 0.11,
62        ratio: 3.2,
63        gap: 0.32,
64        ..Self::CALM
65    };
66    /// `n` near 2 is a true ellipse, which at this ratio is a circle.
67    pub const DOTS: Self = Self {
68        rx: 0.11,
69        ratio: 1.0,
70        gap: 0.3,
71        n: 2.2,
72        ..Self::CALM
73    };
74    /// A lean only reads on an elongated eye — a round one looks the same at
75    /// every angle.
76    pub const SLY: Self = Self {
77        rx: 0.12,
78        ratio: 2.5,
79        lean: 16.0,
80        ..Self::CALM
81    };
82    pub const CLOSE: Self = Self {
83        gap: 0.22,
84        ..Self::CALM
85    };
86    pub const FAR: Self = Self {
87        gap: 0.5,
88        ..Self::CALM
89    };
90    pub const LOW: Self = Self {
91        rise: -0.16,
92        ..Self::CALM
93    };
94
95    pub const PRESETS: [(&'static str, Self); 10] = [
96        ("calm", Self::CALM),
97        ("wide", Self::WIDE),
98        ("keen", Self::KEEN),
99        ("tall", Self::TALL),
100        ("dots", Self::DOTS),
101        ("sly", Self::SLY),
102        ("sleepy", Self::SLEEPY),
103        ("close", Self::CLOSE),
104        ("far", Self::FAR),
105        ("low", Self::LOW),
106    ];
107}
108
109impl From<u64> for Eyes {
110    fn from(seed: u64) -> Self {
111        // Offset so a name's eyes do not follow its silhouette in lockstep.
112        let mut h = seed ^ 0x5bf0_3635_ca62_9d7f;
113        let mut unit = || {
114            h = (h ^ (h >> 33)).wrapping_mul(0xff51_afd7_ed55_8ccd);
115            (h >> 40) as f32 / 16_777_216.0
116        };
117        let range = |u: f32, lo: f32, hi: f32| lo + u * (hi - lo);
118        Self {
119            rx: range(unit(), 0.12, 0.19),
120            ratio: range(unit(), 1.1, 2.6),
121            gap: range(unit(), 0.28, 0.42),
122            rise: range(unit(), -0.02, 0.12),
123            lean: range(unit(), -12.0, 12.0),
124            n: range(unit(), 2.6, 5.5),
125        }
126    }
127}
128
129/// One eye where it lands, in the same unit space as [`Shape::outline`].
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub struct Eye {
132    pub cx: f32,
133    pub cy: f32,
134    pub rx: f32,
135    pub ry: f32,
136    pub rot: f32,
137    pub n: f32,
138}
139
140impl Eyes {
141    /// Fits the pair against the body's real radius in each eye's own
142    /// direction.
143    ///
144    /// Solved once and never again: a fit that re-ran per frame would resize
145    /// the eyes as the gaze drifted, which is the tremble bloub documents. So
146    /// `drift` — how far motion may later wander — is spent here, at the worst
147    /// case over everywhere it can reach.
148    pub fn place(&self, shape: &Shape, drift: f32) -> [Eye; 2] {
149        let cy = -self.rise;
150        let reach = self.rx.hypot(self.rx * self.ratio);
151        let wander = drift * std::f32::consts::SQRT_2;
152        let fit = [-self.gap, self.gap]
153            .iter()
154            .map(|&cx| {
155                let room = [
156                    (-drift, -drift),
157                    (drift, -drift),
158                    (-drift, drift),
159                    (drift, drift),
160                ]
161                .iter()
162                .map(|(dx, dy)| shape.reach(cx + dx, cy + dy))
163                .fold(f32::INFINITY, f32::min)
164                    * MARGIN;
165                let want = cx.hypot(cy) + reach;
166                if want + wander > room {
167                    (room - wander) / want
168                } else {
169                    1.0
170                }
171            })
172            .fold(1.0f32, f32::min);
173
174        [-1.0f32, 1.0].map(|side| Eye {
175            cx: side * self.gap * fit,
176            cy: cy * fit,
177            rx: self.rx * fit,
178            ry: self.rx * self.ratio * fit,
179            rot: side * self.lean,
180            n: self.n,
181        })
182    }
183}