Skip to main content

pfx_core/anim/
sprite.rs

1use std::sync::Arc;
2
3use super::AnimError;
4use super::animator::{Fired, MAX_REPEATS, Playback};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Atlas {
8    size: [u32; 2],
9    frames: Vec<[u32; 4]>,
10}
11
12impl Atlas {
13    pub fn grid(size: [u32; 2], cell: [u32; 2], count: Option<usize>) -> Result<Self, AnimError> {
14        if size.contains(&0) || cell.contains(&0) || cell[0] > size[0] || cell[1] > size[1] {
15            return Err(AnimError::Atlas(format!(
16                "a grid of {}x{} cells does not fit a {}x{} atlas",
17                cell[0], cell[1], size[0], size[1]
18            )));
19        }
20        let columns = size[0] / cell[0];
21        let rows = size[1] / cell[1];
22        let whole = (columns * rows) as usize;
23        let count = count.unwrap_or(whole);
24        if count == 0 || count > whole {
25            return Err(AnimError::Atlas(format!(
26                "the grid holds {whole} cells, not {count}"
27            )));
28        }
29        let frames = (0..count as u32)
30            .map(|index| {
31                [
32                    index % columns * cell[0],
33                    index / columns * cell[1],
34                    cell[0],
35                    cell[1],
36                ]
37            })
38            .collect();
39        Ok(Self { size, frames })
40    }
41
42    pub fn rects(size: [u32; 2], frames: Vec<[u32; 4]>) -> Result<Self, AnimError> {
43        if size.contains(&0) || frames.is_empty() {
44            return Err(AnimError::Atlas("an atlas needs a size and frames".into()));
45        }
46        for (index, rect) in frames.iter().enumerate() {
47            let inside = rect[2] > 0
48                && rect[3] > 0
49                && rect[0]
50                    .checked_add(rect[2])
51                    .is_some_and(|end| end <= size[0])
52                && rect[1]
53                    .checked_add(rect[3])
54                    .is_some_and(|end| end <= size[1]);
55            if !inside {
56                return Err(AnimError::Atlas(format!(
57                    "frame {index} ({}, {}, {}x{}) is outside the {}x{} atlas",
58                    rect[0], rect[1], rect[2], rect[3], size[0], size[1]
59                )));
60            }
61        }
62        Ok(Self { size, frames })
63    }
64
65    pub fn size(&self) -> [u32; 2] {
66        self.size
67    }
68
69    pub fn frames(&self) -> &[[u32; 4]] {
70        &self.frames
71    }
72
73    pub fn len(&self) -> usize {
74        self.frames.len()
75    }
76
77    pub fn is_empty(&self) -> bool {
78        self.frames.is_empty()
79    }
80
81    pub fn uv(&self, frame: usize) -> Option<[f32; 4]> {
82        let [x, y, w, h] = *self.frames.get(frame)?;
83        let width = self.size[0] as f32;
84        let height = self.size[1] as f32;
85        Some([
86            x as f32 / width,
87            y as f32 / height,
88            (x + w) as f32 / width,
89            (y + h) as f32 / height,
90        ])
91    }
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub struct FrameEvent {
96    pub frame: usize,
97    pub name: String,
98}
99
100impl FrameEvent {
101    pub fn new(frame: usize, name: impl Into<String>) -> Self {
102        Self {
103            frame,
104            name: name.into(),
105        }
106    }
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct SpriteClip {
111    name: String,
112    frames: Vec<usize>,
113    fps: f32,
114    playback: Playback,
115    events: Vec<FrameEvent>,
116}
117
118impl SpriteClip {
119    pub fn new(
120        name: impl Into<String>,
121        frames: Vec<usize>,
122        fps: f32,
123        playback: Playback,
124    ) -> Result<Self, AnimError> {
125        let name = name.into();
126        if frames.is_empty() {
127            return Err(AnimError::Sprite {
128                clip: name,
129                reason: "it has no frames".into(),
130            });
131        }
132        if !fps.is_finite() || fps <= 0.0 {
133            return Err(AnimError::Sprite {
134                clip: name,
135                reason: format!("fps {fps} is not above 0"),
136            });
137        }
138        Ok(Self {
139            name,
140            frames,
141            fps,
142            playback,
143            events: Vec::new(),
144        })
145    }
146
147    pub fn with_events(mut self, mut events: Vec<FrameEvent>) -> Result<Self, AnimError> {
148        if let Some(event) = events.iter().find(|event| event.frame >= self.frames.len()) {
149            return Err(AnimError::Sprite {
150                clip: self.name.clone(),
151                reason: format!(
152                    "event {} is on frame {} of {}",
153                    event.name,
154                    event.frame,
155                    self.frames.len()
156                ),
157            });
158        }
159        events.sort_by(|a, b| a.frame.cmp(&b.frame).then_with(|| a.name.cmp(&b.name)));
160        self.events = events;
161        Ok(self)
162    }
163
164    pub fn name(&self) -> &str {
165        &self.name
166    }
167
168    pub fn frames(&self) -> &[usize] {
169        &self.frames
170    }
171
172    pub fn fps(&self) -> f32 {
173        self.fps
174    }
175
176    pub fn playback(&self) -> Playback {
177        self.playback
178    }
179
180    pub fn events(&self) -> &[FrameEvent] {
181        &self.events
182    }
183
184    pub fn position(&self, step: u64) -> usize {
185        let len = self.frames.len() as u64;
186        match self.playback {
187            Playback::Loop => (step % len) as usize,
188            Playback::Once => step.min(len - 1) as usize,
189            Playback::PingPong if len == 1 => 0,
190            Playback::PingPong => {
191                let at = step % (2 * len - 2);
192                (if at < len { at } else { 2 * len - 2 - at }) as usize
193            }
194        }
195    }
196}
197
198#[derive(Clone, Debug, PartialEq)]
199pub struct SpriteSheet {
200    atlas: Atlas,
201    clips: Vec<SpriteClip>,
202}
203
204impl SpriteSheet {
205    pub fn new(atlas: Atlas, clips: Vec<SpriteClip>) -> Result<Self, AnimError> {
206        for (index, clip) in clips.iter().enumerate() {
207            if clips[..index].iter().any(|other| other.name == clip.name) {
208                return Err(AnimError::DuplicateClip(clip.name.clone()));
209            }
210            if let Some(frame) = clip.frames.iter().find(|frame| **frame >= atlas.len()) {
211                return Err(AnimError::Sprite {
212                    clip: clip.name.clone(),
213                    reason: format!("frame {frame} is past the atlas's {}", atlas.len()),
214                });
215            }
216        }
217        Ok(Self { atlas, clips })
218    }
219
220    pub fn atlas(&self) -> &Atlas {
221        &self.atlas
222    }
223
224    pub fn clips(&self) -> &[SpriteClip] {
225        &self.clips
226    }
227
228    pub fn clip(&self, name: &str) -> Option<usize> {
229        self.clips.iter().position(|clip| clip.name == name)
230    }
231}
232
233#[derive(Clone, Debug, PartialEq)]
234pub struct SpritePlayer {
235    sheet: Arc<SpriteSheet>,
236    rate: u32,
237    clip: Option<usize>,
238    start: u64,
239    offset: f64,
240    speed: f32,
241    heard: Option<u64>,
242}
243
244impl SpritePlayer {
245    pub fn new(sheet: Arc<SpriteSheet>, rate: u32) -> Self {
246        Self {
247            sheet,
248            rate: rate.max(1),
249            clip: None,
250            start: 0,
251            offset: 0.0,
252            speed: 1.0,
253            heard: None,
254        }
255    }
256
257    pub fn sheet(&self) -> &SpriteSheet {
258        &self.sheet
259    }
260
261    pub fn play(&mut self, clip: &str, tick: u64) -> Result<(), AnimError> {
262        self.play_from(clip, 0.0, tick)
263    }
264
265    pub fn play_from(&mut self, clip: &str, seconds: f32, tick: u64) -> Result<(), AnimError> {
266        let index = self
267            .sheet
268            .clip(clip)
269            .ok_or_else(|| AnimError::UnknownClip(clip.to_string()))?;
270        if !seconds.is_finite() || seconds < 0.0 {
271            return Err(AnimError::Start(seconds));
272        }
273        self.clip = Some(index);
274        self.start = tick;
275        self.offset = seconds as f64;
276        self.heard = None;
277        Ok(())
278    }
279
280    pub fn stop(&mut self) {
281        self.clip = None;
282        self.heard = None;
283    }
284
285    pub fn set_speed(&mut self, speed: f32, tick: u64) -> Result<(), AnimError> {
286        if !speed.is_finite() || speed < 0.0 {
287            return Err(AnimError::Speed(speed));
288        }
289        self.offset = self.elapsed(tick);
290        self.start = tick.max(self.start);
291        self.speed = speed;
292        Ok(())
293    }
294
295    pub fn clip(&self) -> Option<&SpriteClip> {
296        self.clip.map(|index| &self.sheet.clips()[index])
297    }
298
299    fn elapsed(&self, tick: u64) -> f64 {
300        let ticks = tick.saturating_sub(self.start) as f64;
301        self.offset + ticks * self.speed as f64 / self.rate as f64
302    }
303
304    pub fn step(&self, tick: u64) -> Option<u64> {
305        let clip = self.clip()?;
306        let fps = clip.fps as f64;
307        let ticks = tick.saturating_sub(self.start) as f64;
308        let steps = self.offset * fps + ticks * self.speed as f64 * fps / self.rate as f64;
309        Some(steps.floor() as u64)
310    }
311
312    pub fn frame(&self, tick: u64) -> Option<usize> {
313        let clip = self.clip()?;
314        let step = self.step(tick)?;
315        Some(clip.frames[clip.position(step)])
316    }
317
318    pub fn uv(&self, tick: u64) -> Option<[f32; 4]> {
319        self.sheet.atlas.uv(self.frame(tick)?)
320    }
321
322    pub fn finished(&self, tick: u64) -> bool {
323        match (self.clip(), self.step(tick)) {
324            (Some(clip), Some(step)) => {
325                clip.playback == Playback::Once && step + 1 >= clip.frames.len() as u64
326            }
327            _ => false,
328        }
329    }
330
331    pub fn advance(&mut self, tick: u64) -> Vec<Fired> {
332        let (Some(clip), Some(now)) = (self.clip(), self.step(tick)) else {
333            return Vec::new();
334        };
335        let first = match self.heard {
336            None => (self.offset * clip.fps as f64).floor() as u64,
337            Some(heard) if heard >= now => return Vec::new(),
338            Some(heard) => heard + 1,
339        };
340        let len = clip.frames.len() as u64;
341        let last = match clip.playback {
342            Playback::Loop | Playback::PingPong => now,
343            Playback::Once => now.min(len - 1),
344        };
345        let first = first.max(last.saturating_sub(MAX_REPEATS * len - 1));
346        let mut fired = Vec::new();
347        if first <= last {
348            for step in first..=last {
349                let position = clip.position(step);
350                for event in clip.events.iter().filter(|event| event.frame == position) {
351                    fired.push(Fired {
352                        clip: clip.name.clone(),
353                        event: event.name.clone(),
354                        time: event.frame as f32 / clip.fps,
355                        tick,
356                    });
357                }
358            }
359        }
360        self.heard = Some(now);
361        fired
362    }
363}