Skip to main content

repame_sprite/
sprite_image.rs

1use std::fmt;
2use std::sync::Arc;
3
4use repose_core::locals::effective_density_scale;
5use repose_core::{
6    Color, ControlVisual, ControlVisualState, Dp, ImageAlignment, ImageFilter, ImageFit,
7    ImageHandle, ImageHandleGuard, ImagePaintStyle, ImageSourceRect, Modifier, Rect, RenderContext,
8    Scene, SceneNode, View,
9};
10use repose_ui::Box as UiBox;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct SpriteImageFrameDef {
14    pub source_rect: ImageSourceRect,
15    pub origin: [f32; 2],
16    pub duration: f32,
17}
18
19#[derive(Clone, Debug, Default, PartialEq)]
20pub struct SpriteImageDef {
21    pub frames: Vec<SpriteImageFrameDef>,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum SpriteImageError {
26    EmptyDefinition,
27    InvalidTextureSize,
28    InvalidFrame(usize),
29    InvalidTiming(usize),
30    InvalidPixelBuffer,
31}
32
33impl fmt::Display for SpriteImageError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::EmptyDefinition => f.write_str("sprite image definition has no frames"),
37            Self::InvalidTextureSize => {
38                f.write_str("sprite image texture dimensions must be nonzero")
39            }
40            Self::InvalidFrame(index) => write!(f, "sprite image frame {index} is invalid"),
41            Self::InvalidTiming(index) => {
42                write!(f, "sprite image frame {index} has invalid timing")
43            }
44            Self::InvalidPixelBuffer => f.write_str("sprite image RGBA buffer has the wrong size"),
45        }
46    }
47}
48
49impl std::error::Error for SpriteImageError {}
50
51impl SpriteImageDef {
52    pub fn new(frames: Vec<SpriteImageFrameDef>) -> Self {
53        Self { frames }
54    }
55
56    pub fn single(frame_size: [u32; 2], origin: [f32; 2]) -> Self {
57        Self {
58            frames: vec![SpriteImageFrameDef {
59                source_rect: ImageSourceRect::new(0, 0, frame_size[0], frame_size[1]),
60                origin,
61                duration: 0.0,
62            }],
63        }
64    }
65
66    pub fn horizontal_strip(
67        frames: u32,
68        frame_size: [u32; 2],
69        origin: [f32; 2],
70        fps: f32,
71    ) -> Result<Self, SpriteImageError> {
72        let duration = if fps > 0.0 { 1.0 / fps } else { 0.0 };
73        if !fps.is_finite() || !duration.is_finite() {
74            return Err(SpriteImageError::InvalidTiming(0));
75        }
76        let frames = (0..frames)
77            .map(|frame| {
78                let x = frame.saturating_mul(frame_size[0]);
79                SpriteImageFrameDef {
80                    source_rect: ImageSourceRect::new(x, 0, frame_size[0], frame_size[1]),
81                    origin,
82                    duration,
83                }
84            })
85            .collect();
86        Ok(Self { frames })
87    }
88}
89
90struct SpriteImageAssetInner {
91    guard: ImageHandleGuard,
92    texture_size: [u32; 2],
93    frames: Arc<[SpriteImageFrameDef]>,
94}
95
96#[derive(Clone)]
97pub struct SpriteImageAsset {
98    inner: Arc<SpriteImageAssetInner>,
99}
100
101impl fmt::Debug for SpriteImageAsset {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.debug_struct("SpriteImageAsset")
104            .field("handle", &self.handle())
105            .field("texture_size", &self.texture_size())
106            .field("frame_count", &self.frame_count())
107            .finish()
108    }
109}
110
111impl SpriteImageAsset {
112    pub fn from_encoded(
113        context: &RenderContext,
114        bytes: impl Into<Vec<u8>>,
115        texture_size: [u32; 2],
116        definition: SpriteImageDef,
117        srgb: bool,
118    ) -> Result<Self, SpriteImageError> {
119        Self::validate(texture_size, &definition)?;
120        let guard = ImageHandleGuard::new(context);
121        context.set_image_encoded(*guard, bytes.into(), srgb);
122        Ok(Self::from_parts(guard, texture_size, definition))
123    }
124
125    pub fn from_rgba8(
126        context: &RenderContext,
127        texture_size: [u32; 2],
128        rgba: Vec<u8>,
129        definition: SpriteImageDef,
130        srgb: bool,
131    ) -> Result<Self, SpriteImageError> {
132        Self::validate(texture_size, &definition)?;
133        let expected = usize::try_from(texture_size[0])
134            .ok()
135            .and_then(|width| {
136                usize::try_from(texture_size[1])
137                    .ok()
138                    .and_then(|height| width.checked_mul(height))
139            })
140            .and_then(|pixels| pixels.checked_mul(4))
141            .ok_or(SpriteImageError::InvalidPixelBuffer)?;
142        if rgba.len() != expected {
143            return Err(SpriteImageError::InvalidPixelBuffer);
144        }
145        let guard = ImageHandleGuard::new(context);
146        context.set_image_rgba8(*guard, texture_size[0], texture_size[1], rgba, srgb);
147        Ok(Self::from_parts(guard, texture_size, definition))
148    }
149
150    fn from_parts(
151        guard: ImageHandleGuard,
152        texture_size: [u32; 2],
153        definition: SpriteImageDef,
154    ) -> Self {
155        Self {
156            inner: Arc::new(SpriteImageAssetInner {
157                guard,
158                texture_size,
159                frames: definition.frames.into(),
160            }),
161        }
162    }
163
164    fn validate(
165        texture_size: [u32; 2],
166        definition: &SpriteImageDef,
167    ) -> Result<(), SpriteImageError> {
168        if texture_size[0] == 0 || texture_size[1] == 0 {
169            return Err(SpriteImageError::InvalidTextureSize);
170        }
171        if definition.frames.is_empty() {
172            return Err(SpriteImageError::EmptyDefinition);
173        }
174        for (index, frame) in definition.frames.iter().enumerate() {
175            let Some(right) = frame.source_rect.x.checked_add(frame.source_rect.width) else {
176                return Err(SpriteImageError::InvalidFrame(index));
177            };
178            let Some(bottom) = frame.source_rect.y.checked_add(frame.source_rect.height) else {
179                return Err(SpriteImageError::InvalidFrame(index));
180            };
181            if frame.source_rect.is_empty()
182                || right > texture_size[0]
183                || bottom > texture_size[1]
184                || !frame.origin[0].is_finite()
185                || !frame.origin[1].is_finite()
186            {
187                return Err(SpriteImageError::InvalidFrame(index));
188            }
189            if !frame.duration.is_finite() || frame.duration < 0.0 {
190                return Err(SpriteImageError::InvalidTiming(index));
191            }
192        }
193        Ok(())
194    }
195
196    pub fn handle(&self) -> ImageHandle {
197        *self.inner.guard
198    }
199
200    pub fn texture_size(&self) -> [u32; 2] {
201        self.inner.texture_size
202    }
203
204    pub fn frame_count(&self) -> usize {
205        self.inner.frames.len()
206    }
207
208    pub fn frame(&self, frame: i32) -> Option<SpriteImage> {
209        if self.inner.frames.is_empty() {
210            return None;
211        }
212        let index = frame.rem_euclid(self.inner.frames.len() as i32) as usize;
213        let def = self.inner.frames[index];
214        Some(SpriteImage {
215            asset: self.clone(),
216            source_rect: def.source_rect,
217            size: [def.source_rect.width as f32, def.source_rect.height as f32],
218            origin: def.origin,
219            duration: def.duration,
220        })
221    }
222}
223
224#[derive(Clone)]
225pub struct SpriteImage {
226    asset: SpriteImageAsset,
227    source_rect: ImageSourceRect,
228    size: [f32; 2],
229    origin: [f32; 2],
230    duration: f32,
231}
232
233impl fmt::Debug for SpriteImage {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.debug_struct("SpriteImage")
236            .field("handle", &self.handle())
237            .field("source_rect", &self.source_rect)
238            .field("size", &self.size)
239            .field("origin", &self.origin)
240            .field("duration", &self.duration)
241            .finish()
242    }
243}
244
245impl SpriteImage {
246    pub fn handle(&self) -> ImageHandle {
247        self.asset.handle()
248    }
249
250    pub fn source_rect(&self) -> ImageSourceRect {
251        self.source_rect
252    }
253
254    pub fn size(&self) -> [f32; 2] {
255        self.size
256    }
257
258    pub fn origin(&self) -> [f32; 2] {
259        self.origin
260    }
261
262    pub fn anchor(&self) -> [f32; 2] {
263        if self.size[0] <= 0.0 || self.size[1] <= 0.0 {
264            [0.0, 0.0]
265        } else {
266            [self.origin[0] / self.size[0], self.origin[1] / self.size[1]]
267        }
268    }
269
270    pub fn duration(&self) -> f32 {
271        self.duration
272    }
273
274    pub fn view(&self, modifier: Modifier) -> View {
275        let has_size = modifier.size.is_some()
276            || modifier.width.is_some()
277            || modifier.height.is_some()
278            || modifier.fill_max.is_some()
279            || modifier.fill_max_w.is_some()
280            || modifier.fill_max_h.is_some();
281        let modifier = if has_size {
282            modifier
283        } else {
284            modifier.size(Dp(self.size[0]), Dp(self.size[1]))
285        };
286        let visual = self.control_visual(Color::WHITE);
287        UiBox(modifier.painter(move |scene, rect, alpha| {
288            visual.paint(
289                scene,
290                rect,
291                ControlVisualState {
292                    alpha,
293                    ..ControlVisualState::default()
294                },
295            );
296        }))
297    }
298
299    pub fn control_visual(&self, tint: Color) -> ControlVisual {
300        let _asset = self.asset.clone();
301        let handle = self.handle();
302        let source = self.source_rect;
303        ControlVisual::custom(move |scene, rect, state| {
304            let _ = &_asset;
305            let alpha = state.alpha.clamp(0.0, 1.0);
306            let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
307            scene.nodes.push(SceneNode::Image {
308                rect,
309                handle,
310                tint,
311                style: ImagePaintStyle {
312                    fit: ImageFit::FillBounds,
313                    filter: ImageFilter::Nearest,
314                    source_rect: Some(source),
315                    alignment: ImageAlignment::Center,
316                },
317            });
318        })
319    }
320
321    pub fn rotated_control_visual(&self, tint: Color, quarter_turns: i32) -> ControlVisual {
322        let quarter_turns = quarter_turns.rem_euclid(4);
323        if quarter_turns == 0 {
324            return self.control_visual(tint);
325        }
326        let _asset = self.asset.clone();
327        let handle = self.handle();
328        let source = self.source_rect;
329        ControlVisual::custom(
330            move |scene: &mut Scene, rect: Rect, state: ControlVisualState| {
331                let _ = &_asset;
332                let angle = quarter_turns as f32 * std::f32::consts::FRAC_PI_2;
333                let (sin, cos) = angle.sin_cos();
334                let center_x = rect.x + rect.w * 0.5;
335                let center_y = rect.y + rect.h * 0.5;
336                let transform = repose_core::Transform {
337                    translate_x: center_x - (cos * center_x - sin * center_y),
338                    translate_y: center_y - (sin * center_x + cos * center_y),
339                    rotate: angle,
340                    origin_x: 0.0,
341                    origin_y: 0.0,
342                    ..repose_core::Transform::identity()
343                };
344                let image_rect = if quarter_turns % 2 == 1 {
345                    Rect {
346                        x: center_x - rect.h * 0.5,
347                        y: center_y - rect.w * 0.5,
348                        w: rect.h,
349                        h: rect.w,
350                    }
351                } else {
352                    rect
353                };
354                let alpha = state.alpha.clamp(0.0, 1.0);
355                let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
356                scene.nodes.push(SceneNode::PushTransform { transform });
357                scene.nodes.push(SceneNode::Image {
358                    rect: image_rect,
359                    handle,
360                    tint,
361                    style: ImagePaintStyle {
362                        fit: ImageFit::FillBounds,
363                        filter: ImageFilter::Nearest,
364                        source_rect: Some(source),
365                        alignment: ImageAlignment::Center,
366                    },
367                });
368                scene.nodes.push(SceneNode::PopTransform);
369            },
370        )
371    }
372
373    pub fn prefix_control_visual(&self, tint: Color) -> ControlVisual {
374        let _asset = self.asset.clone();
375        let handle = self.handle();
376        let source = self.source_rect;
377        let natural_width = self.size[0].max(1.0);
378        ControlVisual::custom(
379            move |scene: &mut Scene, rect: Rect, state: ControlVisualState| {
380                let _ = &_asset;
381                let density = effective_density_scale().max(1e-6);
382                let logical_width = rect.w / density;
383                let visible = if logical_width.is_finite() && logical_width > 0.0 {
384                    ((logical_width / natural_width).clamp(0.0, 1.0) * source.width as f32)
385                        .floor()
386                        .max(1.0) as u32
387                } else {
388                    source.width
389                };
390                let visible = visible.min(source.width);
391                let alpha = state.alpha.clamp(0.0, 1.0);
392                let tint = Color(tint.0, tint.1, tint.2, (tint.3 as f32 * alpha) as u8);
393                scene.nodes.push(SceneNode::Image {
394                    rect,
395                    handle,
396                    tint,
397                    style: ImagePaintStyle {
398                        fit: ImageFit::FillBounds,
399                        filter: ImageFilter::Nearest,
400                        source_rect: Some(ImageSourceRect::new(
401                            source.x,
402                            source.y,
403                            visible,
404                            source.height,
405                        )),
406                        alignment: ImageAlignment::Center,
407                    },
408                });
409            },
410        )
411    }
412
413    pub fn subimage(&self, source_rect: ImageSourceRect) -> Option<Self> {
414        let right = source_rect.x.checked_add(source_rect.width)?;
415        let bottom = source_rect.y.checked_add(source_rect.height)?;
416        let [width, height] = self.asset.texture_size();
417        if source_rect.is_empty() || right > width || bottom > height {
418            return None;
419        }
420        Some(Self {
421            asset: self.asset.clone(),
422            source_rect,
423            size: [source_rect.width as f32, source_rect.height as f32],
424            origin: self.origin,
425            duration: self.duration,
426        })
427    }
428
429    pub fn relative_subimage(&self, x: u32, y: u32, width: u32, height: u32) -> Option<Self> {
430        let source_x = self.source_rect.x.checked_add(x)?;
431        let source_y = self.source_rect.y.checked_add(y)?;
432        self.subimage(ImageSourceRect::new(source_x, source_y, width, height))
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use repose_core::{ControlVisualState, RenderCommand};
440
441    #[test]
442    fn sprite_frame_becomes_source_rect_control_visual() {
443        let context = RenderContext::new();
444        let definition = SpriteImageDef::horizontal_strip(2, [2, 2], [1.0, 1.0], 8.0)
445            .expect("strip should be valid");
446        let (handle, visual) = {
447            let asset = SpriteImageAsset::from_rgba8(
448                &context,
449                [4, 2],
450                vec![255; 4 * 2 * 4],
451                definition,
452                true,
453            )
454            .expect("asset should be valid");
455            let frame = asset.frame(1).expect("frame should exist");
456            (frame.handle(), frame.control_visual(Color::WHITE))
457        };
458        let _ = context.drain();
459        let mut scene = Scene::default();
460        visual.paint(
461            &mut scene,
462            Rect {
463                x: 3.0,
464                y: 5.0,
465                w: 20.0,
466                h: 10.0,
467            },
468            ControlVisualState::default(),
469        );
470        let Some(SceneNode::Image {
471            rect,
472            style:
473                ImagePaintStyle {
474                    source_rect,
475                    filter,
476                    ..
477                },
478            ..
479        }) = scene.nodes.first()
480        else {
481            panic!("expected image node");
482        };
483        assert_eq!(rect.w, 20.0);
484        assert_eq!(*source_rect, Some(ImageSourceRect::new(2, 0, 2, 2)));
485        assert_eq!(*filter, ImageFilter::Nearest);
486        assert!(context.drain().is_empty());
487        drop(visual);
488        assert!(matches!(
489            context.drain().as_slice(),
490            [RenderCommand::RemoveImage { handle: removed }] if *removed == handle
491        ));
492    }
493
494    #[test]
495    fn rotated_visual_swaps_quarter_turn_rect() {
496        let context = RenderContext::new();
497        let definition = SpriteImageDef::single([8, 2], [4.0, 1.0]);
498        let visual =
499            SpriteImageAsset::from_rgba8(&context, [8, 2], vec![255; 8 * 2 * 4], definition, true)
500                .expect("asset should be valid")
501                .frame(0)
502                .expect("frame should exist")
503                .rotated_control_visual(Color::WHITE, 1);
504        let mut scene = Scene::default();
505        visual.paint(
506            &mut scene,
507            Rect {
508                x: 10.0,
509                y: 20.0,
510                w: 20.0,
511                h: 80.0,
512            },
513            ControlVisualState::default(),
514        );
515        assert_eq!(scene.nodes.len(), 3);
516        assert!(matches!(
517            scene.nodes[1],
518            SceneNode::Image { rect, .. } if rect == Rect { x: -20.0, y: 50.0, w: 80.0, h: 20.0 }
519        ));
520        assert!(matches!(&scene.nodes[2], SceneNode::PopTransform));
521    }
522
523    #[test]
524    fn prefix_visual_uses_logical_control_width() {
525        let context = RenderContext::new();
526        let definition = SpriteImageDef::horizontal_strip(1, [2, 2], [1.0, 1.0], 8.0)
527            .expect("strip should be valid");
528        let visual =
529            SpriteImageAsset::from_rgba8(&context, [2, 2], vec![255; 2 * 2 * 4], definition, true)
530                .expect("asset should be valid")
531                .frame(0)
532                .expect("frame should exist")
533                .prefix_control_visual(Color::WHITE);
534        let mut scene = Scene::default();
535        repose_core::locals::with_density(repose_core::locals::Density { scale: 2.0 }, || {
536            visual.paint(
537                &mut scene,
538                Rect {
539                    x: 0.0,
540                    y: 0.0,
541                    w: 2.0,
542                    h: 2.0,
543                },
544                ControlVisualState::default(),
545            );
546        });
547        let Some(SceneNode::Image {
548            style: ImagePaintStyle { source_rect, .. },
549            ..
550        }) = scene.nodes.first()
551        else {
552            panic!("expected image node");
553        };
554        assert_eq!(*source_rect, Some(ImageSourceRect::new(0, 0, 1, 2)));
555    }
556}