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}