1use std::time::{Duration, Instant};
2
3pub const MAX_FRAME_STEP_SECS: f32 = 0.25;
10
11#[derive(Clone, Copy, Debug)]
17pub struct FrameClock {
18 origin: Instant,
19 last_wall: Instant,
20 elapsed: Duration,
21 dt: Duration,
22 frame: u64,
23 speed: f32,
24 reduced_motion: bool,
25 manual: bool,
26}
27
28impl Default for FrameClock {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl FrameClock {
35 pub fn new() -> Self {
36 let now = Instant::now();
37 Self {
38 origin: now,
39 last_wall: now,
40 elapsed: Duration::ZERO,
41 dt: Duration::ZERO,
42 frame: 0,
43 speed: 1.0,
44 reduced_motion: false,
45 manual: false,
46 }
47 }
48
49 pub fn now(&self) -> Instant {
51 self.origin + self.elapsed
52 }
53
54 pub fn elapsed(&self) -> Duration {
55 self.elapsed
56 }
57
58 pub fn elapsed_secs(&self) -> f32 {
59 self.elapsed.as_secs_f32()
60 }
61
62 pub fn dt(&self) -> Duration {
63 self.dt
64 }
65
66 pub fn dt_secs(&self) -> f32 {
67 self.dt.as_secs_f32()
68 }
69
70 pub fn frame(&self) -> u64 {
71 self.frame
72 }
73
74 pub fn speed(&self) -> f32 {
75 self.speed
76 }
77
78 pub fn set_speed(&mut self, speed: f32) {
79 self.speed = if speed.is_finite() {
80 speed.max(0.0)
81 } else {
82 1.0
83 };
84 }
85
86 pub fn reduced_motion(&self) -> bool {
87 self.reduced_motion
88 }
89
90 pub fn set_reduced_motion(&mut self, reduced: bool) {
91 self.reduced_motion = reduced;
92 }
93
94 pub fn is_manual(&self) -> bool {
95 self.manual
96 }
97
98 pub fn set_manual(&mut self, manual: bool) {
99 self.manual = manual;
100 self.last_wall = Instant::now();
101 self.dt = Duration::ZERO;
102 }
103
104 pub fn advance(&mut self) {
106 if self.manual {
107 self.dt = Duration::ZERO;
108 return;
109 }
110 let now = Instant::now();
111 let wall = now.saturating_duration_since(self.last_wall);
112 self.last_wall = now;
113 let scaled = if self.reduced_motion {
114 Duration::ZERO
115 } else {
116 wall.mul_f32(self.speed)
117 };
118 let capped = scaled.min(Duration::from_secs_f32(MAX_FRAME_STEP_SECS));
119 self.elapsed += capped;
120 self.dt = capped;
121 self.frame = self.frame.wrapping_add(1);
122 }
123
124 pub fn advance_by(&mut self, dt: Duration) {
126 let scaled = if self.reduced_motion {
127 Duration::ZERO
128 } else {
129 dt.mul_f32(self.speed)
130 };
131 self.elapsed += scaled;
132 self.dt = scaled;
133 self.frame = self.frame.wrapping_add(1);
134 }
135
136 pub fn reset(&mut self) {
137 let now = Instant::now();
138 self.origin = now;
139 self.last_wall = now;
140 self.elapsed = Duration::ZERO;
141 self.dt = Duration::ZERO;
142 self.frame = 0;
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn manual_clock_advances_exactly() {
152 let mut c = FrameClock::new();
153 c.set_manual(true);
154 c.advance_by(Duration::from_millis(16));
155 c.advance_by(Duration::from_millis(16));
156 assert_eq!(c.frame(), 2);
157 assert_eq!(c.elapsed(), Duration::from_millis(32));
158 assert_eq!(c.dt(), Duration::from_millis(16));
159 }
160
161 #[test]
162 fn reduced_motion_stops_time() {
163 let mut c = FrameClock::new();
164 c.set_manual(true);
165 c.set_reduced_motion(true);
166 c.advance_by(Duration::from_millis(100));
167 assert_eq!(c.elapsed(), Duration::ZERO);
168 assert_eq!(c.frame(), 1);
169 }
170
171 #[test]
172 fn speed_scales_manual_steps() {
173 let mut c = FrameClock::new();
174 c.set_manual(true);
175 c.set_speed(2.0);
176 c.advance_by(Duration::from_millis(50));
177 assert_eq!(c.elapsed(), Duration::from_millis(100));
178 }
179
180 #[test]
181 fn automatic_advance_is_disabled_in_manual_mode() {
182 let mut c = FrameClock::new();
183 c.set_manual(true);
184 c.advance();
185 assert_eq!(c.dt(), Duration::ZERO);
186 assert_eq!(c.frame(), 0);
187 }
188}