1use bevy_asset::{Assets, Handle};
2use bevy_color::Color;
3use bevy_ecs::{component::Component, reflect::ReflectComponent};
4use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
5use bevy_math::{Rect, UVec2, Vec2};
6use bevy_reflect::{std_traits::ReflectDefault, Reflect};
7use bevy_render::{
8 sync_world::SyncToRenderWorld,
9 view::{self, Visibility, VisibilityClass},
10};
11use bevy_transform::components::Transform;
12
13use crate::TextureSlicer;
14
15#[derive(Component, Debug, Default, Clone, Reflect)]
17#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)]
18#[reflect(Component, Default, Debug, Clone)]
19#[component(on_add = view::add_visibility_class::<Sprite>)]
20pub struct Sprite {
21 pub image: Handle<Image>,
23 pub texture_atlas: Option<TextureAtlas>,
25 pub color: Color,
27 pub flip_x: bool,
29 pub flip_y: bool,
31 pub custom_size: Option<Vec2>,
34 pub rect: Option<Rect>,
40 pub anchor: Anchor,
42 pub image_mode: SpriteImageMode,
44}
45
46impl Sprite {
47 pub fn sized(custom_size: Vec2) -> Self {
49 Sprite {
50 custom_size: Some(custom_size),
51 ..Default::default()
52 }
53 }
54
55 pub fn from_image(image: Handle<Image>) -> Self {
57 Self {
58 image,
59 ..Default::default()
60 }
61 }
62
63 pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
65 Self {
66 image,
67 texture_atlas: Some(atlas),
68 ..Default::default()
69 }
70 }
71
72 pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
74 Self {
75 color: color.into(),
76 custom_size: Some(size),
77 ..Default::default()
78 }
79 }
80
81 pub fn compute_pixel_space_point(
86 &self,
87 point_relative_to_sprite: Vec2,
88 images: &Assets<Image>,
89 texture_atlases: &Assets<TextureAtlasLayout>,
90 ) -> Result<Vec2, Vec2> {
91 let image_size = images
92 .get(&self.image)
93 .map(Image::size)
94 .unwrap_or(UVec2::ONE);
95
96 let atlas_rect = self
97 .texture_atlas
98 .as_ref()
99 .and_then(|s| s.texture_rect(texture_atlases))
100 .map(|r| r.as_rect());
101 let texture_rect = match (atlas_rect, self.rect) {
102 (None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32),
103 (None, Some(sprite_rect)) => sprite_rect,
104 (Some(atlas_rect), None) => atlas_rect,
105 (Some(atlas_rect), Some(mut sprite_rect)) => {
106 sprite_rect.min += atlas_rect.min;
108 sprite_rect.max += atlas_rect.min;
109 sprite_rect
110 }
111 };
112
113 let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
114 let sprite_center = -self.anchor.as_vec() * sprite_size;
115
116 let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;
117
118 if self.flip_x {
119 point_relative_to_sprite_center.x *= -1.0;
120 }
121 if !self.flip_y {
124 point_relative_to_sprite_center.y *= -1.0;
125 }
126
127 let sprite_to_texture_ratio = {
128 let texture_size = texture_rect.size();
129 let div_or_zero = |a, b| if b == 0.0 { 0.0 } else { a / b };
130 Vec2::new(
131 div_or_zero(texture_size.x, sprite_size.x),
132 div_or_zero(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
155#[derive(Default, Debug, Clone, Reflect, PartialEq)]
157#[reflect(Debug, Default, Clone)]
158pub enum SpriteImageMode {
159 #[default]
161 Auto,
162 Scale(ScalingMode),
165 Sliced(TextureSlicer),
167 Tiled {
169 tile_x: bool,
171 tile_y: bool,
173 stretch_value: f32,
176 },
177}
178
179impl SpriteImageMode {
180 #[inline]
182 pub fn uses_slices(&self) -> bool {
183 matches!(
184 self,
185 SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
186 )
187 }
188
189 #[inline]
191 #[must_use]
192 pub const fn scale(&self) -> Option<ScalingMode> {
193 if let SpriteImageMode::Scale(scale) = self {
194 Some(*scale)
195 } else {
196 None
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
205#[reflect(Debug, Default, Clone)]
206pub enum ScalingMode {
207 #[default]
212 FillCenter,
213 FillStart,
220 FillEnd,
227 FitCenter,
231 FitStart,
236 FitEnd,
241}
242
243#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
246#[reflect(Component, Default, Debug, PartialEq, Clone)]
247#[doc(alias = "pivot")]
248pub enum Anchor {
249 #[default]
250 Center,
251 BottomLeft,
252 BottomCenter,
253 BottomRight,
254 CenterLeft,
255 CenterRight,
256 TopLeft,
257 TopCenter,
258 TopRight,
259 Custom(Vec2),
262}
263
264impl Anchor {
265 pub fn as_vec(&self) -> Vec2 {
266 match self {
267 Anchor::Center => Vec2::ZERO,
268 Anchor::BottomLeft => Vec2::new(-0.5, -0.5),
269 Anchor::BottomCenter => Vec2::new(0.0, -0.5),
270 Anchor::BottomRight => Vec2::new(0.5, -0.5),
271 Anchor::CenterLeft => Vec2::new(-0.5, 0.0),
272 Anchor::CenterRight => Vec2::new(0.5, 0.0),
273 Anchor::TopLeft => Vec2::new(-0.5, 0.5),
274 Anchor::TopCenter => Vec2::new(0.0, 0.5),
275 Anchor::TopRight => Vec2::new(0.5, 0.5),
276 Anchor::Custom(point) => *point,
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use bevy_asset::{Assets, RenderAssetUsages};
284 use bevy_color::Color;
285 use bevy_image::Image;
286 use bevy_image::{TextureAtlas, TextureAtlasLayout};
287 use bevy_math::{Rect, URect, UVec2, Vec2};
288 use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat};
289
290 use crate::Anchor;
291
292 use super::Sprite;
293
294 fn make_image(size: UVec2) -> Image {
296 Image::new_fill(
297 Extent3d {
298 width: size.x,
299 height: size.y,
300 depth_or_array_layers: 1,
301 },
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 =
322 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
323 assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5)));
324 assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0)));
325 assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5)));
326 assert_eq!(compute(Vec2::new(3.0, 0.0)), Err(Vec2::new(5.5, 5.0)));
327 assert_eq!(compute(Vec2::new(-3.0, 0.0)), Err(Vec2::new(-0.5, 5.0)));
328 }
329
330 #[test]
331 fn compute_pixel_space_point_for_color_sprite() {
332 let image_assets = Assets::<Image>::default();
333 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
334
335 let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0));
337
338 let compute = |point| {
339 sprite
340 .compute_pixel_space_point(point, &image_assets, &texture_atlas_assets)
341 .map(|x| (x * 1e5).round() / 1e5)
343 .map_err(|x| (x * 1e5).round() / 1e5)
344 };
345 assert_eq!(compute(Vec2::new(-20.0, -40.0)), Ok(Vec2::new(0.1, 0.9)));
346 assert_eq!(compute(Vec2::new(0.0, 10.0)), Ok(Vec2::new(0.5, 0.4)));
347 assert_eq!(compute(Vec2::new(75.0, 100.0)), Err(Vec2::new(2.0, -0.5)));
348 assert_eq!(compute(Vec2::new(-75.0, -100.0)), Err(Vec2::new(-1.0, 1.5)));
349 assert_eq!(compute(Vec2::new(-30.0, -40.0)), Err(Vec2::new(-0.1, 0.9)));
350 }
351
352 #[test]
353 fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() {
354 let mut image_assets = Assets::<Image>::default();
355 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
356
357 let image = image_assets.add(make_image(UVec2::new(5, 10)));
358
359 let sprite = Sprite {
360 image,
361 anchor: Anchor::BottomLeft,
362 ..Default::default()
363 };
364
365 let compute =
366 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
367 assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5)));
368 assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
369 assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
370 assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
371 assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
372 }
373
374 #[test]
375 fn compute_pixel_space_point_for_sprite_with_anchor_top_right() {
376 let mut image_assets = Assets::<Image>::default();
377 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
378
379 let image = image_assets.add(make_image(UVec2::new(5, 10)));
380
381 let sprite = Sprite {
382 image,
383 anchor: Anchor::TopRight,
384 ..Default::default()
385 };
386
387 let compute =
388 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
389 assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5)));
390 assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
391 assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5)));
392 assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
393 assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
394 }
395
396 #[test]
397 fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() {
398 let mut image_assets = Assets::<Image>::default();
399 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
400
401 let image = image_assets.add(make_image(UVec2::new(5, 10)));
402
403 let sprite = Sprite {
404 image,
405 anchor: Anchor::BottomLeft,
406 flip_x: true,
407 ..Default::default()
408 };
409
410 let compute =
411 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
412 assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5)));
413 assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
414 assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
415 assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
416 assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
417 }
418
419 #[test]
420 fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() {
421 let mut image_assets = Assets::<Image>::default();
422 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
423
424 let image = image_assets.add(make_image(UVec2::new(5, 10)));
425
426 let sprite = Sprite {
427 image,
428 anchor: Anchor::TopRight,
429 flip_y: true,
430 ..Default::default()
431 };
432
433 let compute =
434 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
435 assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5)));
436 assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
437 assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5)));
438 assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
439 assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
440 }
441
442 #[test]
443 fn compute_pixel_space_point_for_sprite_with_rect() {
444 let mut image_assets = Assets::<Image>::default();
445 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
446
447 let image = image_assets.add(make_image(UVec2::new(5, 10)));
448
449 let sprite = Sprite {
450 image,
451 rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)),
452 anchor: Anchor::BottomLeft,
453 ..Default::default()
454 };
455
456 let compute =
457 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
458 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0)));
459 assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0)));
461 }
462
463 #[test]
464 fn compute_pixel_space_point_for_texture_atlas_sprite() {
465 let mut image_assets = Assets::<Image>::default();
466 let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
467
468 let image = image_assets.add(make_image(UVec2::new(5, 10)));
469 let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
470 size: UVec2::new(5, 10),
471 textures: vec![URect::new(1, 1, 4, 4)],
472 });
473
474 let sprite = Sprite {
475 image,
476 anchor: Anchor::BottomLeft,
477 texture_atlas: Some(TextureAtlas {
478 layout: texture_atlas,
479 index: 0,
480 }),
481 ..Default::default()
482 };
483
484 let compute =
485 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
486 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5)));
487 assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5)));
489 }
490
491 #[test]
492 fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() {
493 let mut image_assets = Assets::<Image>::default();
494 let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
495
496 let image = image_assets.add(make_image(UVec2::new(5, 10)));
497 let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
498 size: UVec2::new(5, 10),
499 textures: vec![URect::new(1, 1, 4, 4)],
500 });
501
502 let sprite = Sprite {
503 image,
504 anchor: Anchor::BottomLeft,
505 texture_atlas: Some(TextureAtlas {
506 layout: texture_atlas,
507 index: 0,
508 }),
509 rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)),
511 ..Default::default()
512 };
513
514 let compute =
515 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
516 assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5)));
517 assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5)));
519 }
520
521 #[test]
522 fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() {
523 let mut image_assets = Assets::<Image>::default();
524 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
525
526 let image = image_assets.add(make_image(UVec2::new(5, 10)));
527
528 let sprite = Sprite {
529 image,
530 custom_size: Some(Vec2::new(100.0, 50.0)),
531 rect: Some(Rect::new(0.0, 0.0, 5.0, 5.0)),
532 ..Default::default()
533 };
534
535 let compute =
536 |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
537 assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0)));
538 assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0)));
539 assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0)));
541 }
542}