1use bevy_asset::{AsAssetId, AssetId, Assets, Handle};
2use bevy_camera::visibility::{self, Visibility, VisibilityClass};
3use bevy_color::Color;
4use bevy_derive::{Deref, DerefMut};
5use bevy_ecs::{component::Component, reflect::ReflectComponent};
6use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
7use bevy_math::{Rect, UVec2, Vec2};
8use bevy_reflect::{std_traits::ReflectDefault, Reflect};
9use bevy_transform::components::Transform;
10
11use crate::TextureSlicer;
12
13#[derive(Component, Debug, Default, Clone, Reflect)]
15#[require(Transform, Visibility, VisibilityClass, Anchor)]
16#[reflect(Component, Default, Debug, Clone)]
17#[component(on_add = visibility::add_visibility_class::<Sprite>)]
18pub struct Sprite {
19 pub image: Handle<Image>,
21 pub texture_atlas: Option<TextureAtlas>,
23 pub color: Color,
25 pub flip_x: bool,
27 pub flip_y: bool,
29 pub custom_size: Option<Vec2>,
32 pub rect: Option<Rect>,
38 pub image_mode: SpriteImageMode,
40}
41
42impl Sprite {
43 pub fn sized(custom_size: Vec2) -> Self {
45 Sprite {
46 custom_size: Some(custom_size),
47 ..Default::default()
48 }
49 }
50
51 pub fn from_image(image: Handle<Image>) -> Self {
53 Self {
54 image,
55 ..Default::default()
56 }
57 }
58
59 pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
61 Self {
62 image,
63 texture_atlas: Some(atlas),
64 ..Default::default()
65 }
66 }
67
68 pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
70 Self {
71 color: color.into(),
72 custom_size: Some(size),
73 ..Default::default()
74 }
75 }
76
77 pub fn compute_pixel_space_point(
82 &self,
83 point_relative_to_sprite: Vec2,
84 anchor: Anchor,
85 images: &Assets<Image>,
86 texture_atlases: &Assets<TextureAtlasLayout>,
87 ) -> Result<Vec2, Vec2> {
88 let image_size = images
89 .get(&self.image)
90 .map(Image::size)
91 .unwrap_or(UVec2::ONE);
92
93 let atlas_rect = self
94 .texture_atlas
95 .as_ref()
96 .and_then(|s| s.texture_rect(texture_atlases))
97 .map(|r| r.as_rect());
98 let texture_rect = match (atlas_rect, self.rect) {
99 (None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32),
100 (None, Some(sprite_rect)) => sprite_rect,
101 (Some(atlas_rect), None) => atlas_rect,
102 (Some(atlas_rect), Some(mut sprite_rect)) => {
103 sprite_rect.min += atlas_rect.min;
105 sprite_rect.max += atlas_rect.min;
106 sprite_rect
107 }
108 };
109
110 let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
111 let sprite_center = -anchor.as_vec() * sprite_size;
112
113 let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;
114
115 if self.flip_x {
116 point_relative_to_sprite_center.x *= -1.0;
117 }
118 if !self.flip_y {
121 point_relative_to_sprite_center.y *= -1.0;
122 }
123
124 if sprite_size.x == 0.0 || sprite_size.y == 0.0 {
125 return Err(point_relative_to_sprite_center);
126 }
127
128 let sprite_to_texture_ratio = {
129 let texture_size = texture_rect.size();
130 Vec2::new(
131 texture_size.x / sprite_size.x,
132 texture_size.y / sprite_size.y,
133 )
134 };
135
136 let point_relative_to_texture =
137 point_relative_to_sprite_center * sprite_to_texture_ratio + texture_rect.center();
138
139 if texture_rect.contains(point_relative_to_texture) {
142 Ok(point_relative_to_texture)
143 } else {
144 Err(point_relative_to_texture)
145 }
146 }
147}
148
149impl From<Handle<Image>> for Sprite {
150 fn from(image: Handle<Image>) -> Self {
151 Self::from_image(image)
152 }
153}
154
155impl AsAssetId for Sprite {
156 type Asset = Image;
157
158 fn as_asset_id(&self) -> AssetId<Self::Asset> {
159 self.image.id()
160 }
161}
162
163#[derive(Default, Debug, Clone, Reflect, PartialEq)]
165#[reflect(Debug, Default, Clone)]
166pub enum SpriteImageMode {
167 #[default]
169 Auto,
170 Scale(ScalingMode),
173 Sliced(TextureSlicer),
175 Tiled {
177 tile_x: bool,
179 tile_y: bool,
181 stretch_value: f32,
184 },
185}
186
187impl SpriteImageMode {
188 #[inline]
190 pub fn uses_slices(&self) -> bool {
191 matches!(
192 self,
193 SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
194 )
195 }
196
197 #[inline]
199 #[must_use]
200 pub const fn scale(&self) -> Option<ScalingMode> {
201 if let SpriteImageMode::Scale(scale) = self {
202 Some(*scale)
203 } else {
204 None
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
213#[reflect(Debug, Default, Clone)]
214pub enum ScalingMode {
215 #[default]
220 FillCenter,
221 FillStart,
228 FillEnd,
235 FitCenter,
239 FitStart,
244 FitEnd,
249}
250
251#[derive(Component, Debug, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
253#[reflect(Component, Default, Debug, PartialEq, Clone)]
254#[doc(alias = "pivot")]
255pub struct Anchor(pub Vec2);
256
257impl Anchor {
258 pub const BOTTOM_LEFT: Self = Self(Vec2::new(-0.5, -0.5));
259 pub const BOTTOM_CENTER: Self = Self(Vec2::new(0.0, -0.5));
260 pub const BOTTOM_RIGHT: Self = Self(Vec2::new(0.5, -0.5));
261 pub const CENTER_LEFT: Self = Self(Vec2::new(-0.5, 0.0));
262 pub const CENTER: Self = Self(Vec2::ZERO);
263 pub const CENTER_RIGHT: Self = Self(Vec2::new(0.5, 0.0));
264 pub const TOP_LEFT: Self = Self(Vec2::new(-0.5, 0.5));
265 pub const TOP_CENTER: Self = Self(Vec2::new(0.0, 0.5));
266 pub const TOP_RIGHT: Self = Self(Vec2::new(0.5, 0.5));
267
268 pub fn as_vec(&self) -> Vec2 {
269 self.0
270 }
271}
272
273impl Default for Anchor {
274 fn default() -> Self {
275 Self::CENTER
276 }
277}
278
279impl From<Vec2> for Anchor {
280 fn from(value: Vec2) -> Self {
281 Self(value)
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use bevy_asset::{Assets, RenderAssetUsages};
288 use bevy_color::Color;
289 use bevy_image::{Image, ToExtents};
290 use bevy_image::{TextureAtlas, TextureAtlasLayout};
291 use bevy_math::{Rect, URect, UVec2, Vec2};
292 use wgpu_types::{TextureDimension, TextureFormat};
293
294 use crate::Anchor;
295
296 use super::Sprite;
297
298 fn make_image(size: UVec2) -> Image {
300 Image::new_fill(
301 size.to_extents(),
302 TextureDimension::D2,
303 &[0, 0, 0, 255],
304 TextureFormat::Rgba8Unorm,
305 RenderAssetUsages::all(),
306 )
307 }
308
309 #[test]
310 fn compute_pixel_space_point_for_regular_sprite() {
311 let mut image_assets = Assets::<Image>::default();
312 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
313
314 let image = image_assets.add(make_image(UVec2::new(5, 10)));
315
316 let sprite = Sprite {
317 image,
318 ..Default::default()
319 };
320
321 let compute = |point| {
322 sprite.compute_pixel_space_point(
323 point,
324 Anchor::default(),
325 &image_assets,
326 &texture_atlas_assets,
327 )
328 };
329 assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5)));
330 assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0)));
331 assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5)));
332 assert_eq!(compute(Vec2::new(3.0, 0.0)), Err(Vec2::new(5.5, 5.0)));
333 assert_eq!(compute(Vec2::new(-3.0, 0.0)), Err(Vec2::new(-0.5, 5.0)));
334 }
335
336 #[test]
337 fn compute_pixel_space_point_for_color_sprite() {
338 let image_assets = Assets::<Image>::default();
339 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
340
341 let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0));
343
344 let compute = |point| {
345 sprite
346 .compute_pixel_space_point(
347 point,
348 Anchor::default(),
349 &image_assets,
350 &texture_atlas_assets,
351 )
352 .map(|x| (x * 1e5).round() / 1e5)
354 .map_err(|x| (x * 1e5).round() / 1e5)
355 };
356 assert_eq!(compute(Vec2::new(-20.0, -40.0)), Ok(Vec2::new(0.1, 0.9)));
357 assert_eq!(compute(Vec2::new(0.0, 10.0)), Ok(Vec2::new(0.5, 0.4)));
358 assert_eq!(compute(Vec2::new(75.0, 100.0)), Err(Vec2::new(2.0, -0.5)));
359 assert_eq!(compute(Vec2::new(-75.0, -100.0)), Err(Vec2::new(-1.0, 1.5)));
360 assert_eq!(compute(Vec2::new(-30.0, -40.0)), Err(Vec2::new(-0.1, 0.9)));
361 }
362
363 #[test]
364 fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() {
365 let mut image_assets = Assets::<Image>::default();
366 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
367
368 let image = image_assets.add(make_image(UVec2::new(5, 10)));
369
370 let sprite = Sprite {
371 image,
372 ..Default::default()
373 };
374 let anchor = Anchor::BOTTOM_LEFT;
375
376 let compute = |point| {
377 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
378 };
379 assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5)));
380 assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
381 assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
382 assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
383 assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
384 }
385
386 #[test]
387 fn compute_pixel_space_point_for_sprite_with_anchor_top_right() {
388 let mut image_assets = Assets::<Image>::default();
389 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
390
391 let image = image_assets.add(make_image(UVec2::new(5, 10)));
392
393 let sprite = Sprite {
394 image,
395 ..Default::default()
396 };
397 let anchor = Anchor::TOP_RIGHT;
398
399 let compute = |point| {
400 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
401 };
402 assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5)));
403 assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
404 assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5)));
405 assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
406 assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
407 }
408
409 #[test]
410 fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() {
411 let mut image_assets = Assets::<Image>::default();
412 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
413
414 let image = image_assets.add(make_image(UVec2::new(5, 10)));
415
416 let sprite = Sprite {
417 image,
418 flip_x: true,
419 ..Default::default()
420 };
421 let anchor = Anchor::BOTTOM_LEFT;
422
423 let compute = |point| {
424 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
425 };
426 assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5)));
427 assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
428 assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
429 assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
430 assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
431 }
432
433 #[test]
434 fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() {
435 let mut image_assets = Assets::<Image>::default();
436 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
437
438 let image = image_assets.add(make_image(UVec2::new(5, 10)));
439
440 let sprite = Sprite {
441 image,
442 flip_y: true,
443 ..Default::default()
444 };
445 let anchor = Anchor::TOP_RIGHT;
446
447 let compute = |point| {
448 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
449 };
450 assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5)));
451 assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
452 assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5)));
453 assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
454 assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
455 }
456
457 #[test]
458 fn compute_pixel_space_point_for_sprite_with_rect() {
459 let mut image_assets = Assets::<Image>::default();
460 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
461
462 let image = image_assets.add(make_image(UVec2::new(5, 10)));
463
464 let sprite = Sprite {
465 image,
466 rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)),
467 ..Default::default()
468 };
469 let anchor = Anchor::BOTTOM_LEFT;
470
471 let compute = |point| {
472 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
473 };
474 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0)));
475 assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0)));
477 }
478
479 #[test]
480 fn compute_pixel_space_point_for_texture_atlas_sprite() {
481 let mut image_assets = Assets::<Image>::default();
482 let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
483
484 let image = image_assets.add(make_image(UVec2::new(5, 10)));
485 let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
486 size: UVec2::new(5, 10),
487 textures: vec![URect::new(1, 1, 4, 4)],
488 });
489
490 let sprite = Sprite {
491 image,
492 texture_atlas: Some(TextureAtlas {
493 layout: texture_atlas,
494 index: 0,
495 }),
496 ..Default::default()
497 };
498 let anchor = Anchor::BOTTOM_LEFT;
499
500 let compute = |point| {
501 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
502 };
503 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5)));
504 assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5)));
506 }
507
508 #[test]
509 fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() {
510 let mut image_assets = Assets::<Image>::default();
511 let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
512
513 let image = image_assets.add(make_image(UVec2::new(5, 10)));
514 let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
515 size: UVec2::new(5, 10),
516 textures: vec![URect::new(1, 1, 4, 4)],
517 });
518
519 let sprite = Sprite {
520 image,
521 texture_atlas: Some(TextureAtlas {
522 layout: texture_atlas,
523 index: 0,
524 }),
525 rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)),
527 ..Default::default()
528 };
529 let anchor = Anchor::BOTTOM_LEFT;
530
531 let compute = |point| {
532 sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
533 };
534 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5)));
535 assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5)));
537 }
538
539 #[test]
540 fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() {
541 let mut image_assets = Assets::<Image>::default();
542 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
543
544 let image = image_assets.add(make_image(UVec2::new(5, 10)));
545
546 let sprite = Sprite {
547 image,
548 custom_size: Some(Vec2::new(100.0, 50.0)),
549 rect: Some(Rect::new(0.0, 0.0, 5.0, 5.0)),
550 ..Default::default()
551 };
552
553 let compute = |point| {
554 sprite.compute_pixel_space_point(
555 point,
556 Anchor::default(),
557 &image_assets,
558 &texture_atlas_assets,
559 )
560 };
561 assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0)));
562 assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0)));
563 assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0)));
565 }
566
567 #[test]
568 fn compute_pixel_space_point_for_sprite_with_zero_custom_size() {
569 let mut image_assets = Assets::<Image>::default();
570 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
571
572 let image = image_assets.add(make_image(UVec2::new(5, 10)));
573
574 let sprite = Sprite {
575 image,
576 custom_size: Some(Vec2::new(0.0, 0.0)),
577 ..Default::default()
578 };
579
580 let compute = |point| {
581 sprite.compute_pixel_space_point(
582 point,
583 Anchor::default(),
584 &image_assets,
585 &texture_atlas_assets,
586 )
587 };
588 assert_eq!(compute(Vec2::new(30.0, 15.0)), Err(Vec2::new(30.0, -15.0)));
589 assert_eq!(
590 compute(Vec2::new(-10.0, -15.0)),
591 Err(Vec2::new(-10.0, 15.0))
592 );
593 assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(0.0, -35.0)));
595 }
596}