Skip to main content

agent/avatar/
pixels.rs

1//! The same face on a grid: one silhouette sampled per cell, the eyes punched
2//! through rather than painted, so the surface shows where they are and the
3//! sprite survives being dimmed in a list.
4
5use crate::avatar::{Eye, Face, Shape};
6use gpui::{
7    AnyElement, Bounds, Hsla, IntoElement, Pixels, Styled, Window, canvas, fill, point, px, size,
8};
9use theme::Theme;
10
11/// Cells across the sprite.
12pub const GRID: usize = 8;
13
14/// The lid closes the eye to a sliver rather than to nothing, so a blink keeps
15/// its shape on the way down. Shut all the way, the eye leaves no hole.
16const SQUEEZE: f32 = 0.92;
17
18/// One row per byte, bit `x` set where the body covers cell `x`.
19fn cells(shape: &Shape, eyes: &[Eye; 2], lid: f32) -> [u8; GRID] {
20    // Unlike the smooth painter, which insets its body inside the box: a
21    // mascot fills its grid, so the furthest the outline reaches is the edge.
22    let span = shape
23        .outline()
24        .iter()
25        .fold(0.0f32, |far, (x, y)| far.max(x.hypot(*y)));
26
27    let mut rows = [0u8; GRID];
28    for (y, row) in rows.iter_mut().enumerate() {
29        let uy = unit(y, span);
30        for x in 0..GRID {
31            let ux = unit(x, span);
32            // Sound because the profile floors above zero, so the body is
33            // star-shaped and a point is inside iff it is nearer than the reach
34            // in its own direction.
35            if ux.hypot(uy) <= shape.reach(ux, uy) {
36                *row |= 1 << x;
37            }
38        }
39    }
40    for eye in eyes {
41        punch(&mut rows, eye, lid, span);
42    }
43    rows
44}
45
46/// The centre of cell `i` in the shape's own units.
47fn unit(i: usize, span: f32) -> f32 {
48    ((i as f32 + 0.5) / GRID as f32 * 2.0 - 1.0) * span
49}
50
51fn punch(rows: &mut [u8; GRID], eye: &Eye, lid: f32, span: f32) {
52    if lid >= 1.0 {
53        return;
54    }
55    let ry = (eye.ry * (1.0 - lid * SQUEEZE)).max(f32::EPSILON);
56    let (sin, cos) = eye.rot.to_radians().sin_cos();
57    let mut hit = false;
58    for (y, row) in rows.iter_mut().enumerate() {
59        for x in 0..GRID {
60            let (dx, dy) = (unit(x, span) - eye.cx, unit(y, span) - eye.cy);
61            let (a, b) = (dx * cos + dy * sin, dy * cos - dx * sin);
62            if (a / eye.rx).abs().powf(eye.n) + (b / ry).abs().powf(eye.n) <= 1.0 {
63                *row &= !(1 << x);
64                hit = true;
65            }
66        }
67    }
68    if hit || lid >= 0.5 {
69        return;
70    }
71    // At eight cells an open eye can fall between every centre and punch
72    // nothing, leaving a blank body. Walk in until a cell is claimed — the
73    // centre is always inside, so this terminates.
74    for step in 0..=GRID {
75        let k = 1.0 - step as f32 / GRID as f32;
76        let (x, y) = (cell(eye.cx * k, span), cell(eye.cy * k, span));
77        if rows[y] >> x & 1 == 1 {
78            rows[y] &= !(1 << x);
79            return;
80        }
81    }
82}
83
84/// Which cell a coordinate falls in.
85fn cell(u: f32, span: f32) -> usize {
86    (((u / span + 1.0) / 2.0 * GRID as f32) as usize).min(GRID - 1)
87}
88
89/// The element, filling its layout bounds. Draws a [`Face`], never a
90/// [`Pose`](crate::avatar::Pose).
91pub fn mascot(face: &Face, t: f32) -> AnyElement {
92    // Drift and breath are both under a cell at this size, so only the blink
93    // survives the trip; the outline is the resting one, since a wobble that
94    // cannot move a whole cell only flickers the ones on the edge.
95    let rows = cells(
96        &face.shape,
97        &face.eyes.place(&face.shape, 0.0),
98        face.motion.beat(t).lid,
99    );
100    let tint = face.color;
101    canvas(
102        move |_: Bounds<Pixels>, _, _| (),
103        move |bounds, (), window, cx| {
104            let color = tint.unwrap_or(Theme::of(cx).accent);
105            paint(window, bounds, &rows, color);
106        },
107    )
108    .size_full()
109    .into_any_element()
110}
111
112fn paint(window: &mut Window, bounds: Bounds<Pixels>, rows: &[u8; GRID], color: Hsla) {
113    let scale = window.scale_factor();
114    let snap = |v: f32| (v * scale).round() / scale;
115    let box_side = bounds.size.width.min(bounds.size.height).to_f64() as f32;
116    // A whole number of device pixels a side, or the grid shimmers.
117    let side = ((box_side * scale / GRID as f32).floor() / scale).max(1.0 / scale);
118    let sprite = side * GRID as f32;
119    let left = snap(bounds.center().x.to_f64() as f32 - sprite / 2.0);
120    let top = snap(bounds.center().y.to_f64() as f32 - sprite / 2.0);
121
122    for (y, row) in rows.iter().enumerate() {
123        let mut x = 0;
124        while x < GRID {
125            if row >> x & 1 == 0 {
126                x += 1;
127                continue;
128            }
129            // Filled cells merge into one quad a run, so a solid row is one.
130            let mut run = 1;
131            while x + run < GRID && row >> (x + run) & 1 == 1 {
132                run += 1;
133            }
134            window.paint_quad(fill(
135                Bounds::new(
136                    point(px(left + side * x as f32), px(top + side * y as f32)),
137                    size(px(side * run as f32), px(side)),
138                ),
139                color,
140            ));
141            x += run;
142        }
143    }
144}