psx_fx/shake.rs
1//! Screen-shake counter.
2//!
3//! The cheapest possible "the world reacted" feedback. A single
4//! frame counter; `tick()` decrements it and returns a small
5//! `(dx, dy)` offset the caller adds to their vertex positions.
6//!
7//! Triangle-wave pattern -- sign flips each frame so it reads as a
8//! judder rather than a smooth drift. Magnitude decays linearly
9//! with the counter, so the last frames are subtle.
10//!
11//! # Example
12//!
13//! ```ignore
14//! let mut shake = ShakeState::new();
15//! // On impact:
16//! shake.trigger(6);
17//! // Every frame, folded into rendering:
18//! let (dx, dy) = shake.tick();
19//! draw_game_world_at_offset(dx, dy);
20//! ```
21
22/// Frame-countdown that emits a judder offset.
23#[derive(Copy, Clone, Debug, Default)]
24pub struct ShakeState {
25 /// Frames of shake remaining. `0` = idle.
26 pub frames: u8,
27}
28
29impl ShakeState {
30 /// Idle instance.
31 pub const fn new() -> Self {
32 Self { frames: 0 }
33 }
34
35 /// Start (or retrigger) a shake. If a bigger shake is already
36 /// active, it stays -- smaller triggers don't interrupt.
37 pub fn trigger(&mut self, frames: u8) {
38 self.frames = self.frames.max(frames);
39 }
40
41 /// Advance one frame. Returns `(dx, dy)` in pixels. Safe to
42 /// call every frame even when idle -- returns `(0, 0)` then.
43 pub fn tick(&mut self) -> (i16, i16) {
44 if self.frames == 0 {
45 return (0, 0);
46 }
47 let s = self.frames as i16;
48 // Sign flips with parity so the shake rattles instead of
49 // drifting. Magnitudes decrease linearly with `frames`.
50 let sign = if s & 1 == 0 { 1 } else { -1 };
51 let offset = ((s / 2) * sign, ((s + 1) / 2) * -sign);
52 self.frames -= 1;
53 offset
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn idle_is_zero() {
63 let mut s = ShakeState::new();
64 assert_eq!(s.tick(), (0, 0));
65 assert_eq!(s.tick(), (0, 0));
66 }
67
68 #[test]
69 fn trigger_then_decay_to_zero() {
70 let mut s = ShakeState::new();
71 s.trigger(4);
72 for _ in 0..4 {
73 let _ = s.tick();
74 }
75 assert_eq!(s.tick(), (0, 0));
76 assert_eq!(s.frames, 0);
77 }
78
79 #[test]
80 fn trigger_doesnt_shrink() {
81 let mut s = ShakeState::new();
82 s.trigger(10);
83 s.trigger(3); // smaller trigger shouldn't reset
84 assert_eq!(s.frames, 10);
85 }
86}