1use std::time::Duration;
18
19use wisp_animation::Driver;
20
21#[derive(Clone, Debug)]
27pub struct EditorPlayer {
28 driver: Driver,
29 fps: u32,
30 duration_frames: u64,
31 in_frame: u64,
32 out_frame: u64,
34 looping: bool,
35}
36
37impl EditorPlayer {
38 #[must_use]
41 pub fn new(fps: u32, duration_frames: u64) -> Self {
42 Self::with_driver(Driver::realtime(), fps, duration_frames)
43 }
44
45 #[must_use]
48 pub fn fixed(fps: u32, duration_frames: u64) -> Self {
49 let fps = fps.max(1);
50 let dt = Duration::from_secs_f64(1.0 / f64::from(fps));
51 Self::with_driver(Driver::fixed(dt), fps, duration_frames)
52 }
53
54 fn with_driver(driver: Driver, fps: u32, duration_frames: u64) -> Self {
55 let fps = fps.max(1);
56 Self {
57 driver,
58 fps,
59 duration_frames,
60 in_frame: 0,
61 out_frame: duration_frames,
62 looping: false,
63 }
64 }
65
66 pub fn play(&mut self) {
71 if !self.looping && self.current_frame() >= self.last_frame() {
72 self.driver.seek(self.frame_to_elapsed(self.in_frame));
73 }
74 self.driver.play();
75 }
76
77 pub fn pause(&mut self) {
79 self.driver.pause();
80 }
81
82 pub fn toggle_play(&mut self) {
84 if self.is_playing() {
85 self.pause();
86 } else {
87 self.play();
88 }
89 }
90
91 #[must_use]
93 pub const fn is_playing(&self) -> bool {
94 self.driver.is_playing()
95 }
96
97 pub fn seek(&mut self, frame: u64) {
99 let clamped = frame.clamp(self.in_frame, self.last_frame());
100 self.driver.seek(self.frame_to_elapsed(clamped));
101 }
102
103 pub fn step(&mut self, delta: i64) {
106 self.driver.pause();
107 self.seek(self.current_frame().saturating_add_signed(delta));
108 }
109
110 pub fn set_rate(&mut self, rate: f32) {
113 self.driver.set_time_scale(rate);
114 }
115
116 #[must_use]
118 pub const fn rate(&self) -> f32 {
119 self.driver.time_scale()
120 }
121
122 pub fn tick(&mut self, dt: Duration) {
127 if !self.driver.is_playing() {
128 return;
129 }
130 self.driver.tick(dt);
131 if self.raw_frame() >= self.out_frame {
132 if self.looping {
133 self.driver.seek(self.frame_to_elapsed(self.in_frame));
134 } else {
135 self.driver.seek(self.frame_to_elapsed(self.last_frame()));
136 self.driver.pause();
137 }
138 }
139 }
140
141 pub fn set_in_out(&mut self, a: u64, b: u64) {
147 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
148 self.in_frame = lo.min(self.duration_frames.saturating_sub(1));
149 self.out_frame = hi
150 .max(self.in_frame + 1)
151 .min(self.duration_frames.max(self.in_frame + 1));
152 let reclamped = self.current_frame();
153 self.driver.seek(self.frame_to_elapsed(reclamped));
154 }
155
156 pub fn clear_in_out(&mut self) {
158 self.in_frame = 0;
159 self.out_frame = self.duration_frames;
160 }
161
162 pub fn set_duration(&mut self, duration_frames: u64) {
167 let was_full = self.out_frame >= self.duration_frames;
168 self.duration_frames = duration_frames;
169 self.in_frame = self.in_frame.min(duration_frames.saturating_sub(1));
170 self.out_frame = if was_full {
171 duration_frames
172 } else {
173 self.out_frame
174 .clamp(self.in_frame + 1, duration_frames.max(self.in_frame + 1))
175 };
176 let clamped = self.current_frame();
177 self.driver.seek(self.frame_to_elapsed(clamped));
178 }
179
180 #[must_use]
182 pub const fn in_frame(&self) -> u64 {
183 self.in_frame
184 }
185
186 #[must_use]
188 pub const fn out_frame(&self) -> u64 {
189 self.out_frame
190 }
191
192 pub const fn set_looping(&mut self, looping: bool) {
194 self.looping = looping;
195 }
196
197 #[must_use]
199 pub const fn looping(&self) -> bool {
200 self.looping
201 }
202
203 #[must_use]
207 pub fn current_frame(&self) -> u64 {
208 self.raw_frame().clamp(self.in_frame, self.last_frame())
209 }
210
211 #[must_use]
213 pub const fn fps(&self) -> u32 {
214 self.fps
215 }
216
217 #[must_use]
219 pub const fn duration_frames(&self) -> u64 {
220 self.duration_frames
221 }
222
223 #[must_use]
226 pub const fn driver(&self) -> &Driver {
227 &self.driver
228 }
229
230 #[must_use]
232 pub fn progress(&self) -> f32 {
233 let span = self.out_frame.saturating_sub(self.in_frame);
234 if span == 0 {
235 return 0.0;
236 }
237 let into = self.current_frame().saturating_sub(self.in_frame);
238 #[allow(
239 clippy::cast_precision_loss,
240 reason = "frame counts are well under 2^24; f32 is plenty for a 0..1 scrubber fraction"
241 )]
242 let p = into as f32 / span as f32;
243 p.clamp(0.0, 1.0)
244 }
245
246 fn last_frame(&self) -> u64 {
250 self.out_frame.saturating_sub(1).max(self.in_frame)
251 }
252
253 fn raw_frame(&self) -> u64 {
255 let raw = self.driver.elapsed().as_secs_f64() * f64::from(self.fps) + 1e-6;
258 if raw <= 0.0 {
259 return 0;
260 }
261 #[allow(
262 clippy::cast_possible_truncation,
263 clippy::cast_sign_loss,
264 reason = "raw is positive and finite (elapsed * fps); frame counts fit u64"
265 )]
266 let frame = raw.floor() as u64;
267 frame
268 }
269
270 fn frame_to_elapsed(&self, frame: u64) -> Duration {
271 #[allow(
272 clippy::cast_precision_loss,
273 reason = "frame counts are well under 2^52; u64→f64 is lossless at these magnitudes"
274 )]
275 let secs = frame as f64 / f64::from(self.fps);
276 Duration::from_secs_f64(secs.max(0.0))
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 const FPS: u32 = 30;
285
286 #[test]
287 fn starts_paused_at_zero() {
288 let p = EditorPlayer::new(FPS, 1000);
289 assert!(!p.is_playing());
290 assert_eq!(p.current_frame(), 0);
291 assert!((p.rate() - 1.0).abs() < 1e-6);
292 }
293
294 #[test]
295 fn play_and_tick_advances_by_dt_times_fps() {
296 let mut p = EditorPlayer::new(FPS, 1000);
297 p.play();
298 p.tick(Duration::from_secs(1)); assert_eq!(p.current_frame(), 30);
300 }
301
302 #[test]
303 fn rate_scales_advance() {
304 let mut p = EditorPlayer::new(FPS, 1000);
305 p.set_rate(2.0);
306 p.play();
307 p.tick(Duration::from_secs(1)); assert_eq!(p.current_frame(), 60);
309
310 let mut half = EditorPlayer::new(FPS, 1000);
311 half.set_rate(0.5);
312 half.play();
313 half.tick(Duration::from_secs(1)); assert_eq!(half.current_frame(), 15);
315 }
316
317 #[test]
318 fn pause_holds_the_playhead() {
319 let mut p = EditorPlayer::new(FPS, 1000);
320 p.play();
321 p.tick(Duration::from_secs(1));
322 p.pause();
323 p.tick(Duration::from_secs(5));
324 assert_eq!(p.current_frame(), 30);
325 }
326
327 #[test]
328 fn seek_is_exact() {
329 let mut p = EditorPlayer::new(FPS, 1000);
330 for target in [0, 1, 7, 99, 100, 333, 999] {
331 p.seek(target);
332 assert_eq!(p.current_frame(), target, "seek({target})");
333 }
334 }
335
336 #[test]
337 fn step_moves_one_frame_and_pauses() {
338 let mut p = EditorPlayer::new(FPS, 1000);
339 p.play();
340 p.seek(100);
341 p.step(1);
342 assert_eq!(p.current_frame(), 101);
343 assert!(!p.is_playing(), "stepping pauses");
344 p.step(-1);
345 assert_eq!(p.current_frame(), 100);
346 p.step(-1000); assert_eq!(p.current_frame(), 0);
348 }
349
350 #[test]
351 fn in_out_clamps_the_playhead() {
352 let mut p = EditorPlayer::new(FPS, 1000);
353 p.set_in_out(10, 20); assert_eq!(p.in_frame(), 10);
355 assert_eq!(p.out_frame(), 20);
356 p.seek(5);
357 assert_eq!(p.current_frame(), 10, "below in clamps to in");
358 p.seek(50);
359 assert_eq!(p.current_frame(), 19, "above out clamps to last");
360 p.set_in_out(40, 30);
362 assert_eq!(p.in_frame(), 30);
363 assert_eq!(p.out_frame(), 40);
364 }
365
366 #[test]
367 fn not_looping_clamps_and_pauses_at_end() {
368 let mut p = EditorPlayer::new(FPS, 10); p.play();
370 p.tick(Duration::from_secs(1)); assert_eq!(p.current_frame(), 9, "clamped to last frame");
372 assert!(!p.is_playing(), "paused at end");
373 }
374
375 #[test]
376 fn looping_wraps_to_in_point() {
377 let mut p = EditorPlayer::new(FPS, 1000);
378 p.set_in_out(0, 10);
379 p.set_looping(true);
380 p.play();
381 p.tick(Duration::from_secs(1)); assert!(p.is_playing(), "loop keeps playing");
383 assert!(
384 p.current_frame() < 10,
385 "wrapped back into [0,10), got {}",
386 p.current_frame()
387 );
388 }
389
390 #[test]
391 fn play_from_end_restarts() {
392 let mut p = EditorPlayer::new(FPS, 100);
393 p.seek(99); p.play();
395 assert_eq!(p.current_frame(), 0);
397 assert!(p.is_playing());
398 }
399
400 #[test]
401 fn fixed_driver_steps_one_frame_per_tick() {
402 let mut p = EditorPlayer::fixed(FPS, 100);
403 p.play();
404 for expected in 1..=5 {
405 p.tick(Duration::ZERO); assert_eq!(p.current_frame(), expected);
407 }
408 }
409
410 #[test]
411 fn progress_spans_in_out() {
412 let mut p = EditorPlayer::new(FPS, 100);
413 p.set_in_out(0, 100);
414 p.seek(0);
415 assert!(p.progress() < 1e-6);
416 p.seek(50);
417 assert!((p.progress() - 0.5).abs() < 0.02, "got {}", p.progress());
418 p.seek(99);
419 assert!(p.progress() > 0.95);
420 }
421
422 #[test]
423 fn set_duration_tracks_full_range_and_clamps_playhead() {
424 let mut p = EditorPlayer::new(FPS, 900);
425 p.seek(800);
426 p.set_duration(500);
428 assert_eq!(p.duration_frames(), 500);
429 assert_eq!(p.out_frame(), 500);
430 assert_eq!(p.current_frame(), 499);
431 p.set_duration(900);
433 assert_eq!(p.out_frame(), 900);
434 p.set_in_out(10, 400);
436 p.set_duration(200);
437 assert!(p.out_frame() <= 200 && p.out_frame() > p.in_frame());
438 }
439}