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