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

+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));
}