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}