1
use figures::{Displayable, Rectlike, Scaled};
2

            
3
use crate::math::{ExtentsRect, Pixels, Point, Rect, Size};
4
use crate::scene::{Element, Target};
5
use crate::sprite::{RenderedSprite, SpriteRotation};
6
use crate::texture::Texture;
7

            
8
/// A sprite's source location and texture. Cheap to clone.
9
#[derive(Debug, Clone)]
10
pub struct SpriteSource {
11
    /// The location of the sprite
12
    pub location: SpriteSourceLocation,
13
    /// The texture.
14
    pub texture: Texture,
15
}
16

            
17
/// A sprite location.
18
#[derive(Debug, Clone)]
19
pub enum SpriteSourceLocation {
20
    /// A single rectangle.
21
    Rect(Rect<u32>),
22
    /// A joined series of images. Useful for constructing a 32x32 sprite from
23
    /// four 16x16 sprites.
24
    Joined(Vec<SpriteSourceSublocation>),
25
}
26

            
27
impl SpriteSourceLocation {
28
    /// Returns the bounding box of the source rect.
29
    #[must_use]
30
    pub fn bounds(&self) -> Rect<u32> {
31
        match self {
32
            Self::Rect(rect) => *rect,
33
            Self::Joined(locations) => locations
34
                .iter()
35
                .fold(Option::<Rect<u32>>::None, |union, location| {
36
                    union.map_or_else(
37
                        || Some(location.destination_rect()),
38
                        |total| {
39
                            total
40
                                .union(&location.destination_rect())
41
                                .map(|r| r.as_sized())
42
                        },
43
                    )
44
                })
45
                .unwrap_or_default(),
46
        }
47
    }
48

            
49
    /// Returns the size of the bounds.
50
    #[must_use]
51
    pub fn size(&self) -> Size<u32> {
52
        self.bounds().size
53
    }
54
}
55

            
56
/// A sub-location of a joined sprite.
57
#[derive(Debug, Clone)]
58
pub struct SpriteSourceSublocation {
59
    /// The source rectangle.
60
    pub source: Rect<u32>,
61
    /// The relative destination when rendering.
62
    pub destination: Point<u32>,
63
}
64

            
65
impl SpriteSourceSublocation {
66
    /// Returns the destination with the source's size.
67
    #[must_use]
68
    pub const fn destination_rect(&self) -> Rect<u32> {
69
        Rect::new(self.destination, self.source.size)
70
    }
71
}
72

            
73
impl SpriteSource {
74
    /// Creates a new sprite source with the location and textuer given.
75
    #[must_use]
76
    pub const fn new(location: Rect<u32>, texture: Texture) -> Self {
77
        Self {
78
            location: SpriteSourceLocation::Rect(location),
79
            texture,
80
        }
81
    }
82

            
83
    /// Creates a sprite by joining multiple rectangular areas from `texture`
84
    /// into one drawable sprite.
85
    #[must_use]
86
    pub fn joined<I: IntoIterator<Item = SpriteSourceSublocation>>(
87
        locations: I,
88
        texture: Texture,
89
    ) -> Self {
90
        Self {
91
            location: SpriteSourceLocation::Joined(locations.into_iter().collect()),
92
            texture,
93
        }
94
    }
95

            
96
    /// Creates a sprite by joining an iterator of `SpriteSource`s into one. All
97
    /// `SpriteSources` must be from the same texture, and the iterator must
98
    /// have a square number of sprites.
99
    #[must_use]
100
    pub fn joined_square<I: IntoIterator<Item = Self>>(sources: I) -> Self {
101
        let sources: Vec<_> = sources.into_iter().collect();
102
        #[allow(clippy::cast_sign_loss)] // sqrt of a positive number is always positive
103
        let sprites_wide = (sources.len() as f32).sqrt() as usize;
104
        assert!(sprites_wide * sprites_wide == sources.len()); // check for square
105
        let texture = sources[0].texture.clone();
106

            
107
        let sprite_size = sources[0].location.bounds().size;
108
        let mut sources = sources.into_iter();
109
        let mut locations = Vec::new();
110
        for y in 0..sprites_wide {
111
            for x in 0..sprites_wide {
112
                let source = sources.next().unwrap();
113
                debug_assert!(texture.id() == source.texture.id());
114
                locations.push(SpriteSourceSublocation {
115
                    source: source.location.bounds(),
116
                    destination: Point::new(
117
                        x as u32 * sprite_size.width,
118
                        y as u32 * sprite_size.height,
119
                    ),
120
                });
121
            }
122
        }
123

            
124
        Self::joined(locations, texture)
125
    }
126

            
127
    /// Creates a sprite source for an entire texture.
128
    #[must_use]
129
    pub fn entire_texture(texture: Texture) -> Self {
130
        Self::new(Rect::new(Point::default(), texture.size()), texture)
131
    }
132

            
133
    /// Renders the sprite at `location` with `rotation` into `scene`.
134
    pub fn render_at(
135
        &self,
136
        scene: &Target,
137
        location: impl Displayable<f32, Pixels = Point<f32, Pixels>>,
138
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
139
    ) {
140
        self.render_at_with_alpha(scene, location, rotation, 1.);
141
    }
142

            
143
    /// Renders the sprite within `bounds` (stretching if needed) with
144
    /// `rotation` into `scene`.
145
    pub fn render_within(
146
        &self,
147
        scene: &Target,
148
        bounds: impl Displayable<f32, Pixels = Rect<f32, Pixels>>,
149
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
150
    ) {
151
        self.render_with_alpha(scene, bounds, rotation, 1.);
152
    }
153

            
154
    /// Renders the sprite with `alpha` at `location` with `rotation` into
155
    /// `scene`.
156
    #[allow(clippy::needless_pass_by_value)]
157
    pub fn render_at_with_alpha(
158
        &self,
159
        scene: &Target,
160
        location: impl Displayable<f32, Pixels = Point<f32, Pixels>>,
161
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
162
        alpha: f32,
163
    ) {
164
        self.render_with_alpha(
165
            scene,
166
            Rect::new(
167
                location.to_pixels(scene.scale()),
168
                self.location
169
                    .size()
170
                    .cast::<f32>()
171
                    .cast_unit::<Scaled>()
172
                    .to_pixels(scene.scale()),
173
            ),
174
            rotation,
175
            alpha,
176
        );
177
    }
178

            
179
    /// Renders the sprite with `alpha` within `bounds` with `rotation` into
180
    /// `scene`.
181
    #[allow(clippy::needless_pass_by_value)]
182
    pub fn render_with_alpha(
183
        &self,
184
        scene: &Target,
185
        bounds: impl Displayable<f32, Pixels = Rect<f32, Pixels>>,
186
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
187
        alpha: f32,
188
    ) {
189
        self.render_with_alpha_in_box(
190
            scene,
191
            bounds.to_pixels(scene.scale()).as_extents(),
192
            rotation,
193
            alpha,
194
        );
195
    }
196

            
197
    /// Renders the sprite with `alpha` within `bounds` with `rotation` into
198
    /// `scene`.
199
    #[allow(clippy::needless_pass_by_value)]
200
    pub fn render_with_alpha_in_box(
201
        &self,
202
        scene: &Target,
203
        bounds: impl Displayable<f32, Pixels = ExtentsRect<f32, Pixels>>,
204
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
205
        alpha: f32,
206
    ) {
207
        let effective_scale = scene.scale();
208
        self.render_raw_with_alpha_in_box(
209
            scene,
210
            bounds.to_pixels(effective_scale),
211
            rotation.to_pixels(effective_scale),
212
            alpha,
213
        );
214
    }
215

            
216
    /// Renders the sprite with `alpha` within `bounds` with `rotation` into
217
    /// `scene`.
218
    #[allow(clippy::needless_pass_by_value)]
219
    pub fn render_raw_with_alpha_in_box(
220
        &self,
221
        scene: &Target,
222
        bounds: impl Displayable<f32, Pixels = ExtentsRect<f32, Pixels>>,
223
        rotation: impl Displayable<f32, Pixels = SpriteRotation<Pixels>>,
224
        alpha: f32,
225
    ) {
226
        let bounds = bounds.to_pixels(scene.scale());
227
        let bounds = ExtentsRect::new(
228
            scene.offset_point_raw(bounds.origin),
229
            scene.offset_point_raw(bounds.extent),
230
        );
231
        scene.push_element(Element::Sprite {
232
            sprite: RenderedSprite::new(
233
                bounds,
234
                rotation.to_pixels(scene.scale()),
235
                alpha,
236
                self.clone(),
237
            ),
238
            clip: scene.clip,
239
        });
240
    }
241
}