bevy_sprite/
sprite.rs

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/// Describes a sprite to be rendered to a 2D camera
14#[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    /// The image used to render the sprite
20    pub image: Handle<Image>,
21    /// The (optional) texture atlas used to render the sprite
22    pub texture_atlas: Option<TextureAtlas>,
23    /// The sprite's color tint
24    pub color: Color,
25    /// Flip the sprite along the `X` axis
26    pub flip_x: bool,
27    /// Flip the sprite along the `Y` axis
28    pub flip_y: bool,
29    /// An optional custom size for the sprite that will be used when rendering, instead of the size
30    /// of the sprite's image
31    pub custom_size: Option<Vec2>,
32    /// An optional rectangle representing the region of the sprite's image to render, instead of rendering
33    /// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
34    ///
35    /// When used with a [`TextureAtlas`], the rect
36    /// is offset by the atlas's minimal (top-left) corner position.
37    pub rect: Option<Rect>,
38    /// How the sprite's image will be scaled.
39    pub image_mode: SpriteImageMode,
40}
41
42impl Sprite {
43    /// Create a Sprite with a custom size
44    pub fn sized(custom_size: Vec2) -> Self {
45        Sprite {
46            custom_size: Some(custom_size),
47            ..Default::default()
48        }
49    }
50
51    /// Create a sprite from an image
52    pub fn from_image(image: Handle<Image>) -> Self {
53        Self {
54            image,
55            ..Default::default()
56        }
57    }
58
59    /// Create a sprite from an image, with an associated texture atlas
60    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    /// Create a sprite from a solid color
69    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    /// Computes the pixel point where `point_relative_to_sprite` is sampled
78    /// from in this sprite. `point_relative_to_sprite` must be in the sprite's
79    /// local frame. Returns an Ok if the point is inside the bounds of the
80    /// sprite (not just the image), and returns an Err otherwise.
81    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                // Make the sprite rect relative to the atlas rect.
104                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        // Texture coordinates start at the top left, whereas world coordinates start at the bottom
119        // left. So flip by default, and then don't flip if `flip_y` is set.
120        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        // TODO: Support `SpriteImageMode`.
140
141        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/// Controls how the image is altered when scaled.
164#[derive(Default, Debug, Clone, Reflect, PartialEq)]
165#[reflect(Debug, Default, Clone)]
166pub enum SpriteImageMode {
167    /// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
168    #[default]
169    Auto,
170    /// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`].
171    /// Otherwise no scaling will be applied.
172    Scale(ScalingMode),
173    /// The texture will be cut in 9 slices, keeping the texture in proportions on resize
174    Sliced(TextureSlicer),
175    /// The texture will be repeated if stretched beyond `stretched_value`
176    Tiled {
177        /// Should the image repeat horizontally
178        tile_x: bool,
179        /// Should the image repeat vertically
180        tile_y: bool,
181        /// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
182        /// *original texture size* are above this value.
183        stretch_value: f32,
184    },
185}
186
187impl SpriteImageMode {
188    /// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
189    #[inline]
190    pub fn uses_slices(&self) -> bool {
191        matches!(
192            self,
193            SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
194        )
195    }
196
197    /// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise.
198    #[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/// Represents various modes for proportional scaling of a texture.
210///
211/// Can be used in [`SpriteImageMode::Scale`].
212#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
213#[reflect(Debug, Default, Clone)]
214pub enum ScalingMode {
215    /// Scale the texture uniformly (maintain the texture's aspect ratio)
216    /// so that both dimensions (width and height) of the texture will be equal
217    /// to or larger than the corresponding dimension of the target rectangle.
218    /// Fill sprite with a centered texture.
219    #[default]
220    FillCenter,
221    /// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
222    /// One dimension of the texture will match the rectangle's size,
223    /// while the other dimension may exceed it.
224    /// The exceeding portion is aligned to the start:
225    /// * Horizontal overflow is left-aligned if the width exceeds the rectangle.
226    /// * Vertical overflow is top-aligned if the height exceeds the rectangle.
227    FillStart,
228    /// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
229    /// One dimension of the texture will match the rectangle's size,
230    /// while the other dimension may exceed it.
231    /// The exceeding portion is aligned to the end:
232    /// * Horizontal overflow is right-aligned if the width exceeds the rectangle.
233    /// * Vertical overflow is bottom-aligned if the height exceeds the rectangle.
234    FillEnd,
235    /// Scaling the texture will maintain the original aspect ratio
236    /// and ensure that the original texture fits entirely inside the rect.
237    /// At least one axis (x or y) will fit exactly. The result is centered inside the rect.
238    FitCenter,
239    /// Scaling the texture will maintain the original aspect ratio
240    /// and ensure that the original texture fits entirely inside rect.
241    /// At least one axis (x or y) will fit exactly.
242    /// Aligns the result to the left and top edges of rect.
243    FitStart,
244    /// Scaling the texture will maintain the original aspect ratio
245    /// and ensure that the original texture fits entirely inside rect.
246    /// At least one axis (x or y) will fit exactly.
247    /// Aligns the result to the right and bottom edges of rect.
248    FitEnd,
249}
250
251/// Normalized (relative to its size) offset of a 2d renderable entity from its [`Transform`].
252#[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    /// Makes a new image of the specified size.
299    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        // This also tests the `custom_size` field.
342        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                // Round to remove floating point errors.
353                .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        // The pixel is outside the rect, but is still a valid pixel in the image.
476        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        // The pixel is outside the texture atlas, but is still a valid pixel in the image.
505        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            // The rect is relative to the texture atlas sprite.
526            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        // The pixel is outside the texture atlas, but is still a valid pixel in the image.
536        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        // The pixel is outside the texture atlas, but is still a valid pixel in the image.
564        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        // The pixel is outside the texture atlas, but is still a valid pixel in the image.
594        assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(0.0, -35.0)));
595    }
596}