Skip to main content

goud_engine/ecs/components/sprite/
component.rs

1//! Core [`Sprite`] component type and its builder methods.
2
3use crate::assets::{loaders::TextureAsset, AssetHandle};
4use crate::core::math::{Color, Rect, Vec2};
5use crate::ecs::Component;
6
7// =============================================================================
8// Sprite Component
9// =============================================================================
10
11/// A 2D sprite component for rendering textured quads.
12///
13/// The `Sprite` component defines how a texture should be rendered in 2D space.
14/// It must be paired with a [`Transform2D`](crate::ecs::components::Transform2D)
15/// or [`GlobalTransform2D`](crate::ecs::components::GlobalTransform2D) component
16/// to define the sprite's position, rotation, and scale.
17///
18/// # Fields
19///
20/// - `texture`: Handle to the texture asset to render
21/// - `color`: Color tint multiplied with texture pixels (default: white)
22/// - `source_rect`: Optional UV rectangle for sprite sheets (default: full texture)
23/// - `flip_x`: Flip the sprite horizontally (default: false)
24/// - `flip_y`: Flip the sprite vertically (default: false)
25/// - `anchor`: Normalized anchor point for rotation/positioning (default: center)
26/// - `custom_size`: Optional override for sprite size (default: texture size)
27///
28/// # Examples
29///
30/// ## Basic Sprite
31///
32/// ```
33/// use goud_engine::ecs::components::Sprite;
34/// use goud_engine::assets::{AssetServer, loaders::TextureAsset};
35///
36/// let mut asset_server = AssetServer::new();
37/// let texture = asset_server.load::<TextureAsset>("player.png");
38///
39/// let sprite = Sprite::new(texture);
40/// ```
41///
42/// ## Sprite with Custom Properties
43///
44/// ```
45/// use goud_engine::ecs::components::Sprite;
46/// use goud_engine::core::math::{Color, Vec2};
47/// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
48/// # let mut asset_server = AssetServer::new();
49/// # let texture = asset_server.load::<TextureAsset>("player.png");
50///
51/// let sprite = Sprite::new(texture)
52///     .with_color(Color::rgba(1.0, 0.5, 0.5, 0.8))
53///     .with_flip_x(true)
54///     .with_anchor(0.5, 1.0)
55///     .with_custom_size(Vec2::new(64.0, 64.0));
56/// ```
57///
58/// ## Sprite Sheet Frame
59///
60/// ```
61/// use goud_engine::ecs::components::Sprite;
62/// use goud_engine::core::math::Rect;
63/// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
64/// # let mut asset_server = AssetServer::new();
65/// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
66///
67/// // Extract a 32x32 tile from the sprite sheet
68/// let sprite = Sprite::new(texture)
69///     .with_source_rect(Rect::new(64.0, 32.0, 32.0, 32.0));
70/// ```
71#[derive(Debug, Clone, PartialEq)]
72pub struct Sprite {
73    /// Handle to the texture asset to render.
74    pub texture: AssetHandle<TextureAsset>,
75
76    /// Color tint multiplied with texture pixels.
77    ///
78    /// Each component is in range [0.0, 1.0]. White (1, 1, 1, 1) renders
79    /// the texture unmodified. RGB values tint the color, alpha controls
80    /// transparency.
81    pub color: Color,
82
83    /// Optional source rectangle for sprite sheets and atlases.
84    ///
85    /// If `None`, the entire texture is rendered. If `Some`, only the
86    /// specified rectangle (in pixel coordinates) is rendered.
87    ///
88    /// For normalized UV coordinates, multiply by texture dimensions.
89    pub source_rect: Option<Rect>,
90
91    /// Flip the sprite horizontally.
92    ///
93    /// When true, the texture is mirrored along the Y-axis.
94    pub flip_x: bool,
95
96    /// Flip the sprite vertically.
97    ///
98    /// When true, the texture is mirrored along the X-axis.
99    pub flip_y: bool,
100
101    /// Normalized anchor point for rotation and positioning.
102    ///
103    /// Coordinates are in range [0.0, 1.0]:
104    /// - `(0.0, 0.0)` = Top-left corner
105    /// - `(0.5, 0.5)` = Center (default)
106    /// - `(1.0, 1.0)` = Bottom-right corner
107    ///
108    /// The anchor point is the origin for rotation and the point that aligns
109    /// with the entity's Transform2D position.
110    pub anchor: Vec2,
111
112    /// Optional custom size override.
113    ///
114    /// If `None`, the sprite renders at the texture's pixel dimensions
115    /// (or source_rect dimensions if specified). If `Some`, the sprite
116    /// is scaled to this size.
117    pub custom_size: Option<Vec2>,
118}
119
120impl Sprite {
121    /// Creates a new sprite with default settings.
122    ///
123    /// The sprite will render the entire texture with:
124    /// - White color tint (no modification)
125    /// - No source rectangle (full texture)
126    /// - No flipping
127    /// - Center anchor point (0.5, 0.5)
128    /// - No custom size (uses texture dimensions)
129    ///
130    /// # Example
131    ///
132    /// ```
133    /// use goud_engine::ecs::components::Sprite;
134    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
135    /// # let mut asset_server = AssetServer::new();
136    /// # let texture = asset_server.load::<TextureAsset>("player.png");
137    ///
138    /// let sprite = Sprite::new(texture);
139    /// ```
140    #[inline]
141    pub fn new(texture: AssetHandle<TextureAsset>) -> Self {
142        Self {
143            texture,
144            color: Color::WHITE,
145            source_rect: None,
146            flip_x: false,
147            flip_y: false,
148            anchor: Vec2::new(0.5, 0.5),
149            custom_size: None,
150        }
151    }
152
153    /// Sets the color tint for this sprite.
154    ///
155    /// The color is multiplied with each texture pixel. Use white (1, 1, 1, 1)
156    /// for no tinting.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use goud_engine::ecs::components::Sprite;
162    /// use goud_engine::core::math::Color;
163    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
164    /// # let mut asset_server = AssetServer::new();
165    /// # let texture = asset_server.load::<TextureAsset>("player.png");
166    ///
167    /// let sprite = Sprite::new(texture)
168    ///     .with_color(Color::rgba(1.0, 0.0, 0.0, 0.5)); // Red, 50% transparent
169    /// ```
170    #[inline]
171    pub fn with_color(mut self, color: Color) -> Self {
172        self.color = color;
173        self
174    }
175
176    /// Sets the source rectangle for sprite sheet rendering.
177    ///
178    /// The rectangle is specified in pixel coordinates relative to the
179    /// top-left corner of the texture.
180    ///
181    /// # Example
182    ///
183    /// ```
184    /// use goud_engine::ecs::components::Sprite;
185    /// use goud_engine::core::math::Rect;
186    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
187    /// # let mut asset_server = AssetServer::new();
188    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
189    ///
190    /// // Extract a 32x32 tile at position (64, 32)
191    /// let sprite = Sprite::new(texture)
192    ///     .with_source_rect(Rect::new(64.0, 32.0, 32.0, 32.0));
193    /// ```
194    #[inline]
195    pub fn with_source_rect(mut self, rect: Rect) -> Self {
196        self.source_rect = Some(rect);
197        self
198    }
199
200    /// Removes the source rectangle, rendering the full texture.
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// # use goud_engine::ecs::components::Sprite;
206    /// # use goud_engine::core::math::Rect;
207    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
208    /// # let mut asset_server = AssetServer::new();
209    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
210    /// let mut sprite = Sprite::new(texture)
211    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
212    ///
213    /// sprite = sprite.without_source_rect();
214    /// assert!(sprite.source_rect.is_none());
215    /// ```
216    #[inline]
217    pub fn without_source_rect(mut self) -> Self {
218        self.source_rect = None;
219        self
220    }
221
222    /// Sets the horizontal flip flag.
223    ///
224    /// When true, the sprite is mirrored along the Y-axis.
225    ///
226    /// # Example
227    ///
228    /// ```
229    /// use goud_engine::ecs::components::Sprite;
230    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
231    /// # let mut asset_server = AssetServer::new();
232    /// # let texture = asset_server.load::<TextureAsset>("player.png");
233    ///
234    /// let sprite = Sprite::new(texture).with_flip_x(true);
235    /// ```
236    #[inline]
237    pub fn with_flip_x(mut self, flip: bool) -> Self {
238        self.flip_x = flip;
239        self
240    }
241
242    /// Sets the vertical flip flag.
243    ///
244    /// When true, the sprite is mirrored along the X-axis.
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// use goud_engine::ecs::components::Sprite;
250    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
251    /// # let mut asset_server = AssetServer::new();
252    /// # let texture = asset_server.load::<TextureAsset>("player.png");
253    ///
254    /// let sprite = Sprite::new(texture).with_flip_y(true);
255    /// ```
256    #[inline]
257    pub fn with_flip_y(mut self, flip: bool) -> Self {
258        self.flip_y = flip;
259        self
260    }
261
262    /// Sets both flip flags at once.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// use goud_engine::ecs::components::Sprite;
268    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
269    /// # let mut asset_server = AssetServer::new();
270    /// # let texture = asset_server.load::<TextureAsset>("player.png");
271    ///
272    /// let sprite = Sprite::new(texture).with_flip(true, true);
273    /// ```
274    #[inline]
275    pub fn with_flip(mut self, flip_x: bool, flip_y: bool) -> Self {
276        self.flip_x = flip_x;
277        self.flip_y = flip_y;
278        self
279    }
280
281    /// Sets the anchor point with individual coordinates.
282    ///
283    /// Coordinates are normalized in range [0.0, 1.0]:
284    /// - `(0.0, 0.0)` = Top-left
285    /// - `(0.5, 0.5)` = Center
286    /// - `(1.0, 1.0)` = Bottom-right
287    ///
288    /// # Example
289    ///
290    /// ```
291    /// use goud_engine::ecs::components::Sprite;
292    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
293    /// # let mut asset_server = AssetServer::new();
294    /// # let texture = asset_server.load::<TextureAsset>("player.png");
295    ///
296    /// // Bottom-center anchor for ground-aligned sprites
297    /// let sprite = Sprite::new(texture).with_anchor(0.5, 1.0);
298    /// ```
299    #[inline]
300    pub fn with_anchor(mut self, x: f32, y: f32) -> Self {
301        self.anchor = Vec2::new(x, y);
302        self
303    }
304
305    /// Sets the anchor point from a Vec2.
306    ///
307    /// # Example
308    ///
309    /// ```
310    /// use goud_engine::ecs::components::Sprite;
311    /// use goud_engine::core::math::Vec2;
312    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
313    /// # let mut asset_server = AssetServer::new();
314    /// # let texture = asset_server.load::<TextureAsset>("player.png");
315    ///
316    /// let sprite = Sprite::new(texture)
317    ///     .with_anchor_vec(Vec2::new(0.5, 1.0));
318    /// ```
319    #[inline]
320    pub fn with_anchor_vec(mut self, anchor: Vec2) -> Self {
321        self.anchor = anchor;
322        self
323    }
324
325    /// Sets a custom size for the sprite.
326    ///
327    /// When set, the sprite is scaled to this size regardless of the
328    /// texture dimensions.
329    ///
330    /// # Example
331    ///
332    /// ```
333    /// use goud_engine::ecs::components::Sprite;
334    /// use goud_engine::core::math::Vec2;
335    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
336    /// # let mut asset_server = AssetServer::new();
337    /// # let texture = asset_server.load::<TextureAsset>("player.png");
338    ///
339    /// // Force sprite to 64x64 size
340    /// let sprite = Sprite::new(texture)
341    ///     .with_custom_size(Vec2::new(64.0, 64.0));
342    /// ```
343    #[inline]
344    pub fn with_custom_size(mut self, size: Vec2) -> Self {
345        self.custom_size = Some(size);
346        self
347    }
348
349    /// Removes the custom size, using texture dimensions.
350    ///
351    /// # Example
352    ///
353    /// ```
354    /// use goud_engine::ecs::components::Sprite;
355    /// use goud_engine::core::math::Vec2;
356    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
357    /// # let mut asset_server = AssetServer::new();
358    /// # let texture = asset_server.load::<TextureAsset>("player.png");
359    ///
360    /// let mut sprite = Sprite::new(texture)
361    ///     .with_custom_size(Vec2::new(64.0, 64.0));
362    ///
363    /// sprite = sprite.without_custom_size();
364    /// assert!(sprite.custom_size.is_none());
365    /// ```
366    #[inline]
367    pub fn without_custom_size(mut self) -> Self {
368        self.custom_size = None;
369        self
370    }
371
372    /// Gets the effective size of the sprite.
373    ///
374    /// Returns the custom size if set, otherwise the source rect size if set,
375    /// otherwise falls back to a default size (requires texture dimensions).
376    ///
377    /// For actual rendering, you'll need to query the texture asset to get
378    /// its dimensions when custom_size and source_rect are both None.
379    ///
380    /// # Example
381    ///
382    /// ```
383    /// use goud_engine::ecs::components::Sprite;
384    /// use goud_engine::core::math::{Vec2, Rect};
385    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
386    /// # let mut asset_server = AssetServer::new();
387    /// # let texture = asset_server.load::<TextureAsset>("player.png");
388    ///
389    /// let sprite = Sprite::new(texture)
390    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
391    ///
392    /// let size = sprite.size_or_rect();
393    /// assert_eq!(size, Vec2::new(32.0, 32.0));
394    /// ```
395    #[inline]
396    pub fn size_or_rect(&self) -> Vec2 {
397        if let Some(size) = self.custom_size {
398            size
399        } else if let Some(rect) = self.source_rect {
400            Vec2::new(rect.width, rect.height)
401        } else {
402            Vec2::zero() // Caller must query texture dimensions
403        }
404    }
405
406    /// Returns true if the sprite has a source rectangle set.
407    #[inline]
408    pub fn has_source_rect(&self) -> bool {
409        self.source_rect.is_some()
410    }
411
412    /// Returns true if the sprite has a custom size set.
413    #[inline]
414    pub fn has_custom_size(&self) -> bool {
415        self.custom_size.is_some()
416    }
417
418    /// Returns true if the sprite is flipped on either axis.
419    #[inline]
420    pub fn is_flipped(&self) -> bool {
421        self.flip_x || self.flip_y
422    }
423}
424
425// Implement Component trait so Sprite can be used in the ECS
426impl Component for Sprite {}
427
428// =============================================================================
429// Default Implementation
430// =============================================================================
431
432impl Default for Sprite {
433    /// Creates a sprite with an invalid texture handle.
434    ///
435    /// This is primarily useful for deserialization or when the texture
436    /// will be set later. The sprite will not render correctly until a
437    /// valid texture handle is assigned.
438    fn default() -> Self {
439        Self {
440            texture: AssetHandle::INVALID,
441            color: Color::WHITE,
442            source_rect: None,
443            flip_x: false,
444            flip_y: false,
445            anchor: Vec2::new(0.5, 0.5),
446            custom_size: None,
447        }
448    }
449}