1mod eyes;
12mod motion;
13mod shape;
14
15pub use eyes::{Eye, Eyes};
16pub use motion::{Beat, Motion};
17pub use shape::{Lobe, SAMPLES, Shape, seed};
18
19use gpui::{
20 AnyElement, Bounds, Hsla, IntoElement, PathBuilder, Pixels, Point, Styled, Window, canvas,
21 hsla, point, px,
22};
23use theme::{Theme, contrast_ratio, flatten};
24
25const FILL: f32 = 0.4;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct Face {
31 pub shape: Shape,
32 pub eyes: Eyes,
33 pub motion: Motion,
34 pub color: Option<Hsla>,
36}
37
38impl Face {
39 pub fn new(shape: Shape) -> Self {
40 Self {
41 shape,
42 eyes: Eyes::default(),
43 motion: Motion::default(),
44 color: None,
45 }
46 }
47
48 pub fn eyes(mut self, eyes: Eyes) -> Self {
49 self.eyes = eyes;
50 self
51 }
52
53 pub fn motion(mut self, motion: Motion) -> Self {
54 self.motion = motion;
55 self
56 }
57
58 pub fn color(mut self, color: Hsla) -> Self {
59 self.color = Some(color);
60 self
61 }
62}
63
64impl From<u64> for Face {
65 fn from(seed: u64) -> Self {
66 Self::new(Shape::from(seed)).eyes(Eyes::from(seed))
67 }
68}
69
70impl From<&str> for Face {
71 fn from(name: &str) -> Self {
72 Self::from(seed(name))
73 }
74}
75
76impl Face {
77 pub fn pose(&self, t: f32) -> Pose {
80 let beat = self.motion.beat(t);
81 let shape = self.motion.shape(self.shape, t);
82 let breath = |(x, y): (f32, f32)| (x * beat.scale, y * beat.scale);
83 Pose {
84 outline: shape.outline().map(breath),
85 eyes: self.eyes.place(&shape, self.motion.drift).map(|e| {
86 let (cx, cy) = breath((e.cx + beat.gaze.0, e.cy + beat.gaze.1));
87 Eye {
88 cx,
89 cy,
90 rx: e.rx * beat.scale,
91 ry: e.ry * beat.scale * (1.0 - beat.lid * 0.92),
92 ..e
93 }
94 }),
95 color: self.color,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq)]
104pub struct Pose {
105 pub outline: [(f32, f32); SAMPLES],
106 pub eyes: [Eye; 2],
107 pub color: Option<Hsla>,
108}
109
110impl Pose {
111 pub fn lerp(a: &Self, b: &Self, k: f32) -> Self {
114 let k = k.clamp(0.0, 1.0);
115 let f = |x: f32, y: f32| x + (y - x) * k;
116 Self {
117 outline: std::array::from_fn(|i| {
118 (
119 f(a.outline[i].0, b.outline[i].0),
120 f(a.outline[i].1, b.outline[i].1),
121 )
122 }),
123 eyes: std::array::from_fn(|i| Eye {
124 cx: f(a.eyes[i].cx, b.eyes[i].cx),
125 cy: f(a.eyes[i].cy, b.eyes[i].cy),
126 rx: f(a.eyes[i].rx, b.eyes[i].rx),
127 ry: f(a.eyes[i].ry, b.eyes[i].ry),
128 rot: f(a.eyes[i].rot, b.eyes[i].rot),
129 n: f(a.eyes[i].n, b.eyes[i].n),
130 }),
131 color: match (a.color, b.color) {
134 (Some(x), Some(y)) => Some(flatten(hsla(y.h, y.s, y.l, k), x)),
135 (x, y) => {
136 if k < 0.5 {
137 x
138 } else {
139 y
140 }
141 }
142 },
143 }
144 }
145}
146
147pub fn avatar(pose: Pose) -> AnyElement {
149 canvas(
150 move |_: Bounds<Pixels>, _, _| (),
151 move |bounds, (), window, cx| {
152 let theme = Theme::of(cx);
153 let head = pose.color.unwrap_or(theme.accent);
154 let ink = if contrast_ratio(head, theme.bg) >= contrast_ratio(head, theme.text) {
156 theme.bg
157 } else {
158 theme.text
159 };
160 paint(window, bounds, &pose, head, ink);
161 },
162 )
163 .size_full()
164 .into_any_element()
165}
166
167fn paint(window: &mut Window, bounds: Bounds<Pixels>, pose: &Pose, head: Hsla, ink: Hsla) {
168 let span = bounds.size.width.min(bounds.size.height).to_f64() as f32;
169 let unit = span * FILL;
170 let mid = bounds.center();
171 let map = |x: f32, y: f32| point(mid.x + px(x * unit), mid.y + px(y * unit));
172
173 window.paint_layer(bounds, |window| {
174 fill(
175 window,
176 map(pose.outline[0].0, pose.outline[0].1),
177 spline(&pose.outline, &map),
178 head,
179 );
180 for eye in &pose.eyes {
181 let (start, segs) = superellipse(eye, &map);
182 fill(window, start, segs, ink);
183 }
184 });
185}
186
187type Cubic = (Point<Pixels>, Point<Pixels>, Point<Pixels>);
188
189fn fill(window: &mut Window, start: Point<Pixels>, segs: Vec<Cubic>, color: Hsla) {
190 let mut b = PathBuilder::fill();
191 b.move_to(start);
192 for (end, c1, c2) in segs {
193 b.cubic_bezier_to(end, c1, c2);
194 }
195 b.close();
196 if let Ok(path) = b.build() {
197 window.paint_path(path, color);
198 }
199}
200
201fn spline(pts: &[(f32, f32); SAMPLES], map: &impl Fn(f32, f32) -> Point<Pixels>) -> Vec<Cubic> {
204 let at = |i: isize| pts[i.rem_euclid(SAMPLES as isize) as usize];
205 (0..SAMPLES as isize)
206 .map(|i| {
207 let ((x0, y0), (x1, y1), (x2, y2), (x3, y3)) = (at(i - 1), at(i), at(i + 1), at(i + 2));
208 (
209 map(x2, y2),
210 map(x1 + (x2 - x0) / 6.0, y1 + (y2 - y0) / 6.0),
211 map(x2 - (x3 - x1) / 6.0, y2 - (y3 - y1) / 6.0),
212 )
213 })
214 .collect()
215}
216
217fn superellipse(e: &Eye, map: &impl Fn(f32, f32) -> Point<Pixels>) -> (Point<Pixels>, Vec<Cubic>) {
220 let k = ((8.0 * 2f32.powf(-1.0 / e.n) - 4.0) / 3.0).min(1.0);
223 let (ak, bk) = (e.rx * k, e.ry * k);
224 let (rx, ry) = (e.rx, e.ry);
225 let pts = [
226 (rx, 0.0),
227 (rx, bk),
228 (ak, ry),
229 (0.0, ry),
230 (-ak, ry),
231 (-rx, bk),
232 (-rx, 0.0),
233 (-rx, -bk),
234 (-ak, -ry),
235 (0.0, -ry),
236 (ak, -ry),
237 (rx, -bk),
238 (rx, 0.0),
239 ];
240 let (sin, cos) = e.rot.to_radians().sin_cos();
241 let at = |(x, y): (f32, f32)| map(e.cx + x * cos - y * sin, e.cy + x * sin + y * cos);
242 (
243 at(pts[0]),
244 (1..13)
245 .step_by(3)
246 .map(|i| (at(pts[i + 2]), at(pts[i]), at(pts[i + 1])))
247 .collect(),
248 )
249}