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, serde::Serialize, serde::Deserialize)]
72pub struct Sprite {
73    /// Handle to the texture asset to render.
74    // TODO(#219): Resolve texture_path back to a handle on deserialization
75    #[serde(skip)]
76    pub texture: AssetHandle<TextureAsset>,
77
78    /// Optional path to the texture asset for serialization.
79    ///
80    /// When a scene is serialized the handle cannot be persisted, but
81    /// this path string can. At load time a higher-level system
82    /// resolves the path back to an [`AssetHandle`].
83    #[serde(default)]
84    pub texture_path: Option<String>,
85
86    /// Color tint multiplied with texture pixels.
87    ///
88    /// Each component is in range [0.0, 1.0]. White (1, 1, 1, 1) renders
89    /// the texture unmodified. RGB values tint the color, alpha controls
90    /// transparency.
91    pub color: Color,
92
93    /// Optional source rectangle for sprite sheets and atlases.
94    ///
95    /// If `None`, the entire texture is rendered. If `Some`, only the
96    /// specified rectangle (in pixel coordinates) is rendered.
97    ///
98    /// For normalized UV coordinates, multiply by texture dimensions.
99    pub source_rect: Option<Rect>,
100
101    /// Flip the sprite horizontally.
102    ///
103    /// When true, the texture is mirrored along the Y-axis.
104    pub flip_x: bool,
105
106    /// Flip the sprite vertically.
107    ///
108    /// When true, the texture is mirrored along the X-axis.
109    pub flip_y: bool,
110
111    /// Normalized anchor point for rotation and positioning.
112    ///
113    /// Coordinates are in range [0.0, 1.0]:
114    /// - `(0.0, 0.0)` = Top-left corner
115    /// - `(0.5, 0.5)` = Center (default)
116    /// - `(1.0, 1.0)` = Bottom-right corner
117    ///
118    /// The anchor point is the origin for rotation and the point that aligns
119    /// with the entity's Transform2D position.
120    pub anchor: Vec2,
121
122    /// Optional custom size override.
123    ///
124    /// If `None`, the sprite renders at the texture's pixel dimensions
125    /// (or source_rect dimensions if specified). If `Some`, the sprite
126    /// is scaled to this size.
127    pub custom_size: Option<Vec2>,
128}
129
130impl Sprite {
131    /// Creates a new sprite with default settings.
132    ///
133    /// The sprite will render the entire texture with:
134    /// - White color tint (no modification)
135    /// - No source rectangle (full texture)
136    /// - No flipping
137    /// - Center anchor point (0.5, 0.5)
138    /// - No custom size (uses texture dimensions)
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use goud_engine::ecs::components::Sprite;
144    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
145    /// # let mut asset_server = AssetServer::new();
146    /// # let texture = asset_server.load::<TextureAsset>("player.png");
147    ///
148    /// let sprite = Sprite::new(texture);
149    /// ```
150    #[inline]
151    pub fn new(texture: AssetHandle<TextureAsset>) -> Self {
152        Self {
153            texture,
154            texture_path: None,
155            color: Color::WHITE,
156            source_rect: None,
157            flip_x: false,
158            flip_y: false,
159            anchor: Vec2::new(0.5, 0.5),
160            custom_size: None,
161        }
162    }
163
164    /// Sets the texture asset path for serialization.
165    ///
166    /// The path is stored alongside the sprite so it survives
167    /// serialization. A higher-level system is responsible for
168    /// resolving it back to an [`AssetHandle`] after deserialization.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use goud_engine::ecs::components::Sprite;
174    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
175    /// # let mut asset_server = AssetServer::new();
176    /// # let texture = asset_server.load::<TextureAsset>("player.png");
177    ///
178    /// let sprite = Sprite::new(texture)
179    ///     .with_texture_path("player.png");
180    /// assert_eq!(sprite.texture_path.as_deref(), Some("player.png"));
181    /// ```
182    #[inline]
183    pub fn with_texture_path(mut self, path: impl Into<String>) -> Self {
184        self.texture_path = Some(path.into());
185        self
186    }
187
188    /// Sets the color tint for this sprite.
189    ///
190    /// The color is multiplied with each texture pixel. Use white (1, 1, 1, 1)
191    /// for no tinting.
192    ///
193    /// # Example
194    ///
195    /// ```
196    /// use goud_engine::ecs::components::Sprite;
197    /// use goud_engine::core::math::Color;
198    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
199    /// # let mut asset_server = AssetServer::new();
200    /// # let texture = asset_server.load::<TextureAsset>("player.png");
201    ///
202    /// let sprite = Sprite::new(texture)
203    ///     .with_color(Color::rgba(1.0, 0.0, 0.0, 0.5)); // Red, 50% transparent
204    /// ```
205    #[inline]
206    pub fn with_color(mut self, color: Color) -> Self {
207        self.color = color;
208        self
209    }
210
211    /// Sets the source rectangle for sprite sheet rendering.
212    ///
213    /// The rectangle is specified in pixel coordinates relative to the
214    /// top-left corner of the texture.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// use goud_engine::ecs::components::Sprite;
220    /// use goud_engine::core::math::Rect;
221    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
222    /// # let mut asset_server = AssetServer::new();
223    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
224    ///
225    /// // Extract a 32x32 tile at position (64, 32)
226    /// let sprite = Sprite::new(texture)
227    ///     .with_source_rect(Rect::new(64.0, 32.0, 32.0, 32.0));
228    /// ```
229    #[inline]
230    pub fn with_source_rect(mut self, rect: Rect) -> Self {
231        self.source_rect = Some(rect);
232        self
233    }
234
235    /// Removes the source rectangle, rendering the full texture.
236    ///
237    /// # Example
238    ///
239    /// ```
240    /// # use goud_engine::ecs::components::Sprite;
241    /// # use goud_engine::core::math::Rect;
242    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
243    /// # let mut asset_server = AssetServer::new();
244    /// # let texture = asset_server.load::<TextureAsset>("spritesheet.png");
245    /// let mut sprite = Sprite::new(texture)
246    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
247    ///
248    /// sprite = sprite.without_source_rect();
249    /// assert!(sprite.source_rect.is_none());
250    /// ```
251    #[inline]
252    pub fn without_source_rect(mut self) -> Self {
253        self.source_rect = None;
254        self
255    }
256
257    /// Sets the horizontal flip flag.
258    ///
259    /// When true, the sprite is mirrored along the Y-axis.
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use goud_engine::ecs::components::Sprite;
265    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
266    /// # let mut asset_server = AssetServer::new();
267    /// # let texture = asset_server.load::<TextureAsset>("player.png");
268    ///
269    /// let sprite = Sprite::new(texture).with_flip_x(true);
270    /// ```
271    #[inline]
272    pub fn with_flip_x(mut self, flip: bool) -> Self {
273        self.flip_x = flip;
274        self
275    }
276
277    /// Sets the vertical flip flag.
278    ///
279    /// When true, the sprite is mirrored along the X-axis.
280    ///
281    /// # Example
282    ///
283    /// ```
284    /// use goud_engine::ecs::components::Sprite;
285    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
286    /// # let mut asset_server = AssetServer::new();
287    /// # let texture = asset_server.load::<TextureAsset>("player.png");
288    ///
289    /// let sprite = Sprite::new(texture).with_flip_y(true);
290    /// ```
291    #[inline]
292    pub fn with_flip_y(mut self, flip: bool) -> Self {
293        self.flip_y = flip;
294        self
295    }
296
297    /// Sets both flip flags at once.
298    ///
299    /// # Example
300    ///
301    /// ```
302    /// use goud_engine::ecs::components::Sprite;
303    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
304    /// # let mut asset_server = AssetServer::new();
305    /// # let texture = asset_server.load::<TextureAsset>("player.png");
306    ///
307    /// let sprite = Sprite::new(texture).with_flip(true, true);
308    /// ```
309    #[inline]
310    pub fn with_flip(mut self, flip_x: bool, flip_y: bool) -> Self {
311        self.flip_x = flip_x;
312        self.flip_y = flip_y;
313        self
314    }
315
316    /// Sets the anchor point with individual coordinates.
317    ///
318    /// Coordinates are normalized in range [0.0, 1.0]:
319    /// - `(0.0, 0.0)` = Top-left
320    /// - `(0.5, 0.5)` = Center
321    /// - `(1.0, 1.0)` = Bottom-right
322    ///
323    /// # Example
324    ///
325    /// ```
326    /// use goud_engine::ecs::components::Sprite;
327    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
328    /// # let mut asset_server = AssetServer::new();
329    /// # let texture = asset_server.load::<TextureAsset>("player.png");
330    ///
331    /// // Bottom-center anchor for ground-aligned sprites
332    /// let sprite = Sprite::new(texture).with_anchor(0.5, 1.0);
333    /// ```
334    #[inline]
335    pub fn with_anchor(mut self, x: f32, y: f32) -> Self {
336        self.anchor = Vec2::new(x, y);
337        self
338    }
339
340    /// Sets the anchor point from a Vec2.
341    ///
342    /// # Example
343    ///
344    /// ```
345    /// use goud_engine::ecs::components::Sprite;
346    /// use goud_engine::core::math::Vec2;
347    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
348    /// # let mut asset_server = AssetServer::new();
349    /// # let texture = asset_server.load::<TextureAsset>("player.png");
350    ///
351    /// let sprite = Sprite::new(texture)
352    ///     .with_anchor_vec(Vec2::new(0.5, 1.0));
353    /// ```
354    #[inline]
355    pub fn with_anchor_vec(mut self, anchor: Vec2) -> Self {
356        self.anchor = anchor;
357        self
358    }
359
360    /// Sets a custom size for the sprite.
361    ///
362    /// When set, the sprite is scaled to this size regardless of the
363    /// texture dimensions.
364    ///
365    /// # Example
366    ///
367    /// ```
368    /// use goud_engine::ecs::components::Sprite;
369    /// use goud_engine::core::math::Vec2;
370    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
371    /// # let mut asset_server = AssetServer::new();
372    /// # let texture = asset_server.load::<TextureAsset>("player.png");
373    ///
374    /// // Force sprite to 64x64 size
375    /// let sprite = Sprite::new(texture)
376    ///     .with_custom_size(Vec2::new(64.0, 64.0));
377    /// ```
378    #[inline]
379    pub fn with_custom_size(mut self, size: Vec2) -> Self {
380        self.custom_size = Some(size);
381        self
382    }
383
384    /// Removes the custom size, using texture dimensions.
385    ///
386    /// # Example
387    ///
388    /// ```
389    /// use goud_engine::ecs::components::Sprite;
390    /// use goud_engine::core::math::Vec2;
391    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
392    /// # let mut asset_server = AssetServer::new();
393    /// # let texture = asset_server.load::<TextureAsset>("player.png");
394    ///
395    /// let mut sprite = Sprite::new(texture)
396    ///     .with_custom_size(Vec2::new(64.0, 64.0));
397    ///
398    /// sprite = sprite.without_custom_size();
399    /// assert!(sprite.custom_size.is_none());
400    /// ```
401    #[inline]
402    pub fn without_custom_size(mut self) -> Self {
403        self.custom_size = None;
404        self
405    }
406
407    /// Gets the effective size of the sprite.
408    ///
409    /// Returns the custom size if set, otherwise the source rect size if set,
410    /// otherwise falls back to a default size (requires texture dimensions).
411    ///
412    /// For actual rendering, you'll need to query the texture asset to get
413    /// its dimensions when custom_size and source_rect are both None.
414    ///
415    /// # Example
416    ///
417    /// ```
418    /// use goud_engine::ecs::components::Sprite;
419    /// use goud_engine::core::math::{Vec2, Rect};
420    /// # use goud_engine::assets::{AssetServer, loaders::TextureAsset};
421    /// # let mut asset_server = AssetServer::new();
422    /// # let texture = asset_server.load::<TextureAsset>("player.png");
423    ///
424    /// let sprite = Sprite::new(texture)
425    ///     .with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0));
426    ///
427    /// let size = sprite.size_or_rect();
428    /// assert_eq!(size, Vec2::new(32.0, 32.0));
429    /// ```
430    #[inline]
431    pub fn size_or_rect(&self) -> Vec2 {
432        if let Some(size) = self.custom_size {
433            size
434        } else if let Some(rect) = self.source_rect {
435            Vec2::new(rect.width, rect.height)
436        } else {
437            Vec2::zero() // Caller must query texture dimensions
438        }
439    }
440
441    /// Returns true if the sprite has a source rectangle set.
442    #[inline]
443    pub fn has_source_rect(&self) -> bool {
444        self.source_rect.is_some()
445    }
446
447    /// Returns true if the sprite has a custom size set.
448    #[inline]
449    pub fn has_custom_size(&self) -> bool {
450        self.custom_size.is_some()
451    }
452
453    /// Returns true if the sprite is flipped on either axis.
454    #[inline]
455    pub fn is_flipped(&self) -> bool {
456        self.flip_x || self.flip_y
457    }
458}
459
460// Implement Component trait so Sprite can be used in the ECS
461impl Component for Sprite {}
462
463// =============================================================================
464// Default Implementation
465// =============================================================================
466
467impl Default for Sprite {
468    /// Creates a sprite with an invalid texture handle.
469    ///
470    /// This is primarily useful for deserialization or when the texture
471    /// will be set later. The sprite will not render correctly until a
472    /// valid texture handle is assigned.
473    fn default() -> Self {
474        Self {
475            texture: AssetHandle::INVALID,
476            texture_path: None,
477            color: Color::WHITE,
478            source_rect: None,
479            flip_x: false,
480            flip_y: false,
481            anchor: Vec2::new(0.5, 0.5),
482            custom_size: None,
483        }
484    }
485}