Skip to main content

animation/
animation.rs

1//! An `Elf` posed by one `Animator` over ten states: `Idle` and `Dance`
2//! loop in place, `Locomotion` blends `Walk` into `Jog` paced by the speed
3//! `WASD` or the arrows are held (`Left Shift` runs, relative to the
4//! camera), `Attack`, `Hit`, `Jump`, `SitDown` and `StandUp` each play
5//! once, and `Sit` loops between them. A walk onto one of the red patches
6//! plays `Hit`; a third one plays `Death` and holds it until
7//! `Button::Restart` starts a new `Animator`. `Button::Interact` near the
8//! seat sits the elf on it and stands it back up, and a move, `Button::Attack`
9//! or `Button::Jump` leaves the seat at once. A second `Elf`, standing
10//! in place, is posed by a machine of one state that scrubs its own
11//! `SitDown` by its distance from the first. A click, `Button::Hold`,
12//! takes the held pointer; `Button::Release` (escape) frees it again. The
13//! pointer orbits and tilts the camera, which always frames the whole
14//! figure; a prompt over the seat,
15//! the patches, and the second elf names what each does; the panel names
16//! the state, the hits taken, and the controls. The sun lights the ground; a
17//! lamp post by the seat casts the elf's own shadow as it walks past; a cone
18//! of light over the red patches lights them from above; and a `Butterfly`,
19//! one `Animator` looping `Fly`, orbits the seat and the lamp, its own point
20//! light colorful and shadowed too.
21
22use core::f32::consts::TAU;
23use core::ops::Range;
24use core::time::Duration;
25
26use mirage_engine::prelude::*;
27
28/// The elf's source, next to the other example assets.
29const ELF_SOURCE: &str = "examples/assets/elf.glb";
30/// The root node the source names the elf under.
31const ELF_ROOT: &str = "Elf";
32/// The elf's own height, as `tools/elf_fixture.py` holds it: read here only
33/// to lift the second elf's prompt over its head, since nothing in the
34/// mesh API reports a draw's bounds back to the game.
35const ELF_HEIGHT: f32 = 1.6;
36
37/// The window this game opens at, and the surface [`CAMERA_YAW_SCALE`] and
38/// [`CAMERA_PITCH_SCALE`] read a drag as a fraction of.
39const WINDOW_WIDTH: u32 = 1280;
40const WINDOW_HEIGHT: u32 = 720;
41
42/// The elf's top speed, in meters per second.
43const ELF_SPEED: f32 = 4.0;
44/// The fraction of top speed a walk, without `Button::Run` held, is held
45/// at: past it a held run is what raises `Locomotion`'s blend toward `Jog`.
46const WALK_CAP: f32 = 0.5;
47/// The least speed `Locomotion` reads over `Idle`.
48const WALK_THRESHOLD: f32 = 0.1;
49/// The turn rate facing a new heading, in radians a second.
50const TURN_RATE: f32 = TAU * 2.0;
51
52/// The elf's upward speed the instant `Jump` is entered, in meters a
53/// second; with [`GRAVITY`] it is back on the ground in about `0.9`
54/// seconds, near where the clip's own landing falls, inside its length.
55const JUMP_LAUNCH_SPEED: f32 = 4.5;
56/// Acceleration that takes the elf's height down while off the ground, in
57/// meters a second squared.
58const GRAVITY: f32 = 9.8;
59
60const IDLE_LOCOMOTION_FADE: Duration = Duration::from_millis(200);
61const ATTACK_ENTER_FADE: Duration = Duration::from_millis(80);
62const ATTACK_CHAIN_FADE: Duration = Duration::from_millis(60);
63const ATTACK_EXIT_FADE: Duration = Duration::from_millis(200);
64/// Where along `Attack` a second press chains into it again, past the
65/// start the first press already played.
66const ATTACK_CHAIN_ENTRY: f32 = 0.15;
67/// How far along `Attack` it stops reading new presses and instead runs to
68/// its own end.
69const ATTACK_RELEASE: f32 = 0.8;
70const HIT_ENTER_FADE: Duration = Duration::from_millis(50);
71const HIT_EXIT_FADE: Duration = Duration::from_millis(150);
72const DEATH_FADE: Duration = Duration::from_millis(150);
73const JUMP_ENTER_FADE: Duration = Duration::from_millis(100);
74const JUMP_EXIT_FADE: Duration = Duration::from_millis(150);
75const SIT_DOWN_FADE: Duration = Duration::from_millis(200);
76const STAND_UP_FADE: Duration = Duration::from_millis(150);
77const STAND_EXIT_FADE: Duration = Duration::from_millis(150);
78const DANCE_FADE: Duration = Duration::from_millis(200);
79
80const ELF_START: Vec3 = Vec3::new(-3.0, 0.0, 4.0);
81
82/// Ground positions of the three hurt patches, and their radius.
83const HURT_PATCHES: [Vec3; 3] = [
84    Vec3::new(1.5, 0.0, -1.0),
85    Vec3::new(-1.5, 0.0, -3.5),
86    Vec3::new(3.0, 0.0, 2.0),
87];
88const HURT_RADIUS: f32 = 0.9;
89/// Hits it takes before the elf reads `Death` instead of `Hit`.
90const FATAL_HITS: u32 = 3;
91
92/// The seat's position, at the ground, and its footprint: measured on the
93/// asset, `sit_down` lowers the pelvis from `0.77` meters to `0.47` meters
94/// and moves it `0.29` meters toward the seat, the feet staying where they
95/// stood, so a block this tall under that landing puts the pelvis on its
96/// top face.
97const SEAT_POSITION: Vec3 = Vec3::new(-3.5, 0.0, -3.0);
98const SEAT_FOOTPRINT: f32 = 1.0;
99const SEAT_HEIGHT: f32 = 0.45;
100/// How tall the figure sits, from the seat's top to its head.
101const SEATED_HEIGHT: f32 = 0.75;
102/// Height from the ground to the seated elf's head.
103const SEAT_HEAD_HEIGHT: f32 = SEAT_HEIGHT + SEATED_HEIGHT;
104/// The gap in front of the seat's own face the elf stands at.
105const SEAT_STAND_CLEARANCE: f32 = 0.05;
106/// Where the elf stands to sit on the seat, at its front face plus
107/// [`SEAT_STAND_CLEARANCE`], and which way it faces there: away from the
108/// seat, so `sit_down` moves the pelvis back onto it.
109const SEAT_SPOT: Vec3 = Vec3::new(
110    SEAT_POSITION.x,
111    0.0,
112    SEAT_POSITION.z + SEAT_FOOTPRINT * 0.5 + SEAT_STAND_CLEARANCE,
113);
114const SEAT_FACING: f32 = 0.0;
115/// The least distance from [`SEAT_POSITION`] `Button::Interact` sits the
116/// elf down at.
117const SEAT_INTERACT_RADIUS: f32 = 1.6;
118
119/// The second elf's fixed position, under a machine scrubbed by its
120/// distance from the first.
121const SCRUBBED_ELF_POSITION: Vec3 = Vec3::new(3.5, 0.0, 4.0);
122/// The distance at and under which the scrubbed elf reads fully seated.
123const SCRUB_NEAR: f32 = 1.5;
124/// The distance at and past which it reads fully standing.
125const SCRUB_FAR: f32 = 5.0;
126
127/// The lamp post's own position, near the seat and its stand.
128const LAMP_POST_POSITION: Vec3 = Vec3::new(-4.9, 0.0, -2.0);
129const LAMP_POST_HEIGHT: f32 = 2.2;
130const LAMP_POST_THICKNESS: f32 = 0.16;
131const LAMP_POST_COLOR: Color = Color::rgb(0.16, 0.14, 0.12);
132/// The lamp's own head, on top of the post, emissive in [`LAMP_LIGHT_COLOR`].
133const LAMP_HEAD_SIZE: f32 = 0.34;
134/// The gap left between the post's own top and the head's bottom face, so
135/// the light sits clear of both meshes rather than inside the head it
136/// would then cast no light from.
137const LAMP_HEAD_GAP: f32 = 0.06;
138/// Past `1.0`, so its glow lands on the ground near it, visible against
139/// the sky, and the head reads bright once bloom spreads it.
140const LAMP_LIGHT_COLOR: Color = Color::rgb(5.5, 4.2, 2.2);
141const LAMP_LIGHT_RANGE: f32 = 6.0;
142
143/// Centered over the three [`HURT_PATCHES`], tall enough for one cone to
144/// reach all of them.
145const SPOT_POSITION: Vec3 = Vec3::new(1.0, 6.0, -0.83);
146const SPOT_DIRECTION: Vec3 = Vec3::NEG_Y;
147/// Past `1.0`, so the cone is visible on the ground against the sky, and
148/// bright enough that a patch inside it reads well past a patch outside.
149const SPOT_COLOR: Color = Color::rgb(11.0, 9.8, 8.2);
150const SPOT_RANGE: f32 = 9.0;
151const SPOT_ANGLE: f32 = 0.85;
152/// The fixture's own edge length, drawn where the cone starts.
153const SPOT_FIXTURE_SIZE: f32 = 0.22;
154const SPOT_FIXTURE_COLOR: Color = Color::rgb(0.2, 0.2, 0.22);
155
156/// The butterfly's own source, next to the other example assets.
157const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
158/// The root node the source names the butterfly under.
159const BUTTERFLY_ROOT: &str = "Butterfly";
160/// The point halfway between [`SEAT_POSITION`] and [`LAMP_POST_POSITION`],
161/// the closed path's own center.
162const BUTTERFLY_CENTER: Vec3 = Vec3::new(-4.2, 0.0, -2.5);
163/// The closed path's radius along `x` and `z`, wide enough to loop around
164/// both the seat and the lamp.
165const BUTTERFLY_RADIUS: Vec2 = Vec2::new(1.8, 1.4);
166/// About the lamp's own height.
167const BUTTERFLY_HEIGHT: f32 = LAMP_POST_HEIGHT;
168/// Radians a second around the path; a full loop takes about 14 seconds.
169const BUTTERFLY_ANGULAR_SPEED: f32 = TAU / 14.0;
170/// Past `1.0`, so its glow lands on the ground and the lamp post it passes.
171const BUTTERFLY_LIGHT_COLOR: Color = Color::rgb(1.8, 5.5, 5.0);
172const BUTTERFLY_LIGHT_RANGE: f32 = 3.0;
173/// The butterfly's own small emissive, so it reads bright rather than
174/// dark against the glow it casts.
175const BUTTERFLY_EMISSIVE: Color = Color::rgb(0.6, 1.8, 1.6);
176
177const GROUND_SIZE: f32 = 400.0;
178const GROUND_COLOR: Color = Color::rgb(0.24, 0.30, 0.22);
179const HURT_COLOR: Color = Color::rgb(0.75, 0.12, 0.10);
180const SEAT_COLOR: Color = Color::rgb(0.5, 0.42, 0.3);
181/// Low, near the horizon, and dim.
182const SUN_DIRECTION: Vec3 = Vec3::new(-0.85, -0.18, -0.5);
183const SUN_COLOR: Color = Color::rgb(0.55, 0.32, 0.22);
184const SKY_ZENITH: Color = Color::rgb(0.06, 0.07, 0.2);
185const SKY_HORIZON: Color = Color::rgb(0.55, 0.35, 0.28);
186const SKY_NADIR: Color = Color::rgb(0.05, 0.05, 0.07);
187/// The fraction of its own light the sky lands and reflects: dim, so the
188/// lamp, spotlight and butterfly lights read against it.
189const SKY_LIGHT: f32 = 0.15;
190
191/// The orbit camera's distance behind and height above its target.
192const CAMERA_BACK: f32 = 3.4;
193const CAMERA_UP: f32 = 1.7;
194/// Height above the ground the camera looks at, framing the whole figure.
195const CAMERA_LOOK_HEIGHT: f32 = 0.8;
196const CAMERA_FOV: f32 = 50.0;
197/// Radians the camera orbits, or tilts, per pixel the pointer moves,
198/// chosen so a drag the width, or the height, of the window turns it by
199/// [`CAMERA_YAW_PER_DRAG`], or [`CAMERA_PITCH_PER_DRAG`].
200const CAMERA_YAW_PER_DRAG: f32 = core::f32::consts::PI;
201const CAMERA_PITCH_PER_DRAG: f32 = core::f32::consts::FRAC_PI_3;
202const CAMERA_YAW_SCALE: f32 = CAMERA_YAW_PER_DRAG / WINDOW_WIDTH as f32;
203const CAMERA_PITCH_SCALE: f32 = CAMERA_PITCH_PER_DRAG / WINDOW_HEIGHT as f32;
204/// The range the camera's tilt is held inside, in radians: short of
205/// looking flat along the ground or straight down, either of which would
206/// stop framing the figure.
207const CAMERA_PITCH_RANGE: Range<f32> = -0.4..0.9;
208
209/// The panel's controls, a key and what it does.
210const CONTROLS: [(&str, &str); 10] = [
211    ("mouse", "turns the camera"),
212    ("click", "locks the pointer"),
213    ("escape", "frees the pointer"),
214    ("wasd or arrows", "walk"),
215    ("left shift", "runs"),
216    ("f", "attacks, chains on a second press"),
217    ("space", "jumps"),
218    ("n", "dances while idle"),
219    ("e", "sits on the seat and stands back up"),
220    ("r", "starts a new elf"),
221];
222
223/// The UI's own text color, read over the ground and the sky both.
224const PANEL_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
225/// How much dark a panel or a prompt's own backdrop puts behind its text.
226const PANEL_BACKDROP: u8 = 190;
227/// The panel's own inner margin, around its labels.
228const PANEL_PADDING: i8 = 8;
229/// The size a world-space prompt reads at, in logical points.
230const PROMPT_SIZE: f32 = 15.0;
231/// Height a world-space prompt is lifted over the point it names.
232const PROMPT_LIFT: f32 = 0.35;
233/// Margin a prompt's own backdrop keeps past its galley, in logical points.
234const PROMPT_PADDING: f32 = 4.0;
235
236meshes! { enum Shape { Plane, Cube, Elf, Butterfly } }
237
238/// The one sky this game draws, a gradient set each frame.
239#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
240enum Sky {
241    Day,
242}
243
244impl Skyboxes for Sky {
245    fn build(&self, _assets: &Assets) -> SkyboxData {
246        match self {
247            Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
248        }
249    }
250}
251
252#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
253struct Elf;
254
255/// The clips `ElfState` and `ScrubbedState` play, named as the source
256/// names them.
257#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
258enum ElfClip {
259    #[clip("idle")]
260    Idle,
261    #[clip("walk")]
262    Walk,
263    #[clip("jog")]
264    Jog,
265    #[clip("attack")]
266    Attack,
267    #[clip("hit")]
268    Hit,
269    #[clip("death")]
270    Death,
271    #[clip("sit_down")]
272    SitDown,
273    #[clip("sit")]
274    Sit,
275    #[clip("stand_up")]
276    StandUp,
277    #[clip("jump")]
278    Jump,
279    #[clip("dance")]
280    Dance,
281}
282
283impl Mesh<NoParts, ElfClip> for Elf {
284    fn build(&self, assets: &Assets) -> MeshData<NoParts, ElfClip> {
285        assets.model(ELF_ROOT)
286    }
287}
288
289/// What the game fills each tick to move `ElfState` on.
290#[derive(Default)]
291struct ElfInput {
292    /// The elf's speed this tick, a fraction of [`ELF_SPEED`].
293    speed: f32,
294    attack: bool,
295    /// True the tick a hurt patch is stepped onto and [`FATAL_HITS`] have
296    /// not yet landed.
297    hit: bool,
298    /// True the tick a hurt patch is stepped onto the third time, which
299    /// [`FATAL_HITS`] counts.
300    dying: bool,
301    jump: bool,
302    /// True the tick `Jump`'s own height curve returns the elf to the
303    /// ground after it launches.
304    landed: bool,
305    dance: bool,
306    /// `Button::Interact`, read as sitting down, standing back up, or
307    /// nothing, by the state it reaches.
308    interact: bool,
309    /// Whether the elf stands close enough to the cube to sit on it.
310    near_seat: bool,
311}
312
313#[derive(Clone, Copy, Eq, PartialEq, Debug)]
314enum ElfState {
315    Idle,
316    Locomotion,
317    Attack,
318    Hit,
319    Death,
320    SitDown,
321    Sit,
322    StandUp,
323    Jump,
324    Dance,
325}
326
327impl ElfState {
328    /// Whether the elf is on the seat, or on its way onto or off it.
329    fn seated(self) -> bool {
330        matches!(self, Self::SitDown | Self::Sit | Self::StandUp)
331    }
332
333    /// `Locomotion` where `input` reads a walk or a run, `Idle` at rest:
334    /// where a grounded state returns once whatever interrupted it ends.
335    fn grounded(input: &ElfInput) -> Self {
336        match input.speed > WALK_THRESHOLD {
337            true => Self::Locomotion,
338            false => Self::Idle,
339        }
340    }
341}
342
343impl AnimationStates for ElfState {
344    type Clip = ElfClip;
345    type Input = ElfInput;
346
347    fn entry() -> Self {
348        Self::Idle
349    }
350
351    fn motion(&self, input: &ElfInput) -> Motion<ElfClip> {
352        match self {
353            Self::Idle => Motion::looping(ElfClip::Idle),
354            Self::Locomotion => {
355                Motion::blend(ElfClip::Walk, ElfClip::Jog, input.speed).paced(input.speed)
356            }
357            Self::Attack => Motion::once(ElfClip::Attack),
358            Self::Hit => Motion::once(ElfClip::Hit),
359            Self::Death => Motion::once(ElfClip::Death),
360            Self::SitDown => Motion::once(ElfClip::SitDown),
361            Self::Sit => Motion::looping(ElfClip::Sit),
362            Self::StandUp => Motion::once(ElfClip::StandUp),
363            Self::Jump => Motion::once(ElfClip::Jump),
364            Self::Dance => Motion::looping(ElfClip::Dance),
365        }
366    }
367
368    fn next(&self, input: &ElfInput, at: Progress) -> Option<Transition<Self>> {
369        match (self, input) {
370            (Self::Death, _) => None,
371            (_, ElfInput { dying: true, .. }) => Some(Self::Death.fade(DEATH_FADE)),
372            (_, ElfInput { hit: true, .. }) if *self != Self::Hit => {
373                Some(Self::Hit.fade(HIT_ENTER_FADE))
374            }
375            (Self::Hit, _) if at.ended() => Some(ElfState::grounded(input).fade(HIT_EXIT_FADE)),
376            (Self::Attack, ElfInput { attack: true, .. }) if at.past(ATTACK_RELEASE) => Some(
377                Self::Attack
378                    .restarted()
379                    .entering_at(ATTACK_CHAIN_ENTRY)
380                    .fade(ATTACK_CHAIN_FADE),
381            ),
382            (Self::Attack, _) if at.past(ATTACK_RELEASE) => {
383                Some(ElfState::grounded(input).fade(ATTACK_EXIT_FADE))
384            }
385            (Self::SitDown | Self::Sit | Self::StandUp, i) if i.speed > WALK_THRESHOLD => {
386                Some(Self::Locomotion.fade(STAND_EXIT_FADE))
387            }
388            (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { attack: true, .. }) => {
389                Some(Self::Attack.fade(ATTACK_ENTER_FADE))
390            }
391            (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { jump: true, .. }) => {
392                Some(Self::Jump.fade(JUMP_ENTER_FADE))
393            }
394            (Self::SitDown, _) if at.ended() => Some(Self::Sit.at_once()),
395            (Self::Sit, ElfInput { interact: true, .. }) => Some(Self::StandUp.fade(STAND_UP_FADE)),
396            (Self::StandUp, _) if at.ended() => Some(Self::Idle.fade(STAND_EXIT_FADE)),
397            (Self::Jump, ElfInput { landed: true, .. }) => {
398                Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE))
399            }
400            (Self::Jump, _) if at.ended() => Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE)),
401            (
402                Self::Idle | Self::Locomotion,
403                ElfInput {
404                    interact: true,
405                    near_seat: true,
406                    ..
407                },
408            ) => Some(Self::SitDown.fade(SIT_DOWN_FADE)),
409            (Self::Idle | Self::Locomotion, ElfInput { attack: true, .. }) => {
410                Some(Self::Attack.fade(ATTACK_ENTER_FADE))
411            }
412            (Self::Idle | Self::Locomotion, ElfInput { jump: true, .. }) => {
413                Some(Self::Jump.fade(JUMP_ENTER_FADE))
414            }
415            (Self::Idle, ElfInput { dance: true, .. }) => Some(Self::Dance.fade(DANCE_FADE)),
416            (Self::Dance, i) if i.speed > WALK_THRESHOLD => {
417                Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
418            }
419            (Self::Idle, i) if i.speed > WALK_THRESHOLD => {
420                Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
421            }
422            (Self::Locomotion, i) if i.speed <= WALK_THRESHOLD => {
423                Some(Self::Idle.fade(IDLE_LOCOMOTION_FADE))
424            }
425            _ => None,
426        }
427    }
428}
429
430/// A machine of one state, posed by nothing but the value it scrubs.
431#[derive(Clone, Copy, Eq, PartialEq, Debug)]
432enum ScrubbedState {
433    SitDown,
434}
435
436/// What the game fills each tick to move `ScrubbedState` on.
437#[derive(Default)]
438struct ScrubbedInput {
439    /// How far into sitting down the second elf reads, a fraction in
440    /// `0.0..=1.0`.
441    settled: f32,
442}
443
444impl AnimationStates for ScrubbedState {
445    type Clip = ElfClip;
446    type Input = ScrubbedInput;
447
448    fn entry() -> Self {
449        Self::SitDown
450    }
451
452    fn motion(&self, input: &ScrubbedInput) -> Motion<ElfClip> {
453        Motion::scrubbed(ElfClip::SitDown, input.settled)
454    }
455
456    fn next(&self, _input: &ScrubbedInput, _at: Progress) -> Option<Transition<Self>> {
457        None
458    }
459}
460
461#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
462struct Butterfly;
463
464/// The one clip `FlyingState` plays, named as the source names it.
465#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
466enum ButterflyClip {
467    #[clip("fly")]
468    Fly,
469}
470
471impl Mesh<NoParts, ButterflyClip> for Butterfly {
472    fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
473        assets.model(BUTTERFLY_ROOT)
474    }
475}
476
477/// A machine of one state, looping the butterfly's only clip.
478#[derive(Clone, Copy, Eq, PartialEq, Debug)]
479enum FlyingState {
480    Flying,
481}
482
483impl AnimationStates for FlyingState {
484    type Clip = ButterflyClip;
485    type Input = ();
486
487    fn entry() -> Self {
488        Self::Flying
489    }
490
491    fn motion(&self, _input: &()) -> Motion<ButterflyClip> {
492        Motion::looping(ButterflyClip::Fly)
493    }
494
495    fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
496        None
497    }
498}
499
500/// The butterfly's position and the `yaw` it faces, `t` seconds into its
501/// closed loop around [`BUTTERFLY_CENTER`].
502fn butterfly_pose(t: f32) -> (Vec3, f32) {
503    let angle = t * BUTTERFLY_ANGULAR_SPEED;
504    let position = BUTTERFLY_CENTER
505        + Vec3::new(
506            BUTTERFLY_RADIUS.x * angle.cos(),
507            BUTTERFLY_HEIGHT,
508            BUTTERFLY_RADIUS.y * angle.sin(),
509        );
510    let direction = Vec3::new(
511        -BUTTERFLY_RADIUS.x * angle.sin(),
512        0.0,
513        BUTTERFLY_RADIUS.y * angle.cos(),
514    );
515    (position, direction.x.atan2(direction.z))
516}
517
518/// How far into sitting down the second elf reads at `distance` from the
519/// first.
520fn settled_at(distance: f32) -> f32 {
521    1.0 - (distance - SCRUB_NEAR) / (SCRUB_FAR - SCRUB_NEAR)
522}
523
524/// A camera [`CAMERA_BACK`] behind and [`CAMERA_UP`] above `target`, tilted
525/// `pitch` radians and turned `yaw` radians around it, looking at a point
526/// [`CAMERA_LOOK_HEIGHT`] above `target`.
527fn orbit_camera(target: Vec3, yaw: f32, pitch: f32) -> Camera {
528    let look_at = target + Vec3::Y * CAMERA_LOOK_HEIGHT;
529    let base = Vec3::new(0.0, CAMERA_UP, CAMERA_BACK);
530    let offset = Quat::from_rotation_y(yaw) * (Quat::from_rotation_x(pitch) * base);
531    Camera::new(
532        View::look_at(look_at + offset, look_at),
533        Projection::perspective(CAMERA_FOV),
534    )
535}
536
537/// The logical point egui paints the physical pixel `pixel` at.
538fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
539    let point = pixel / pixels_per_point;
540    egui::pos2(point.x, point.y)
541}
542
543#[derive(InputButtonAction, Clone, Copy)]
544enum Button {
545    Run,
546    Attack,
547    Jump,
548    Dance,
549    Interact,
550    Restart,
551    Hold,
552    Release,
553}
554
555impl InputButtonAction for Button {
556    fn bindings(&self) -> Vec<ButtonBinding> {
557        match self {
558            Button::Run => vec![Key::LeftShift.into()],
559            Button::Attack => vec![Key::F.into()],
560            Button::Jump => vec![Key::Space.into()],
561            Button::Dance => vec![Key::N.into()],
562            Button::Interact => vec![Key::E.into()],
563            Button::Restart => vec![Key::R.into()],
564            Button::Hold => vec![MouseButton::Left.into()],
565            Button::Release => vec![Key::Escape.into()],
566        }
567    }
568}
569
570/// The camera's own controls: turned and tilted by how far the pointer
571/// moves sideways and upward each tick.
572#[derive(InputAxisAction, Clone, Copy)]
573enum Axis {
574    CameraYaw,
575    CameraPitch,
576}
577
578impl InputAxisAction for Axis {
579    fn bindings(&self) -> Vec<AxisBinding> {
580        match self {
581            Axis::CameraYaw => {
582                vec![AxisBinding::pointer_delta(PointerDelta::Sideways).scale(CAMERA_YAW_SCALE)]
583            }
584            Axis::CameraPitch => {
585                vec![AxisBinding::pointer_delta(PointerDelta::Up).scale(CAMERA_PITCH_SCALE)]
586            }
587        }
588    }
589}
590
591#[derive(InputAxis2Action, Clone, Copy)]
592enum Move {
593    Walk,
594}
595
596impl InputAxis2Action for Move {
597    fn bindings(&self) -> Vec<Axis2Binding> {
598        match self {
599            Move::Walk => vec![
600                Axis2Binding::from(ButtonAxis2 {
601                    left: Key::A,
602                    right: Key::D,
603                    down: Key::S,
604                    up: Key::W,
605                }),
606                Axis2Binding::from(ButtonAxis2 {
607                    left: Key::Left,
608                    right: Key::Right,
609                    down: Key::Down,
610                    up: Key::Up,
611                }),
612            ],
613        }
614    }
615}
616
617struct Controls;
618
619impl InputActions for Controls {
620    type Button = Button;
621    type Axis = Axis;
622    type Axis2 = Move;
623}
624
625/// `text` in [`PANEL_TEXT_COLOR`].
626fn panel_text(text: impl Into<String>) -> egui::RichText {
627    egui::RichText::new(text.into()).color(PANEL_TEXT_COLOR)
628}
629
630struct Scene {
631    elf_pos: Vec3,
632    elf_prev: Vec3,
633    elf_yaw: f32,
634    /// Height the elf is lifted over the ground while off the ground,
635    /// integrated in [`Game::tick`] from [`Self::jump_speed`].
636    elf_height: f32,
637    elf_height_prev: f32,
638    /// The elf's own vertical speed while off the ground, in meters a
639    /// second, positive upward.
640    jump_speed: f32,
641    elf_input: ElfInput,
642    elf_animator: Animator<Elf, ElfState>,
643    scrubbed_animator: Animator<Elf, ScrubbedState>,
644    butterfly_animator: Animator<Butterfly, FlyingState>,
645    hits: u32,
646    /// True while a hurt patch already held the elf, so leaving and
647    /// returning to the same patch counts as a new hit.
648    in_patch: bool,
649    /// Whether the pointer is held; a click takes it, escape frees it.
650    holding: bool,
651    /// The camera's turn around the elf, and its tilt, both in radians.
652    camera_yaw: f32,
653    camera_pitch: f32,
654    /// The last state change the panel names.
655    last_event: &'static str,
656}
657
658impl Scene {
659    fn init(_ctx: &mut InitContext<'_, Scene>) -> Result<Self, Error> {
660        Ok(Self {
661            elf_pos: ELF_START,
662            elf_prev: ELF_START,
663            elf_yaw: 0.0,
664            elf_height: 0.0,
665            elf_height_prev: 0.0,
666            jump_speed: 0.0,
667            elf_input: ElfInput::default(),
668            elf_animator: Animator::new(),
669            scrubbed_animator: Animator::new(),
670            butterfly_animator: Animator::new(),
671            hits: 0,
672            in_patch: false,
673            holding: false,
674            camera_yaw: 0.0,
675            camera_pitch: 0.0,
676            last_event: "none yet",
677        })
678    }
679
680    /// Turns the camera by how far the pointer moves sideways and upward,
681    /// [`CAMERA_PITCH_RANGE`] holding how far it tilts.
682    fn steer_camera(&mut self, ctx: &mut FrameContext<'_, Scene>) {
683        self.camera_yaw -= ctx.axis(Axis::CameraYaw);
684        self.camera_pitch = (self.camera_pitch + ctx.axis(Axis::CameraPitch))
685            .clamp(CAMERA_PITCH_RANGE.start, CAMERA_PITCH_RANGE.end);
686    }
687
688    /// Turns `elf_yaw` toward the heading `ctx` reads, relative to the
689    /// camera's own turn, and moves `elf_pos` along it; the speed it moves
690    /// at, a fraction of [`ELF_SPEED`], held at [`WALK_CAP`] until
691    /// `Button::Run` is held.
692    fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693        let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694        let turn = Quat::from_rotation_y(self.camera_yaw);
695        let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696        let dt = ctx.dt().as_secs_f32();
697        if let Some(direction) = heading.try_normalize() {
698            let wanted = direction.x.atan2(direction.z);
699            let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700                - core::f32::consts::PI;
701            self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702        }
703        let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704        self.elf_pos += heading * cap * ELF_SPEED * dt;
705        heading.length() * cap
706    }
707
708    /// Integrates [`Self::elf_height`] under [`GRAVITY`] from
709    /// [`Self::jump_speed`], held at the ground; `true` the tick it
710    /// returns there from above it.
711    fn fall(&mut self, dt: f32) -> bool {
712        let off_ground = self.elf_height > 0.0;
713        self.jump_speed -= GRAVITY * dt;
714        self.elf_height = (self.elf_height + self.jump_speed * dt).max(0.0);
715        if self.elf_height == 0.0 {
716            self.jump_speed = 0.0;
717        }
718        off_ground && self.elf_height == 0.0
719    }
720
721    /// The hurt patch `elf_pos` stands inside, if any.
722    fn patch_underfoot(&self) -> Option<Vec3> {
723        HURT_PATCHES
724            .into_iter()
725            .find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
726    }
727
728    /// Reads the controls and moves the elf, filling [`Self::elf_input`]
729    /// for [`ElfState`] to read.
730    fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731        let grounded = matches!(
732            self.elf_animator.state(),
733            ElfState::Idle | ElfState::Locomotion
734        );
735
736        self.elf_input.speed = self.advance(ctx);
737        self.elf_input.attack = ctx.pressed(Button::Attack);
738        self.elf_input.jump = ctx.pressed(Button::Jump);
739        self.elf_input.dance = ctx.pressed(Button::Dance);
740
741        self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742        self.elf_input.interact = ctx.pressed(Button::Interact);
743        if self.elf_input.interact && grounded && self.elf_input.near_seat {
744            self.elf_pos = SEAT_SPOT;
745            self.elf_yaw = SEAT_FACING;
746        }
747
748        let underfoot = self.patch_underfoot();
749        let entered_patch = underfoot.is_some() && !self.in_patch;
750        self.in_patch = underfoot.is_some();
751        self.hits += u32::from(entered_patch);
752        self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753        self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754        if entered_patch {
755            self.last_event = match self.elf_input.dying {
756                true => "elf died",
757                false => "elf hit",
758            };
759        }
760    }
761
762    /// Starts a new [`Animator`] over the elf's own state, its position and
763    /// hit count reset with it.
764    fn restart_elf(&mut self) {
765        self.elf_animator = Animator::new();
766        self.elf_pos = ELF_START;
767        self.elf_prev = ELF_START;
768        self.elf_yaw = 0.0;
769        self.elf_height = 0.0;
770        self.elf_height_prev = 0.0;
771        self.jump_speed = 0.0;
772        self.elf_input = ElfInput::default();
773        self.hits = 0;
774        self.in_patch = false;
775        self.last_event = "new elf started";
776    }
777
778    fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779        let state = match self.elf_animator.state() {
780            ElfState::Idle => "idle",
781            ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782            ElfState::Locomotion => "walking",
783            ElfState::Attack => "attacking",
784            ElfState::Hit => "hit",
785            ElfState::Death => "dead",
786            ElfState::SitDown => "sitting down",
787            ElfState::Sit => "sitting",
788            ElfState::StandUp => "standing up",
789            ElfState::Jump => "jumping",
790            ElfState::Dance => "dancing",
791        };
792        ctx.ui(|ui| {
793            egui::Frame::new()
794                .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795                .inner_margin(PANEL_PADDING)
796                .corner_radius(f32::from(PANEL_PADDING))
797                .show(ui, |ui| {
798                    ui.heading(panel_text(format!("elf is {state}")));
799                    ui.label(panel_text(format!(
800                        "hits taken {} of the {} red patches hurt for, {}",
801                        self.hits, FATAL_HITS, self.last_event
802                    )));
803                    ui.label(panel_text(match self.elf_animator.transitioning() {
804                        true => "fading between clips",
805                        false => "one clip playing",
806                    }));
807                    ui.add_space(f32::from(PANEL_PADDING));
808                    egui::Grid::new("controls").show(ui, |ui| {
809                        for (key, does) in CONTROLS {
810                            ui.label(panel_text(key));
811                            ui.label(panel_text(does));
812                            ui.end_row();
813                        }
814                    });
815                });
816        });
817    }
818
819    /// A prompt over the seat, each hurt patch, and the scrubbed elf,
820    /// naming what a player finds there; the seat's own prompt names the
821    /// live binding of `Button::Interact` by its own name, not one fixed
822    /// in the code, and is absent while the elf sits on it.
823    fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824        let sit_key = ctx
825            .bindings(Button::Interact)
826            .into_iter()
827            .next()
828            .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829        let sit = ctx.text_layout(
830            &format!("{sit_key} sits"),
831            egui::FontId::proportional(PROMPT_SIZE),
832        );
833        let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834        let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836        let mut prompts = vec![(
837            SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838            walk_closer,
839        )];
840        if !self.elf_animator.state().seated() {
841            prompts.push((
842                SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843                sit,
844            ));
845        }
846        prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848        let window_size = ctx.window_size();
849        let pixels_per_point = ctx.pixels_per_point();
850        ctx.ui(|ui| {
851            let painter = ui.painter();
852            for (point, galley) in prompts {
853                let Some(pixel) = camera.pixel_of(point, window_size) else {
854                    continue;
855                };
856                let at = logical(pixel, pixels_per_point);
857                let ink = galley.mesh_bounds;
858                let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859                let backdrop = egui::Rect::from_center_size(
860                    at,
861                    ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862                );
863                painter.rect_filled(
864                    backdrop,
865                    PROMPT_PADDING,
866                    egui::Color32::from_black_alpha(PANEL_BACKDROP),
867                );
868                painter.galley(pos, galley, PANEL_TEXT_COLOR);
869            }
870        });
871    }
872}
873
874impl Game for Scene {
875    type Meshes = Shape;
876    type Sounds = NoSounds;
877    type InputActions = Controls;
878    type Skyboxes = Sky;
879    type SurfaceStyles = NoSurfaceStyles;
880    type PostEffects = NoPostEffects;
881
882    fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883        self.elf_prev = self.elf_pos;
884        self.elf_height_prev = self.elf_height;
885
886        if ctx.pressed(Button::Restart) {
887            self.restart_elf();
888        }
889        if ctx.pressed(Button::Hold) {
890            self.holding = true;
891        }
892        if ctx.pressed(Button::Release) {
893            self.holding = false;
894        }
895
896        self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897        self.tick_elf(ctx);
898        ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900        if self.elf_animator.entered(ElfState::Jump) {
901            self.jump_speed = JUMP_LAUNCH_SPEED;
902        }
903        if self.elf_animator.left(ElfState::StandUp) {
904            self.last_event = "elf stood up";
905        }
906        if self.elf_animator.entered(ElfState::Sit) {
907            self.last_event = "elf sat down";
908        }
909        if self.elf_animator.entered(ElfState::Death) {
910            self.last_event = "elf died";
911        }
912
913        let scrubbed_input = ScrubbedInput {
914            settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915        };
916        ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917        ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918    }
919
920    fn frame(&mut self, ctx: &mut FrameContext<'_, Scene>) {
921        self.steer_camera(ctx);
922
923        let alpha = ctx.alpha();
924        let elf_pos = self.elf_prev.lerp(self.elf_pos, alpha);
925        let elf_height = self.elf_height_prev + (self.elf_height - self.elf_height_prev) * alpha;
926        let (butterfly_pos, butterfly_yaw) = butterfly_pose(ctx.elapsed().as_secs_f32());
927
928        let camera = orbit_camera(elf_pos, self.camera_yaw, self.camera_pitch);
929        ctx.set_camera(camera);
930        ctx.set_cursor(if self.holding {
931            Cursor::Held
932        } else {
933            Cursor::Arrow
934        });
935        ctx.set_skybox(Sky::Day);
936        ctx.set_exposure(3.0);
937        ctx.set_bloom(0.2);
938        ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
939        ctx.light(
940            Light::point(
941                LAMP_POST_POSITION + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP * 0.5),
942                LAMP_LIGHT_COLOR,
943                LAMP_LIGHT_RANGE,
944            )
945            .shadow(),
946        );
947        ctx.light(
948            Light::spot(Spot {
949                position: SPOT_POSITION,
950                direction: SPOT_DIRECTION,
951                color: SPOT_COLOR,
952                range: SPOT_RANGE,
953                angle: SPOT_ANGLE,
954            })
955            .shadow(),
956        );
957        ctx.light(
958            Light::point(butterfly_pos, BUTTERFLY_LIGHT_COLOR, BUTTERFLY_LIGHT_RANGE).shadow(),
959        );
960
961        ctx.draw(
962            Plane
963                .at(Transform::from_scale(Vec3::new(
964                    GROUND_SIZE,
965                    1.0,
966                    GROUND_SIZE,
967                )))
968                .material(Material::lit(GROUND_COLOR)),
969        );
970        for patch in HURT_PATCHES {
971            ctx.draw(
972                Plane
973                    .at(Transform::from_scale_rotation_translation(
974                        Vec3::splat(HURT_RADIUS * 2.0),
975                        Quat::IDENTITY,
976                        patch,
977                    ))
978                    .material(Material::lit(HURT_COLOR)),
979            );
980        }
981        ctx.draw(
982            Cube.at(Transform::from_scale_rotation_translation(
983                Vec3::new(SEAT_FOOTPRINT, SEAT_HEIGHT, SEAT_FOOTPRINT),
984                Quat::IDENTITY,
985                SEAT_POSITION + Vec3::Y * SEAT_HEIGHT * 0.5,
986            ))
987            .material(Material::lit(SEAT_COLOR)),
988        );
989        ctx.draw(
990            Cube.at(Transform::from_scale_rotation_translation(
991                Vec3::new(LAMP_POST_THICKNESS, LAMP_POST_HEIGHT, LAMP_POST_THICKNESS),
992                Quat::IDENTITY,
993                LAMP_POST_POSITION + Vec3::Y * LAMP_POST_HEIGHT * 0.5,
994            ))
995            .material(Material::lit(LAMP_POST_COLOR)),
996        );
997        ctx.draw(
998            Cube.at(Transform::from_scale_rotation_translation(
999                Vec3::splat(LAMP_HEAD_SIZE),
1000                Quat::IDENTITY,
1001                LAMP_POST_POSITION
1002                    + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP + LAMP_HEAD_SIZE * 0.5),
1003            ))
1004            .material(Material::color(Color::BLACK).emissive(LAMP_LIGHT_COLOR)),
1005        );
1006        ctx.draw(
1007            Cube.at(Transform::from_scale_rotation_translation(
1008                Vec3::splat(SPOT_FIXTURE_SIZE),
1009                Quat::IDENTITY,
1010                SPOT_POSITION + Vec3::Y * SPOT_FIXTURE_SIZE * 0.5,
1011            ))
1012            .material(Material::lit(SPOT_FIXTURE_COLOR)),
1013        );
1014
1015        ctx.draw(
1016            Elf.at(Transform::from_rotation_translation(
1017                Quat::from_rotation_y(self.elf_yaw),
1018                elf_pos + Vec3::Y * elf_height,
1019            ))
1020            .posed(&self.elf_animator),
1021        );
1022        ctx.draw(
1023            Elf.at(Transform::from_rotation_translation(
1024                Quat::from_rotation_y(core::f32::consts::PI),
1025                SCRUBBED_ELF_POSITION,
1026            ))
1027            .posed(&self.scrubbed_animator),
1028        );
1029        ctx.draw(
1030            Butterfly
1031                .at(Transform::from_rotation_translation(
1032                    Quat::from_rotation_y(butterfly_yaw),
1033                    butterfly_pos,
1034                ))
1035                .posed(&self.butterfly_animator)
1036                .material(Material::lit(Color::WHITE).emissive(BUTTERFLY_EMISSIVE)),
1037        );
1038
1039        self.draw_prompts(ctx, camera);
1040        self.panel(ctx);
1041    }
1042}
1043
1044fn main() {
1045    run(
1046        Config::new("Mirage: animation")
1047            .with_size(WINDOW_WIDTH, WINDOW_HEIGHT)
1048            .with_assets([ELF_SOURCE, BUTTERFLY_SOURCE]),
1049        Scene::init,
1050    );
1051}