T49: Ely moves all the time, as the films move her, which the owner asked for after 0.1.4, where she stood still but for a press: rigged from the icon's paths in layers of one color, each a mask gpui moves with a matrix of its own at every frame, she breathes, sways, blinks and glances, and each mood moves her, easing from one act to the next; shot's snap writes a child window's frames
CI / platforms (ubuntu-24.04) (push) Successful in 1m38s
CI / platforms (windows-latest) (push) Successful in 2m2s
CI / macos (push) Successful in 13m50s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 14:36:57 -04:00
1 parent c3f1422cbc
commit 9d9144f25e
11 files changed
+1017 -382

No files matched your search

-182
View File
@@ -1,182 +0,0 @@
//! Ely's faces: the icon's head, cut at the edge her fingers hold, one SVG a face.
use std::sync::Arc;
use gpui::{Image, ImageFormat};
/// What Ely's face shows.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Face {
Calm,
Blink,
/// Eyes on the terminal below her, as while you type or a program works.
Down,
Left,
Right,
/// A program finished: eyes shut in a smile, and sparkles.
Happy,
/// A program waits on you: wide eyes, an open mouth, and a mark.
Surprised,
/// A program failed: raised brows and a drop.
Worried,
Sleepy,
/// Pressed: a wink and a heart.
Petted,
}
impl Face {
pub const ALL: [Face; 10] = [
Face::Calm,
Face::Blink,
Face::Down,
Face::Left,
Face::Right,
Face::Happy,
Face::Surprised,
Face::Worried,
Face::Sleepy,
Face::Petted,
];
}
/// The head's box in the icon's units; the edge she holds runs along its bottom.
const VIEW: [u32; 4] = [110, 6, 292, 280];
/// Pixels a unit draws at: three, so a Retina screen draws her sharp.
const SCALE: u32 = 3;
/// Every face as an image gpui draws once and keeps.
pub fn images() -> Vec<(Face, Arc<Image>)> {
Face::ALL
.into_iter()
.map(|face| {
let svg = svg(face).into_bytes();
(face, Arc::new(Image::from_bytes(ImageFormat::Svg, svg)))
})
.collect()
}
/// The SVG of `face`.
pub fn svg(face: Face) -> String {
let (eyes, mouth, mark) = match face {
Face::Calm => (open((0, 0), 1.0), SMILE, ""),
Face::Blink => (SHUT.into(), SMILE, ""),
Face::Down => (open((0, 5), 1.0), FLAT, ""),
Face::Left => (open((-5, 0), 1.0), SMILE, ""),
Face::Right => (open((5, 0), 1.0), SMILE, ""),
Face::Happy => (ARCS.into(), GRIN, SPARKLES),
Face::Surprised => (open((0, 0), 1.12), GASP, BANG),
Face::Worried => (open((0, 3), 1.0), FROWN, DROP),
Face::Sleepy => (LINES.into(), FLAT, SNORE),
Face::Petted => (wink(), GRIN, HEART),
};
let blush = if face == Face::Petted { FLUSH } else { BLUSH };
let brows = if face == Face::Worried { BROWS } else { "" };
let [x, y, w, h] = VIEW;
let (width, height) = (w * SCALE, h * SCALE);
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="{x} {y} {w} {h}">{HEAD}{eyes}{blush}{mouth}{HAIR}{brows}{HANDS}{mark}</svg>"#
)
}
/// An eye at `cx`, its iris moved by `look`, the whole eye grown by `wide`.
fn eye(cx: i32, (dx, dy): (i32, i32), wide: f32) -> String {
format!(
r##"<g transform="translate({cx} 188) scale({wide}) translate({} -188)"><ellipse cx="{cx}" cy="188" rx="18" ry="23" fill="#A24FB9"/><g transform="translate({dx} {dy})"><ellipse cx="{cx}" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/><ellipse cx="{cx}" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/><circle cx="{}" cy="178" r="7" fill="#FFFFFF"/><circle cx="{}" cy="199" r="3.2" fill="#FFFFFF"/></g></g>"##,
-cx,
cx - 7,
cx + 7
)
}
/// Both eyes open, with their lids and lashes.
fn open(look: (i32, i32), wide: f32) -> String {
format!(
"{}{}{LEFT_LID}{RIGHT_LID}{LASHES}",
eye(222, look, wide),
eye(290, look, wide)
)
}
/// The left eye shut in a smile, the right one open.
fn wink() -> String {
format!("{}{RIGHT_LID}{LEFT_ARC}", eye(290, (0, 0), 1.0))
}
const LEFT_LID: &str = r##"<path fill="#45233F" d="M194 182C195 168 208 159 224 159C234 159 241 164 244 172L240 173C236 168 231 165 224 165C214 165 207 172 204 183Z"/>"##;
const RIGHT_LID: &str = r##"<path fill="#45233F" d="M318 182C317 168 304 159 288 159C278 159 271 164 268 172L272 173C276 168 281 165 288 165C298 165 305 172 308 183Z"/>"##;
const LASHES: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M215 212Q222 214 229 212M283 212Q290 214 297 212"/>"##;
const LEFT_ARC: &str = r##"<path fill="none" stroke="#45233F" stroke-width="7" stroke-linecap="round" d="M205 197Q222 176 239 197"/>"##;
const SHUT: &str = r##"<g fill="none" stroke="#45233F" stroke-width="6" stroke-linecap="round"><path d="M205 192Q222 203 239 192"/><path d="M273 192Q290 203 307 192"/></g>"##;
const ARCS: &str = r##"<g fill="none" stroke="#45233F" stroke-width="7" stroke-linecap="round"><path d="M205 197Q222 176 239 197"/><path d="M273 197Q290 176 307 197"/></g>"##;
const LINES: &str = r##"<g fill="none" stroke="#45233F" stroke-width="6" stroke-linecap="round"><path d="M205 194H239"/><path d="M273 194H307"/></g>"##;
const BROWS: &str = r##"<g fill="none" stroke="#45233F" stroke-width="5" stroke-linecap="round"><path d="M206 160L234 152"/><path d="M306 160L278 152"/></g>"##;
const SMILE: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" d="M244 222Q250 229 256 223Q262 229 268 222"/>"##;
const GRIN: &str = r##"<path fill="#C2506F" d="M243 219Q256 241 269 219Z"/><path fill="#FF8DAA" d="M249 229Q256 236 263 229Q256 225 249 229Z"/>"##;
const GASP: &str = r##"<ellipse cx="256" cy="226" rx="5.5" ry="7" fill="#C2506F"/>"##;
const FLAT: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" d="M249 225H263"/>"##;
const FROWN: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" d="M246 228Q256 220 266 228"/>"##;
const BLUSH: &str = r##"<g opacity=".55" fill="#FF8DAA"><ellipse cx="198" cy="216" rx="15" ry="8"/><ellipse cx="314" cy="216" rx="15" ry="8"/></g>"##;
const FLUSH: &str = r##"<g opacity=".85" fill="#FF7A9C"><ellipse cx="198" cy="216" rx="17" ry="9"/><ellipse cx="314" cy="216" rx="17" ry="9"/></g>"##;
const BANG: &str = r##"<g fill="#FFB020" stroke="#45233F" stroke-width="3"><path d="M371 38L385 38L380 86L376 86Z"/><circle cx="378" cy="100" r="7"/></g>"##;
const SNORE: &str = r##"<g fill="none" stroke="#9FA3D9" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M352 70H372L352 92H372"/><path d="M380 38H394L380 54H394"/></g>"##;
const HEART: &str = r##"<path fill="#FF5C93" d="M378 70C366 56 350 66 356 80C360 90 378 100 378 100C378 100 396 90 400 80C406 66 390 56 378 70Z"/>"##;
const SPARKLES: &str = r##"<g fill="#FFE48A"><path d="M140 70Q143 84 156 86Q143 88 140 102Q137 88 124 86Q137 84 140 70Z"/><path d="M380 52Q382 62 392 64Q382 66 380 76Q378 66 368 64Q378 62 380 52Z"/></g>"##;
const DROP: &str = r##"<path fill="#9CD5FF" stroke="#5AA0D8" stroke-width="2" d="M346 128Q356 144 350 152Q340 156 338 146Q338 138 346 128Z"/>"##;
/// Her hair behind, her collar, and her face: the icon's paths.
const HEAD: &str = concat!(
r##"<defs><clipPath id="face"><path d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/></clipPath></defs>"##,
r##"<path fill="#EC86B5" d="M256 46C192 46 142 90 136 158C131 214 126 262 118 306L394 306C386 262 381 214 376 158C370 90 320 46 256 46Z"/>"##,
r##"<path fill="#D9659A" d="M194 200C198 238 214 262 230 276L282 276C298 262 314 238 318 200Z"/>"##,
r##"<path fill="#F7CDBE" d="M236 222L276 222L276 266C268 270 244 270 236 266Z"/>"##,
r##"<path fill="#FFFFFF" d="M198 284C204 268 222 258 238 256L256 272L274 256C290 258 308 268 314 284Z"/>"##,
r##"<path fill="#F05A97" d="M256 264C244 250 226 248 224 258C222 270 236 278 256 264ZM256 264C268 250 286 248 288 258C290 270 276 278 256 264Z"/>"##,
r##"<circle cx="256" cy="264" r="8" fill="#D94683"/>"##,
r##"<path fill="#FFE7DC" d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>"##,
r##"<g clip-path="url(#face)"><path fill="#F9D8CB" transform="translate(0 12)" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/></g>"##,
);
/// Her side locks, bangs, ahoge and the star in her hair: the icon's paths.
const HAIR: &str = concat!(
r##"<path fill="#F7A6C9" d="M184 118C164 150 156 204 160 258C162 282 156 300 148 316C164 312 178 300 186 286C190 256 192 214 198 176C200 152 196 132 184 118ZM328 118C348 150 356 204 352 258C350 282 356 300 364 316C348 312 334 300 326 286C322 256 320 214 314 176C312 152 316 132 328 118Z"/>"##,
r##"<path fill="#F7A6C9" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>"##,
r##"<path fill="#FFD1E4" d="M196 108C220 84 292 84 316 108C292 92 220 92 196 108Z"/>"##,
r##"<path fill="#F7A6C9" d="M250 66C238 40 252 18 280 20C264 28 258 44 262 64Z"/>"##,
r##"<path fill="#FFE48A" d="M318 86Q320 98 332 100Q320 102 318 114Q316 102 304 100Q316 98 318 86Z"/>"##,
);
/// Her fingers over the edge she holds.
const HANDS: &str = concat!(
r##"<path fill="#FFE7DC" d="M150 286L150 272C150 264 161 264 161 272C161 262 172 262 172 271C172 262 183 262 183 272C183 264 194 264 194 273L194 286ZM318 286L318 273C318 264 329 264 329 272C329 262 340 262 340 271C340 262 351 262 351 272C351 264 362 264 362 272L362 286Z"/>"##,
r##"<path fill="none" stroke="#F2BBA8" stroke-width="2" stroke-linecap="round" d="M161 274V286M172 273V286M183 274V286M329 274V286M340 273V286M351 274V286"/>"##,
);
#[cfg(test)]
mod tests {
use resvg::usvg;
use super::*;
#[test]
fn every_face_draws_at_three_times_her_box() {
for face in Face::ALL {
let svg = svg(face);
let tree = usvg::Tree::from_str(&svg, &usvg::Options::default())
.unwrap_or_else(|error| panic!("{face:?}: {error}"));
assert_eq!(tree.size().width(), 292.0 * 3.0, "{face:?}");
assert_eq!(tree.size().height(), 280.0 * 3.0, "{face:?}");
}
assert_eq!(images().len(), Face::ALL.len());
}
#[test]
fn each_face_shows_what_it_says() {
assert!(svg(Face::Surprised).contains(BANG) && svg(Face::Surprised).contains(GASP));
assert!(svg(Face::Worried).contains(BROWS) && svg(Face::Worried).contains(DROP));
assert!(svg(Face::Happy).contains(ARCS) && svg(Face::Happy).contains(SPARKLES));
assert!(svg(Face::Sleepy).contains(LINES) && svg(Face::Sleepy).contains(SNORE));
assert!(svg(Face::Petted).contains(HEART) && svg(Face::Petted).contains(FLUSH));
assert!(svg(Face::Blink).contains(SHUT) && !svg(Face::Blink).contains(LEFT_LID));
assert_ne!(svg(Face::Left), svg(Face::Right));
}
}
+291 -110
View File
@@ -1,8 +1,15 @@
//! What Ely feels from the sessions' marks, and the face she makes at each moment of it.
//! What Ely feels from the sessions' marks, and her pose at each moment of it, moving as the films move her.
use std::time::Duration;
mod curves;
#[cfg(test)]
mod tests;
use super::faces::Face;
use std::f32::consts::PI;
use self::curves::{
SHUT, between, blink, early, floating, fold, hops, opening, overshoot, popping, shaking,
smooth, tap, twinkling, wiggle,
};
use crate::sessions::orb::Look;
/// What the sessions ask of her, least pressing first.
@@ -30,120 +37,294 @@ pub fn mood(looks: impl IntoIterator<Item = Look>) -> Mood {
.unwrap_or(Mood::Calm)
}
/// How long she cheers a program that finished, and how long calm lasts before she dozes.
const CHEER: Duration = Duration::from_secs(4);
const DOZE: Duration = Duration::from_secs(300);
/// How long a press keeps her wink.
pub const PETTED: Duration = Duration::from_millis(1500);
/// Seconds she cheers a program that finished, and calm lasts before she dozes.
const CHEER: f64 = 4.0;
const DOZE: f64 = 300.0;
/// Seconds a press keeps her wink.
const PETTED: f32 = 1.5;
/// Seconds she takes to rise from behind the edge, from as deep as she is tall.
const RISE: f32 = 0.6;
const DEPTH: f32 = 280.0;
/// Seconds she takes to ease from one act into the next.
pub const EASE: f32 = 0.3;
/// A calm round of 20 s: she blinks twice and glances left, then right.
const CALM: (f32, [(f32, Face); 9]) = (
20.0,
[
(0.0, Face::Calm),
(4.0, Face::Blink),
(4.15, Face::Calm),
(9.0, Face::Left),
(10.2, Face::Calm),
(13.0, Face::Blink),
(13.15, Face::Calm),
(16.0, Face::Right),
(17.2, Face::Calm),
],
);
/// What she is doing; a change eases her from her last pose.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Act {
Calm,
Doze,
Cheer,
Work,
Wait,
Fret,
Wink,
}
/// A working round of 8 s: her eyes stay on the terminal but for a blink.
const WORKING: (f32, [(f32, Face); 3]) = (
8.0,
[(0.0, Face::Down), (5.0, Face::Blink), (5.15, Face::Down)],
);
/// Her eyes: open, smiling arcs, sleeping lines, or a wink of the left.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Eyes {
Open,
Arcs,
Lines,
Wink,
}
/// Her face `held` into `mood`, and how long it holds; none while it holds for good.
pub fn face(mood: Mood, held: Duration) -> (Face, Option<Duration>) {
match mood {
Mood::Waiting => (Face::Surprised, None),
Mood::Error => (Face::Worried, None),
Mood::Done if held < CHEER => (Face::Happy, Some(CHEER - held)),
Mood::Working => round(WORKING.0, &WORKING.1, held),
Mood::Calm | Mood::Done if held >= DOZE => (Face::Sleepy, None),
Mood::Calm | Mood::Done => {
let (face, next) = round(CALM.0, &CALM.1, held);
(face, next.map(|next| next.min(DOZE - held)))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mouth {
Smile,
Grin,
Gasp,
Flat,
Frown,
}
/// A mark beside her head.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mark {
Bang,
Z1,
Z2,
Heart,
SparkleLeft,
SparkleRight,
Drop,
}
/// How a mark moves from its place: by `at` units, grown by `scale`, turned `turn` degrees, at `alpha`.
#[derive(Clone, Copy, Debug)]
pub struct Motion {
pub at: (f32, f32),
pub scale: f32,
pub turn: f32,
pub alpha: f32,
}
/// Her pose at one moment, in the icon's units and degrees: lift and bob go up, marks and hands move as the icon's y does, down.
#[derive(Clone, Debug)]
pub struct Pose {
pub act: Act,
/// All of her up, and aside.
pub lift: f32,
pub shake: f32,
/// Her head up with her breath, and turned.
pub bob: f32,
pub tilt: f32,
pub sway: f32,
pub ahoge: f32,
pub look: (f32, f32),
/// Each eye's height, left then right, and both eyes' size.
pub open: [f32; 2],
pub wide: f32,
pub eyes: Eyes,
pub mouth: Mouth,
/// The mouth's size, which pops as it changes.
pub pop: f32,
pub brows: bool,
pub flush: f32,
/// Each hand pressed down, left then right.
pub hands: [f32; 2],
pub marks: Vec<(Mark, Motion)>,
}
/// Her pose `held` seconds into `mood`, `shown` seconds after she came, and `petted` seconds after a press.
pub fn pose(mood: Mood, held: f64, shown: f64, petted: Option<f64>) -> Pose {
let mut pose = match mood {
Mood::Waiting => waiting(held),
Mood::Error => fretting(held),
Mood::Working => working(held),
Mood::Done if held < CHEER => cheering(held),
Mood::Calm | Mood::Done if held >= DOZE => dozing(held - DOZE),
Mood::Calm => calm(held),
Mood::Done => calm(held - CHEER),
};
if shown < RISE as f64 {
pose.lift -= DEPTH * (1.0 - shown as f32 / RISE).powi(3);
}
match petted.map(early) {
Some(since) if since < PETTED => winking(&mut pose, since),
Some(since) => pose.open[0] = pose.open[0].min(opening(since - PETTED)),
None => {}
}
pose
}
/// `to`, `since` seconds after a change of act, eased from `from`, her last pose before it: her place and her moves blend, the marks `to` lacks fade, and the shapes are `to`'s.
pub fn blend(from: &Pose, to: Pose, since: f32) -> Pose {
let k = smooth((since / EASE).clamp(0.0, 1.0));
let mix = |a: f32, b: f32| a + (b - a) * k;
let fading: Vec<(Mark, Motion)> = (from.marks.iter())
.filter(|(mark, _)| to.marks.iter().all(|(kept, _)| kept != mark))
.map(|&(mark, motion)| {
let alpha = motion.alpha * (1.0 - k);
(mark, Motion { alpha, ..motion })
})
.collect();
let mut pose = Pose {
lift: mix(from.lift, to.lift),
shake: mix(from.shake, to.shake),
bob: mix(from.bob, to.bob),
tilt: mix(from.tilt, to.tilt),
sway: mix(from.sway, to.sway),
ahoge: mix(from.ahoge, to.ahoge),
look: (mix(from.look.0, to.look.0), mix(from.look.1, to.look.1)),
wide: mix(from.wide, to.wide),
flush: mix(from.flush, to.flush),
hands: [
mix(from.hands[0], to.hands[0]),
mix(from.hands[1], to.hands[1]),
],
..to
};
pose.marks.extend(fading);
pose
}
/// Life under every act, `t` seconds into it: a bob of `amp` units every `period` seconds, swaying locks, a breathing ahoge, and blinks, her eyes opening and her mouth popping as the act begins.
fn idle(act: Act, t: f64, period: f32, amp: f32) -> Pose {
let phase = 2.0 * PI * fold(t, period) / period;
Pose {
act,
lift: 0.0,
shake: 0.0,
bob: phase.sin() * amp,
tilt: 0.0,
sway: (PI * fold(t, 2.0 * period) / period + 1.1).sin() * 2.2,
ahoge: (phase + 0.6).sin() * 4.0,
look: (0.0, 0.0),
open: [blink(t).min(opening(early(t))); 2],
wide: 1.0,
eyes: Eyes::Open,
mouth: Mouth::Smile,
pop: popping(early(t)),
brows: false,
flush: 0.0,
hands: [0.0; 2],
marks: Vec::new(),
}
}
/// The face a round of `period` seconds shows `held` into it, and how long it holds.
fn round(period: f32, steps: &[(f32, Face)], held: Duration) -> (Face, Option<Duration>) {
let at = held.as_secs_f32() % period;
let now = steps
.iter()
.rposition(|&(start, _)| start <= at)
.expect("a round starts at 0");
let end = steps.get(now + 1).map_or(period, |&(start, _)| start);
(steps[now].1, Some(Duration::from_secs_f32(end - at)))
/// Calm: she glances left, then right, her head after her eyes, and her ahoge wiggles.
fn calm(t: f64) -> Pose {
let mut pose = idle(Act::Calm, t, 2.6, 4.0);
let at = fold(t, 20.0);
let look = 6.0 * (between(at, 16.0, 17.2) - between(at, 9.0, 10.2));
pose.look = (look, 0.0);
pose.tilt = look * 0.5;
pose.ahoge += wiggle(at - 12.4);
pose
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_most_pressing_mark_sets_her_mood() {
assert_eq!(mood([]), Mood::Calm);
assert_eq!(mood([Look::Idle, Look::Done]), Mood::Done);
assert_eq!(
mood([Look::Done, Look::Working(Some(40)), Look::Idle]),
Mood::Working
);
assert_eq!(mood([Look::Working(None), Look::Error]), Mood::Error);
assert_eq!(
mood([Look::Error, Look::Waiting, Look::Done]),
Mood::Waiting
);
}
#[test]
fn calm_blinks_and_glances_then_dozes() {
let at = |secs: f32| face(Mood::Calm, Duration::from_secs_f32(secs)).0;
assert_eq!(at(1.0), Face::Calm);
assert_eq!(at(4.1), Face::Blink);
assert_eq!(at(9.5), Face::Left);
assert_eq!(at(16.5), Face::Right);
assert_eq!(at(24.1), Face::Blink, "the round comes again");
assert_eq!(at(300.0), Face::Sleepy);
assert_eq!(
face(Mood::Calm, Duration::from_secs(400)).1,
None,
"she sleeps on"
);
let (_, wait) = face(Mood::Calm, Duration::from_secs_f32(1.0));
assert!((wait.expect("a next face").as_secs_f32() - 3.0).abs() < 1e-3);
}
#[test]
fn a_finished_program_cheers_her_then_she_calms() {
let (face_now, wait) = face(Mood::Done, Duration::from_secs(1));
assert_eq!(face_now, Face::Happy);
assert_eq!(wait, Some(Duration::from_secs(3)));
assert_eq!(face(Mood::Done, Duration::from_secs(5)).0, Face::Calm);
}
#[test]
fn waiting_and_errors_hold_their_faces() {
assert_eq!(
face(Mood::Waiting, Duration::from_secs(999)),
(Face::Surprised, None)
);
assert_eq!(face(Mood::Error, Duration::ZERO), (Face::Worried, None));
assert_eq!(
face(Mood::Working, Duration::from_secs_f32(5.1)).0,
Face::Blink
);
assert_eq!(
face(Mood::Working, Duration::from_secs(1000)).0,
Face::Down,
"work never dozes"
);
/// Dozing: her eyes fall shut, her head and ahoge droop, and zs float up.
fn dozing(t: f64) -> Pose {
let mut pose = idle(Act::Doze, t, 4.2, 3.0);
let settle = smooth((early(t) / 0.8).min(1.0));
if t < 0.6 {
pose.open = [1.0 - (1.0 - SHUT) * smooth(early(t) / 0.6); 2];
} else {
pose.eyes = Eyes::Lines;
pose.open = [1.0; 2];
}
pose.mouth = Mouth::Flat;
pose.tilt = settle * 5.0 + (2.0 * PI * fold(t, 8.4) / 8.4).sin() * 1.5;
pose.ahoge += settle * 14.0;
pose.marks = vec![(Mark::Z1, floating(t, 0.0)), (Mark::Z2, floating(t, 0.5))];
pose
}
/// Cheering: smiling eyes and mouth, a hop, and twinkling sparkles.
fn cheering(t: f64) -> Pose {
let mut pose = idle(Act::Cheer, t, 2.2, 4.0);
let t = early(t);
pose.eyes = Eyes::Arcs;
pose.mouth = Mouth::Grin;
pose.flush = 0.6;
pose.lift = hops(t, 1.0);
pose.ahoge += wiggle(t - 0.1);
pose.marks = vec![
(Mark::SparkleLeft, twinkling(t, 0.0)),
(Mark::SparkleRight, twinkling(t, 0.45)),
];
pose
}
/// Working: her eyes run along the terminal's lines, and her fingers tap by turns.
fn working(t: f64) -> Pose {
let mut pose = idle(Act::Work, t, 2.0, 2.5);
let at = fold(t, 2.25);
let scan = if at < 2.0 {
-5.0 + 10.0 * smooth(at / 2.0)
} else {
5.0 - 10.0 * smooth((at - 2.0) / 0.25)
};
pose.look = (scan, 5.0);
pose.mouth = Mouth::Flat;
pose.tilt = -2.0;
let beat = fold(t, 3.0);
if beat < 1.8 {
let n = (beat / 0.3) as usize;
pose.hands[n % 2] = tap(beat - n as f32 * 0.3);
}
pose
}
/// Waiting on you: wide eyes, a gasp, a mark that rocks, and two hops every 2.6 s.
fn waiting(t: f64) -> Pose {
let mut pose = idle(Act::Wait, t, 1.6, 3.0);
let beat = 2.0 * PI * fold(t, 0.8) / 0.8;
pose.wide = 1.0 + 0.12 * overshoot((early(t) / 0.12).min(1.0)) + 0.02 * beat.sin();
pose.mouth = Mouth::Gasp;
pose.look = (0.0, -2.0);
let at = fold(t, 2.6);
pose.lift = hops(at, 2.0);
pose.ahoge += wiggle(at);
let bang = Motion {
at: (0.0, 0.0),
scale: popping(early(t)),
turn: beat.sin() * 8.0,
alpha: 1.0,
};
pose.marks = vec![(Mark::Bang, bang)];
pose
}
/// An error: brows up, a frown, a shake, a smaller one every 4.5 s, and a drop that runs down.
fn fretting(t: f64) -> Pose {
let mut pose = idle(Act::Fret, t, 2.4, 1.5);
pose.brows = true;
pose.mouth = Mouth::Frown;
pose.look = (0.0, 3.0);
pose.tilt = -3.0;
pose.shake = shaking(early(t), 10.0) + shaking(fold(t, 4.5) - 3.9, 4.0);
let f = fold(t, 2.0) / 2.0;
let drop = Motion {
at: (0.0, 18.0 * smooth(f)),
scale: 1.0,
turn: 0.0,
alpha: (f * PI).sin(),
};
pose.marks = vec![(Mark::Drop, drop)];
pose
}
/// A press, `since` seconds ago: a wink, a grin, a blush, a small hop, a wiggle and a heart that rises; an eye she slept with opens.
fn winking(pose: &mut Pose, since: f32) {
let woke = pose.eyes != Eyes::Open;
pose.act = Act::Wink;
pose.eyes = Eyes::Wink;
pose.open = [
opening(since),
if woke { opening(since) } else { pose.open[1] },
];
pose.mouth = Mouth::Grin;
pose.pop = popping(since);
pose.flush = 1.0;
pose.lift += hops(since, 1.0) * 0.6;
pose.ahoge += wiggle(since);
let heart = Motion {
at: (0.0, -26.0 * since / PETTED),
scale: popping(since),
turn: 0.0,
alpha: ((PETTED - since) / 0.4).min(1.0),
};
pose.marks.push((Mark::Heart, heart));
}
+140
View File
@@ -0,0 +1,140 @@
//! The timings and eases Ely's moves take, after the films' tweens.
use std::f32::consts::PI;
use super::Motion;
/// How far her eyes close in a blink.
pub(super) const SHUT: f32 = 0.08;
/// A hop's height: 12 points.
const HOP: f32 = 31.0;
/// Seconds between blinks, a round she repeats; the short gap blinks twice.
const BLINKS: [f32; 6] = [3.4, 4.6, 0.32, 5.2, 2.8, 4.1];
/// How open an eye is `t` seconds into the blink round: shut in 70 ms, open again in 120 ms.
pub(super) fn blink(t: f64) -> f32 {
let round: f32 = BLINKS.iter().sum();
let at = fold(t, round);
// A round ends in a blink; the first has none before it.
let mut last = if t < round as f64 {
f32::NEG_INFINITY
} else {
0.0
};
let mut time = 0.0;
for gap in BLINKS {
time += gap;
if time <= at {
last = time;
}
}
match at - last {
s if s < 0.07 => 1.0 - (1.0 - SHUT) * (s / 0.07).powi(2),
s if s < 0.1 => SHUT,
s if s < 0.22 => SHUT + (1.0 - SHUT) * (1.0 - (1.0 - (s - 0.1) / 0.12).powi(2)),
_ => 1.0,
}
}
/// A shape that opens from a line in 150 ms, `t` seconds after it came.
pub(super) fn opening(t: f32) -> f32 {
SHUT + (1.0 - SHUT) * smooth((t / 0.15).clamp(0.0, 1.0))
}
/// A mouth or mark that pops in over 180 ms, `t` seconds after it came.
pub(super) fn popping(t: f32) -> f32 {
0.6 + 0.4 * overshoot((t / 0.18).clamp(0.0, 1.0))
}
/// `count` hops in 0.6 s, `since` they began.
pub(super) fn hops(since: f32, count: f32) -> f32 {
if !(0.0..0.6).contains(&since) {
return 0.0;
}
(since / 0.6 * count * PI).sin().abs() * HOP
}
/// A shake of `amp` units that dies away in 0.6 s, `since` it began.
pub(super) fn shaking(since: f32, amp: f32) -> f32 {
if !(0.0..0.6).contains(&since) {
return 0.0;
}
let u = since / 0.6;
(u * 6.0 * PI).sin() * amp * (1.0 - u)
}
/// The ahoge's wiggle, `since` it began: -16°, 11°, -6°, then still, in 0.7 s.
pub(super) fn wiggle(since: f32) -> f32 {
const KEYS: [(f32, f32); 5] = [
(0.0, 0.0),
(0.12, -16.0),
(0.28, 11.0),
(0.46, -6.0),
(0.7, 0.0),
];
let Some(i) = (KEYS.windows(2)).position(|pair| (pair[0].0..pair[1].0).contains(&since)) else {
return 0.0;
};
let ((t0, v0), (t1, v1)) = (KEYS[i], KEYS[i + 1]);
v0 + (v1 - v0) * smooth((since - t0) / (t1 - t0))
}
/// A finger's tap, `since` it began: down 4 units in 60 ms, up in 160 ms.
pub(super) fn tap(since: f32) -> f32 {
if since < 0.06 {
4.0 * (since / 0.06).powi(2)
} else {
4.0 * (1.0 - smooth(((since - 0.06) / 0.16).min(1.0)))
}
}
/// A z that floats up from its place and fades every 2.4 s, `phase` of a round late.
pub(super) fn floating(t: f64, phase: f32) -> Motion {
let f = (fold(t, 2.4) / 2.4 + phase) % 1.0;
Motion {
at: (f * 6.0, -f * 22.0),
scale: 0.8 + 0.4 * f,
turn: 0.0,
alpha: (f * PI).sin() * (early(t) / 0.8).min(1.0),
}
}
/// A sparkle that grows in and twinkles, `phase` of a 0.9 s round late.
pub(super) fn twinkling(t: f32, phase: f32) -> Motion {
let pulse = (2.0 * PI * (t / 0.9 + phase)).sin();
Motion {
at: (0.0, 0.0),
scale: overshoot((t / 0.25).min(1.0)) * (0.85 + 0.25 * pulse),
turn: pulse * 12.0,
alpha: 1.0,
}
}
/// `t` folded into a round of `period` seconds, which keeps its steps fine after weeks.
pub(super) fn fold(t: f64, period: f32) -> f32 {
(t % period as f64) as f32
}
/// `t` while it is short; past a minute, what follows a start has long ended.
pub(super) fn early(t: f64) -> f32 {
t.min(60.0) as f32
}
/// 1 inside `from..to`, 0 outside, eased in and out over 0.35 s.
pub(super) fn between(at: f32, from: f32, to: f32) -> f32 {
smooth(
((at - from) / 0.35)
.clamp(0.0, 1.0)
.min(((to - at) / 0.35).clamp(0.0, 1.0)),
)
}
pub(super) fn smooth(x: f32) -> f32 {
x * x * (3.0 - 2.0 * x)
}
/// An ease that runs past its end and settles back, as GSAP's back.out.
pub(super) fn overshoot(x: f32) -> f32 {
const C: f32 = 1.70158;
1.0 + (C + 1.0) * (x - 1.0).powi(3) + C * (x - 1.0).powi(2)
}
+203
View File
@@ -0,0 +1,203 @@
use super::*;
#[test]
fn the_most_pressing_mark_sets_her_mood() {
assert_eq!(mood([]), Mood::Calm);
assert_eq!(mood([Look::Idle, Look::Done]), Mood::Done);
assert_eq!(
mood([Look::Done, Look::Working(Some(40)), Look::Idle]),
Mood::Working
);
assert_eq!(mood([Look::Working(None), Look::Error]), Mood::Error);
assert_eq!(
mood([Look::Error, Look::Waiting, Look::Done]),
Mood::Waiting
);
}
/// Her pose `held` seconds into `mood`, long after she came, unpressed.
fn at(mood: Mood, held: f64) -> Pose {
pose(mood, held, 10.0, None)
}
#[test]
fn she_moves_in_every_mood_even_after_weeks() {
let weeks = 3.0 * 7.0 * 86_400.0;
for (mood, held) in [
(Mood::Calm, 30.0),
(Mood::Calm, 400.0),
(Mood::Done, 50.0),
(Mood::Working, 30.0),
(Mood::Waiting, 30.0),
(Mood::Error, 30.0),
(Mood::Working, weeks),
(Mood::Calm, weeks),
] {
let (now, soon) = (at(mood, held), at(mood, held + 0.3));
assert_ne!(now.bob, soon.bob, "{mood:?} at {held}: her breath");
assert_ne!(now.ahoge, soon.ahoge, "{mood:?} at {held}: her ahoge");
assert_ne!(now.sway, soon.sway, "{mood:?} at {held}: her locks");
}
}
#[test]
fn she_blinks_on_her_own_twice_at_the_short_gap() {
let open = |held: f64| at(Mood::Calm, held).open;
assert_eq!(open(2.0), [1.0, 1.0], "open between blinks");
assert!(open(3.48)[0] < 0.1, "shut in the blink at 3.4 s");
assert_eq!(open(3.7), [1.0, 1.0], "open again");
assert!(
open(8.08)[0] < 0.1 && open(8.4)[0] < 0.1,
"twice at the short gap"
);
assert!(
open(3.43)[0] > open(3.46)[0],
"she shuts them, not snaps them"
);
assert!(
open(0.0)[0] < 0.1 && open(0.2) == [1.0, 1.0],
"her eyes open as an act begins"
);
}
#[test]
fn calm_glances_left_then_right_with_her_head() {
let calm = |held: f64| at(Mood::Calm, held);
assert_eq!(calm(5.0).look, (0.0, 0.0));
assert_eq!(calm(9.6).look, (-6.0, 0.0));
assert_eq!(calm(16.6).look, (6.0, 0.0));
assert!(
calm(16.6).tilt > 0.0 && calm(9.6).tilt < 0.0,
"her head follows"
);
assert!(calm(9.1).look.0 > -6.0, "her eyes ease over");
let breath = idle(Act::Calm, 12.6, 2.6, 4.0).ahoge;
assert!(
(calm(12.6).ahoge - breath).abs() > 1.0,
"her ahoge wiggles after her glances"
);
}
#[test]
fn calm_dozes_after_five_minutes_with_zs() {
let dozing = at(Mood::Calm, 301.0);
assert_eq!((dozing.act, dozing.eyes), (Act::Doze, Eyes::Lines));
let marks: Vec<Mark> = dozing.marks.iter().map(|(mark, _)| *mark).collect();
assert_eq!(marks, [Mark::Z1, Mark::Z2]);
assert!(
at(Mood::Calm, 300.3).open[0] < 1.0,
"her eyes fall shut first"
);
let z = |held: f64| at(Mood::Calm, held).marks[0].1;
assert!(z(305.6).at.1 < z(305.0).at.1, "the zs float up");
}
#[test]
fn a_finish_cheers_her_with_a_hop_then_she_calms() {
let cheer = at(Mood::Done, 0.3);
assert_eq!(
(cheer.act, cheer.eyes, cheer.mouth),
(Act::Cheer, Eyes::Arcs, Mouth::Grin)
);
assert!(cheer.lift > 10.0, "a hop");
assert_eq!(cheer.marks.len(), 2, "two sparkles");
let calm = at(Mood::Done, 5.0);
assert_eq!(
(calm.act, calm.eyes, calm.mouth),
(Act::Calm, Eyes::Open, Mouth::Smile)
);
assert!(
at(Mood::Done, 4.0).open[0] < 0.1,
"her eyes open again as she calms"
);
}
#[test]
fn work_runs_her_eyes_along_the_lines_and_taps_her_fingers() {
let work = |held: f64| at(Mood::Working, held);
assert_eq!(work(1.0).act, Act::Work);
assert_eq!(work(1.0).look.1, 5.0, "her eyes on the terminal");
assert!(work(0.4).look.0 < work(1.6).look.0, "along a line");
assert!(
work(0.03).hands[0] > 0.0 && work(0.33).hands[1] > 0.0,
"by turns"
);
assert_eq!(work(2.5).hands, [0.0, 0.0], "then a rest");
}
#[test]
fn a_wait_widens_her_eyes_and_hops_her_twice_every_round() {
let wait = |held: f64| at(Mood::Waiting, held);
assert!(wait(1.0).wide > 1.1);
assert_eq!((wait(1.0).act, wait(1.0).mouth), (Act::Wait, Mouth::Gasp));
assert!(wait(0.15).lift > 20.0 && wait(0.45).lift > 20.0, "two hops");
assert!(wait(2.75).lift > 20.0, "again next round");
assert_eq!(wait(1.5).lift, 0.0);
assert_eq!(wait(1.0).marks[0].0, Mark::Bang);
}
#[test]
fn an_error_frets_her_with_a_shake_and_a_drop() {
let fret = |held: f64| at(Mood::Error, held);
assert!(fret(1.0).brows);
assert_eq!((fret(1.0).act, fret(1.0).mouth), (Act::Fret, Mouth::Frown));
assert!(fret(0.15).shake.abs() > 1.0, "a shake");
assert_eq!(fret(1.0).shake, 0.0);
assert!(fret(4.05).shake.abs() > 1.0, "a smaller one later");
assert!(
fret(1.5).marks[0].1.at.1 > fret(0.5).marks[0].1.at.1,
"the drop runs down"
);
}
#[test]
fn a_press_winks_with_a_heart_that_rises() {
let pressed = |since: f64| pose(Mood::Calm, 30.0, 30.0, Some(since));
assert_eq!(
(pressed(0.5).act, pressed(0.5).eyes),
(Act::Wink, Eyes::Wink)
);
assert_eq!(pressed(0.5).marks[0].0, Mark::Heart);
assert!(pressed(1.0).marks[0].1.at.1 < pressed(0.5).marks[0].1.at.1);
assert_eq!(
(pressed(1.6).act, pressed(1.6).eyes),
(Act::Calm, Eyes::Open),
"the wink lasts 1.5 s"
);
assert!(pressed(1.55).open[0] < 0.5, "her eye opens after it");
let asleep = pose(Mood::Calm, 400.0, 400.0, Some(0.5));
assert!(asleep.open[1] > 0.9, "a press wakes her other eye");
}
#[test]
fn she_rises_from_behind_the_edge_when_she_comes() {
assert!(pose(Mood::Calm, 0.0, 0.0, None).lift <= -DEPTH);
assert!(pose(Mood::Calm, 0.3, 0.3, None).lift < 0.0);
assert_eq!(pose(Mood::Calm, 5.0, 0.6, None).lift, 0.0);
}
#[test]
fn a_change_of_act_eases_her_from_her_last_pose() {
let working = at(Mood::Working, 1.0);
let early = blend(&working, at(Mood::Calm, 0.1), 0.1);
assert!(
early.tilt < 0.0 && early.tilt > working.tilt,
"her head turns back, not at once: {} from {}",
early.tilt,
working.tilt
);
let calm = at(Mood::Calm, EASE as f64);
let eased = blend(&working, calm.clone(), EASE);
assert!(
(eased.tilt - calm.tilt).abs() < 1e-5 && (eased.look.1 - calm.look.1).abs() < 1e-5,
"eased in {EASE} s"
);
let waiting = at(Mood::Waiting, 1.0);
let leaving = blend(&waiting, at(Mood::Calm, 0.1), 0.1);
let bang = (leaving.marks.iter())
.find(|(mark, _)| *mark == Mark::Bang)
.expect("the mark she leaves fades, not vanishes")
.1;
assert!(bang.alpha > 0.0 && bang.alpha < 1.0);
assert_eq!(leaving.eyes, Eyes::Open, "the shapes are the new act's");
}
+304
View File
@@ -0,0 +1,304 @@
//! Ely rigged as the films rig her: the icon's paths in layers of one color, each a mask gpui draws with a matrix of its own.
use gpui::{
App, Bounds, Hsla, Pixels, Point, ScaledPixels, SharedString, TransformationMatrix, Window,
point, px, radians, rgb, size,
};
use super::{
WIDTH,
look::{Eyes, Mark, Mouth, Pose},
};
/// A point or a shift in the window's device pixels.
type Device = (f32, f32);
/// The head's box in the icon's units; the edge she holds runs along its bottom.
const VIEW: [f32; 4] = [110.0, 6.0, 292.0, 280.0];
/// Points a unit draws at.
const UNIT: f32 = WIDTH / VIEW[2];
/// How a layer moves.
#[derive(Clone, Copy, PartialEq)]
enum Bone {
/// Her neck and collar, which stay on the edge as her head bobs.
Body,
/// Bobs, and turns with her head.
Head,
/// An open eye's lid and lash, which a blink squashes; left is 0.
Lid(usize),
/// An open eye, which looks about as well.
Iris(usize),
Arc(usize),
Lines,
Brows,
Blush,
Mouth(Mouth),
Lock(usize),
Ahoge,
/// Holds the edge; taps.
Hand(usize),
Mark(Mark),
}
/// A layer: its name, color and opacity, its box in the icon's units, the point it turns and grows about, how it moves, and its shapes.
struct Part {
name: &'static str,
color: u32,
alpha: f32,
frame: [f32; 4],
pivot: (f32, f32),
bone: Bone,
shapes: &'static str,
}
/// A part with no pivot of its own.
#[rustfmt::skip]
const fn part(name: &'static str, color: u32, frame: [f32; 4], bone: Bone, shapes: &'static str) -> Part {
Part { name, color, alpha: 1.0, frame, pivot: (256.0, 236.0), bone, shapes }
}
/// Every layer, back to front.
#[rustfmt::skip]
const PARTS: &[Part] = &[
part("hair-back", 0xEC86B5, [117.0, 45.0, 278.0, 262.0], Bone::Head, r#"<path d="M256 46C192 46 142 90 136 158C131 214 126 262 118 306L394 306C386 262 381 214 376 158C370 90 320 46 256 46Z"/>"#),
part("shade-back", 0xD9659A, [193.0, 199.0, 126.0, 78.0], Bone::Head, r#"<path d="M194 200C198 238 214 262 230 276L282 276C298 262 314 238 318 200Z"/>"#),
part("neck", 0xF7CDBE, [235.0, 221.0, 42.0, 50.0], Bone::Body, r#"<path d="M236 222L276 222L276 266C268 270 244 270 236 266Z"/>"#),
part("collar", 0xFFFFFF, [197.0, 255.0, 118.0, 30.0], Bone::Body, r#"<path d="M198 284C204 268 222 258 238 256L256 272L274 256C290 258 308 268 314 284Z"/>"#),
part("bow", 0xF05A97, [221.0, 247.0, 70.0, 32.0], Bone::Body, r#"<path d="M256 264C244 250 226 248 224 258C222 270 236 278 256 264ZM256 264C268 250 286 248 288 258C290 270 276 278 256 264Z"/>"#),
part("knot", 0xD94683, [247.0, 255.0, 18.0, 18.0], Bone::Body, r#"<circle cx="256" cy="264" r="8"/>"#),
part("skin", 0xFFE7DC, [177.0, 93.0, 158.0, 152.0], Bone::Head, concat!(r#"<path d=""#, "M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z", r#""/>"#)),
part("face-shade", 0xF9D8CB, [161.0, 73.0, 190.0, 118.0], Bone::Head, concat!(r#"<defs><clipPath id="face"><path d=""#, "M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z", r#""/></clipPath></defs><g clip-path="url(#face)"><path transform="translate(0 12)" d=""#, "M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z", r#""/></g>"#)),
eye("iris-l", 0xA24FB9, 222.0, 0, r#"<ellipse cx="222" cy="188" rx="18" ry="23"/>"#),
eye("ring-l", 0xEE84C4, 222.0, 0, r#"<ellipse cx="222" cy="198" rx="13.5" ry="11.5"/>"#),
eye("pupil-l", 0x5A2368, 222.0, 0, r#"<ellipse cx="222" cy="190" rx="7.5" ry="10.5"/>"#),
eye("shine-l", 0xFFFFFF, 222.0, 0, r#"<circle cx="215" cy="178" r="7"/><circle cx="229" cy="199" r="3.2"/>"#),
Part { name: "lid-l", color: 0x45233F, alpha: 1.0, frame: [193.0, 158.0, 52.0, 26.0], pivot: (222.0, 192.0), bone: Bone::Lid(0), shapes: r#"<path d="M194 182C195 168 208 159 224 159C234 159 241 164 244 172L240 173C236 168 231 165 224 165C214 165 207 172 204 183Z"/>"# },
Part { name: "lash-l", color: 0xC2506F, alpha: 0.35, frame: [213.0, 210.0, 18.0, 6.0], pivot: (222.0, 192.0), bone: Bone::Lid(0), shapes: r#"<path fill="none" stroke="black" stroke-width="2" stroke-linecap="round" d="M215 212Q222 214 229 212"/>"# },
eye("iris-r", 0xA24FB9, 290.0, 1, r#"<ellipse cx="290" cy="188" rx="18" ry="23"/>"#),
eye("ring-r", 0xEE84C4, 290.0, 1, r#"<ellipse cx="290" cy="198" rx="13.5" ry="11.5"/>"#),
eye("pupil-r", 0x5A2368, 290.0, 1, r#"<ellipse cx="290" cy="190" rx="7.5" ry="10.5"/>"#),
eye("shine-r", 0xFFFFFF, 290.0, 1, r#"<circle cx="283" cy="178" r="7"/><circle cx="297" cy="199" r="3.2"/>"#),
Part { name: "lid-r", color: 0x45233F, alpha: 1.0, frame: [267.0, 158.0, 52.0, 26.0], pivot: (290.0, 192.0), bone: Bone::Lid(1), shapes: r#"<path d="M318 182C317 168 304 159 288 159C278 159 271 164 268 172L272 173C276 168 281 165 288 165C298 165 305 172 308 183Z"/>"# },
Part { name: "lash-r", color: 0xC2506F, alpha: 0.35, frame: [281.0, 210.0, 18.0, 6.0], pivot: (290.0, 192.0), bone: Bone::Lid(1), shapes: r#"<path fill="none" stroke="black" stroke-width="2" stroke-linecap="round" d="M283 212Q290 214 297 212"/>"# },
Part { name: "arc-l", color: 0x45233F, alpha: 1.0, frame: [200.0, 171.0, 44.0, 31.0], pivot: (222.0, 192.0), bone: Bone::Arc(0), shapes: r#"<path fill="none" stroke="black" stroke-width="7" stroke-linecap="round" d="M205 197Q222 176 239 197"/>"# },
Part { name: "arc-r", color: 0x45233F, alpha: 1.0, frame: [268.0, 171.0, 44.0, 31.0], pivot: (290.0, 192.0), bone: Bone::Arc(1), shapes: r#"<path fill="none" stroke="black" stroke-width="7" stroke-linecap="round" d="M273 197Q290 176 307 197"/>"# },
part("lines", 0x45233F, [201.0, 190.0, 110.0, 8.0], Bone::Lines, r#"<path fill="none" stroke="black" stroke-width="6" stroke-linecap="round" d="M205 194H239M273 194H307"/>"#),
Part { name: "blush", color: 0xFF8DAA, alpha: 0.55, frame: [180.0, 206.0, 152.0, 20.0], pivot: (256.0, 216.0), bone: Bone::Blush, shapes: r#"<ellipse cx="198" cy="216" rx="15" ry="8"/><ellipse cx="314" cy="216" rx="15" ry="8"/>"# },
mouth("smile", 0xC2506F, [241.0, 219.0, 30.0, 13.0], Mouth::Smile, r#"<path fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" d="M244 222Q250 229 256 223Q262 229 268 222"/>"#),
mouth("grin", 0xC2506F, [242.0, 218.0, 28.0, 24.0], Mouth::Grin, r#"<path d="M243 219Q256 241 269 219Z"/>"#),
mouth("tongue", 0xFF8DAA, [248.0, 224.0, 16.0, 13.0], Mouth::Grin, r#"<path d="M249 229Q256 236 263 229Q256 225 249 229Z"/>"#),
mouth("gasp", 0xC2506F, [249.0, 218.0, 14.0, 16.0], Mouth::Gasp, r#"<ellipse cx="256" cy="226" rx="5.5" ry="7"/>"#),
mouth("flat", 0xC2506F, [246.0, 222.0, 20.0, 6.0], Mouth::Flat, r#"<path fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" d="M249 225H263"/>"#),
mouth("frown", 0xC2506F, [243.0, 217.0, 26.0, 14.0], Mouth::Frown, r#"<path fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" d="M246 228Q256 220 266 228"/>"#),
Part { name: "lock-l", color: 0xF7A6C9, alpha: 1.0, frame: [147.0, 117.0, 54.0, 200.0], pivot: (184.0, 118.0), bone: Bone::Lock(0), shapes: r#"<path d="M184 118C164 150 156 204 160 258C162 282 156 300 148 316C164 312 178 300 186 286C190 256 192 214 198 176C200 152 196 132 184 118Z"/>"# },
Part { name: "lock-r", color: 0xF7A6C9, alpha: 1.0, frame: [311.0, 117.0, 54.0, 200.0], pivot: (328.0, 118.0), bone: Bone::Lock(1), shapes: r#"<path d="M328 118C348 150 356 204 352 258C350 282 356 300 364 316C348 312 334 300 326 286C322 256 320 214 314 176C312 152 316 132 328 118Z"/>"# },
part("bangs", 0xF7A6C9, [161.0, 61.0, 190.0, 118.0], Bone::Head, concat!(r#"<path d=""#, "M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z", r#""/>"#)),
part("gloss", 0xFFD1E4, [195.0, 83.0, 122.0, 26.0], Bone::Head, r#"<path d="M196 108C220 84 292 84 316 108C292 92 220 92 196 108Z"/>"#),
Part { name: "ahoge", color: 0xF7A6C9, alpha: 1.0, frame: [237.0, 17.0, 44.0, 50.0], pivot: (258.0, 64.0), bone: Bone::Ahoge, shapes: r#"<path d="M250 66C238 40 252 18 280 20C264 28 258 44 262 64Z"/>"# },
part("pin", 0xFFE48A, [303.0, 85.0, 30.0, 30.0], Bone::Head, r#"<path d="M318 86Q320 98 332 100Q320 102 318 114Q316 102 304 100Q316 98 318 86Z"/>"#),
part("brows", 0x45233F, [202.0, 148.0, 108.0, 16.0], Bone::Brows, r#"<g fill="none" stroke="black" stroke-width="5" stroke-linecap="round"><path d="M206 160L234 152"/><path d="M306 160L278 152"/></g>"#),
part("hand-l", 0xFFE7DC, [149.0, 261.0, 46.0, 26.0], Bone::Hand(0), r#"<path d="M150 286L150 272C150 264 161 264 161 272C161 262 172 262 172 271C172 262 183 262 183 272C183 264 194 264 194 273L194 286Z"/>"#),
part("knuckles-l", 0xF2BBA8, [159.0, 271.0, 26.0, 17.0], Bone::Hand(0), r#"<path fill="none" stroke="black" stroke-width="2" stroke-linecap="round" d="M161 274V286M172 273V286M183 274V286"/>"#),
part("hand-r", 0xFFE7DC, [317.0, 261.0, 46.0, 26.0], Bone::Hand(1), r#"<path d="M318 286L318 273C318 264 329 264 329 272C329 262 340 262 340 271C340 262 351 262 351 272C351 264 362 264 362 272L362 286Z"/>"#),
part("knuckles-r", 0xF2BBA8, [327.0, 271.0, 26.0, 17.0], Bone::Hand(1), r#"<path fill="none" stroke="black" stroke-width="2" stroke-linecap="round" d="M329 274V286M340 273V286M351 274V286"/>"#),
mark("bang", 0xFFB020, [368.0, 35.0, 20.0, 75.0], (378.0, 107.0), Mark::Bang, r#"<path d="M371 38L385 38L380 86L376 86Z"/><circle cx="378" cy="100" r="7"/>"#),
mark("bang-edge", 0x45233F, [368.0, 35.0, 20.0, 75.0], (378.0, 107.0), Mark::Bang, r#"<g fill="none" stroke="black" stroke-width="3"><path d="M371 38L385 38L380 86L376 86Z"/><circle cx="378" cy="100" r="7"/></g>"#),
mark("z1", 0x9FA3D9, [348.0, 66.0, 28.0, 30.0], (362.0, 81.0), Mark::Z1, r#"<path fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" d="M352 70H372L352 92H372"/>"#),
mark("z2", 0x9FA3D9, [376.0, 34.0, 22.0, 24.0], (387.0, 46.0), Mark::Z2, r#"<path fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" d="M380 38H394L380 54H394"/>"#),
mark("heart", 0xFF5C93, [349.0, 55.0, 58.0, 46.0], (378.0, 82.0), Mark::Heart, r#"<path d="M378 70C366 56 350 66 356 80C360 90 378 100 378 100C378 100 396 90 400 80C406 66 390 56 378 70Z"/>"#),
mark("sparkle-l", 0xFFE48A, [123.0, 69.0, 34.0, 34.0], (140.0, 86.0), Mark::SparkleLeft, r#"<path d="M140 70Q143 84 156 86Q143 88 140 102Q137 88 124 86Q137 84 140 70Z"/>"#),
mark("sparkle-r", 0xFFE48A, [367.0, 51.0, 26.0, 26.0], (380.0, 64.0), Mark::SparkleRight, r#"<path d="M380 52Q382 62 392 64Q382 66 380 76Q378 66 368 64Q378 62 380 52Z"/>"#),
mark("drop", 0x9CD5FF, [336.0, 126.0, 22.0, 32.0], (346.0, 142.0), Mark::Drop, r#"<path d="M346 128Q356 144 350 152Q340 156 338 146Q338 138 346 128Z"/>"#),
mark("drop-edge", 0x5AA0D8, [336.0, 126.0, 22.0, 32.0], (346.0, 142.0), Mark::Drop, r#"<path fill="none" stroke="black" stroke-width="2" d="M346 128Q356 144 350 152Q340 156 338 146Q338 138 346 128Z"/>"#),
];
/// A layer of an open eye centred at `x`.
#[rustfmt::skip]
const fn eye(name: &'static str, color: u32, x: f32, side: usize, shapes: &'static str) -> Part {
let frame = [x - 19.0, 164.0, 38.0, 48.0];
Part { name, color, alpha: 1.0, frame, pivot: (x, 192.0), bone: Bone::Iris(side), shapes }
}
#[rustfmt::skip]
const fn mouth(name: &'static str, color: u32, frame: [f32; 4], mouth: Mouth, shapes: &'static str) -> Part {
Part { name, color, alpha: 1.0, frame, pivot: (256.0, 224.0), bone: Bone::Mouth(mouth), shapes }
}
#[rustfmt::skip]
const fn mark(name: &'static str, color: u32, frame: [f32; 4], pivot: (f32, f32), mark: Mark, shapes: &'static str) -> Part {
Part { name, color, alpha: 1.0, frame, pivot, bone: Bone::Mark(mark), shapes }
}
/// Every layer's key and SVG, made once.
pub struct Rig {
layers: Vec<(&'static Part, SharedString, Vec<u8>)>,
}
impl Rig {
pub fn new() -> Self {
let layers = (PARTS.iter())
.map(|part| {
let key = format!("ely/{}.svg", part.name).into();
(part, key, svg(part).into_bytes())
})
.collect();
Self { layers }
}
/// Draws her in `pose`, the head's box from `origin`.
pub fn paint(&self, origin: Point<Pixels>, pose: &Pose, window: &mut Window, cx: &App) {
let scale = window.scale_factor();
let (left, top) = (f32::from(origin.x), f32::from(origin.y));
let at = |(x, y): (f32, f32)| {
let x = (left + (x - VIEW[0]) * UNIT) * scale;
(x, (top + (y - VIEW[1]) * UNIT) * scale)
};
let by = |(x, y): (f32, f32)| (x * UNIT * scale, y * UNIT * scale);
let whole = shift(TransformationMatrix::unit(), by((pose.shake, -pose.lift)));
let breath = shift(whole, by((0.0, -pose.bob)));
let head = turn(breath, at((256.0, 236.0)), pose.tilt);
for (part, key, svg) in &self.layers {
let Some((matrix, alpha)) = moved(part, pose, [whole, head], &at, &by) else {
continue;
};
let [x, y, w, h] = part.frame;
let bounds = Bounds::new(
origin + point(px((x - VIEW[0]) * UNIT), px((y - VIEW[1]) * UNIT)),
size(px(w * UNIT), px(h * UNIT)),
);
let color = Hsla::from(rgb(part.color)).opacity(part.alpha * alpha);
let drawn =
window.paint_svg(bounds, key.clone(), Some(svg.as_slice()), matrix, color, cx);
if let Err(error) = drawn {
log::error!("pet: Ely's {} does not draw: {error:#}", part.name);
}
}
}
}
/// Where `part` stands in `pose`, and its opacity; none while the pose hides it.
fn moved(
part: &Part,
pose: &Pose,
[whole, head]: [TransformationMatrix; 2],
at: &impl Fn((f32, f32)) -> Device,
by: &impl Fn((f32, f32)) -> Device,
) -> Option<(TransformationMatrix, f32)> {
let pivot = at(part.pivot);
let open = |side: usize| pose.eyes == Eyes::Open || (pose.eyes == Eyes::Wink && side == 1);
let shut = |side: usize| pose.eyes == Eyes::Arcs || (pose.eyes == Eyes::Wink && side == 0);
let squash = |side: usize, wide: f32| grow(head, pivot, (wide, wide * pose.open[side]));
Some(match part.bone {
Bone::Body => (whole, 1.0),
Bone::Head => (head, 1.0),
Bone::Lid(side) if open(side) => (squash(side, pose.wide), 1.0),
Bone::Iris(side) if open(side) => (shift(squash(side, pose.wide), by(pose.look)), 1.0),
Bone::Arc(side) if shut(side) => (squash(side, 1.0), 1.0),
Bone::Lines if pose.eyes == Eyes::Lines => (head, 1.0),
Bone::Brows if pose.brows => (head, 1.0),
Bone::Blush => (head, 1.0 + 0.6 * pose.flush),
Bone::Mouth(mouth) if mouth == pose.mouth => (grow(head, pivot, (pose.pop, pose.pop)), 1.0),
Bone::Lock(side) => {
let sway = if side == 0 { pose.sway } else { -pose.sway };
(turn(head, pivot, sway), 1.0)
}
Bone::Ahoge => (turn(head, pivot, pose.ahoge), 1.0),
Bone::Hand(side) => (shift(whole, by((0.0, pose.hands[side]))), 1.0),
Bone::Mark(mark) => {
let &(_, motion) = pose.marks.iter().find(|(shown, _)| *shown == mark)?;
let moved = shift(head, by(motion.at));
let turned = turn(moved, pivot, motion.turn);
(
grow(turned, pivot, (motion.scale, motion.scale)),
motion.alpha,
)
}
_ => return None,
})
}
fn shift(matrix: TransformationMatrix, (x, y): Device) -> TransformationMatrix {
matrix.translate(point(ScaledPixels(x), ScaledPixels(y)))
}
/// `matrix`, after a turn of `degrees` clockwise about `pivot`.
fn turn(matrix: TransformationMatrix, pivot: Device, degrees: f32) -> TransformationMatrix {
let turned = shift(matrix, pivot).rotate(radians(degrees.to_radians()));
shift(turned, (-pivot.0, -pivot.1))
}
/// `matrix`, after a growth by `(x, y)` about `pivot`.
fn grow(matrix: TransformationMatrix, pivot: Device, (x, y): (f32, f32)) -> TransformationMatrix {
shift(shift(matrix, pivot).scale(size(x, y)), (-pivot.0, -pivot.1))
}
/// The SVG of `part`: its shapes in its box.
fn svg(part: &Part) -> String {
let [x, y, w, h] = part.frame;
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {w} {h}" width="{w}" height="{h}">{}</svg>"#,
part.shapes
)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use resvg::usvg;
use super::*;
#[test]
fn every_layer_parses_and_stays_in_its_box() {
let mut names = HashSet::new();
for part in PARTS {
assert!(names.insert(part.name), "{} twice", part.name);
let tree = usvg::Tree::from_str(&svg(part), &usvg::Options::default())
.unwrap_or_else(|error| panic!("{}: {error}", part.name));
let drawn = tree.root().abs_stroke_bounding_box();
let [_, _, width, height] = part.frame;
assert!(
drawn.left() >= 0.0
&& drawn.top() >= 0.0
&& drawn.right() <= width
&& drawn.bottom() <= height,
"{}: {drawn:?} spills out of {width} by {height}",
part.name
);
}
}
#[test]
fn she_is_drawn_from_every_part_the_icon_gives_her() {
let named = |name: &str| PARTS.iter().any(|part| part.name == name);
for name in [
"hair-back",
"skin",
"bangs",
"ahoge",
"lock-l",
"lock-r",
"hand-l",
"hand-r",
] {
assert!(named(name), "{name}");
}
let marks = [
Mark::Bang,
Mark::Z1,
Mark::Z2,
Mark::Heart,
Mark::SparkleLeft,
Mark::SparkleRight,
Mark::Drop,
];
for mark in marks {
assert!(
PARTS.iter().any(|part| part.bone == Bone::Mark(mark)),
"{mark:?}"
);
}
}
}