sprite_scale/
sprite_scale.rs

1//! Shows how to use sprite scaling to fill and fit textures into the sprite.
2
3use bevy::prelude::*;
4
5fn main() {
6    App::new()
7        .add_plugins(DefaultPlugins)
8        .add_systems(
9            Startup,
10            (setup_sprites, setup_texture_atlas).after(setup_camera),
11        )
12        .add_systems(Update, animate_sprite)
13        .run();
14}
15
16fn setup_camera(mut commands: Commands) {
17    commands.spawn(Camera2d);
18}
19
20fn setup_sprites(mut commands: Commands, asset_server: Res<AssetServer>) {
21    let square = asset_server.load("textures/slice_square_2.png");
22    let banner = asset_server.load("branding/banner.png");
23
24    let rects = [
25        Rect {
26            size: Vec2::new(100., 225.),
27            text: "Stretched".to_string(),
28            transform: Transform::from_translation(Vec3::new(-570., 230., 0.)),
29            texture: square.clone(),
30            image_mode: SpriteImageMode::Auto,
31        },
32        Rect {
33            size: Vec2::new(100., 225.),
34            text: "Fill Center".to_string(),
35            transform: Transform::from_translation(Vec3::new(-450., 230., 0.)),
36            texture: square.clone(),
37            image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
38        },
39        Rect {
40            size: Vec2::new(100., 225.),
41            text: "Fill Start".to_string(),
42            transform: Transform::from_translation(Vec3::new(-330., 230., 0.)),
43            texture: square.clone(),
44            image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
45        },
46        Rect {
47            size: Vec2::new(100., 225.),
48            text: "Fill End".to_string(),
49            transform: Transform::from_translation(Vec3::new(-210., 230., 0.)),
50            texture: square.clone(),
51            image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
52        },
53        Rect {
54            size: Vec2::new(300., 100.),
55            text: "Fill Start Horizontal".to_string(),
56            transform: Transform::from_translation(Vec3::new(10., 290., 0.)),
57            texture: square.clone(),
58            image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
59        },
60        Rect {
61            size: Vec2::new(300., 100.),
62            text: "Fill End Horizontal".to_string(),
63            transform: Transform::from_translation(Vec3::new(10., 155., 0.)),
64            texture: square.clone(),
65            image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
66        },
67        Rect {
68            size: Vec2::new(200., 200.),
69            text: "Fill Center".to_string(),
70            transform: Transform::from_translation(Vec3::new(280., 230., 0.)),
71            texture: banner.clone(),
72            image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
73        },
74        Rect {
75            size: Vec2::new(200., 100.),
76            text: "Fill Center".to_string(),
77            transform: Transform::from_translation(Vec3::new(500., 230., 0.)),
78            texture: square.clone(),
79            image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
80        },
81        Rect {
82            size: Vec2::new(100., 100.),
83            text: "Stretched".to_string(),
84            transform: Transform::from_translation(Vec3::new(-570., -40., 0.)),
85            texture: banner.clone(),
86            image_mode: SpriteImageMode::Auto,
87        },
88        Rect {
89            size: Vec2::new(200., 200.),
90            text: "Fit Center".to_string(),
91            transform: Transform::from_translation(Vec3::new(-400., -40., 0.)),
92            texture: banner.clone(),
93            image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
94        },
95        Rect {
96            size: Vec2::new(200., 200.),
97            text: "Fit Start".to_string(),
98            transform: Transform::from_translation(Vec3::new(-180., -40., 0.)),
99            texture: banner.clone(),
100            image_mode: SpriteImageMode::Scale(ScalingMode::FitStart),
101        },
102        Rect {
103            size: Vec2::new(200., 200.),
104            text: "Fit End".to_string(),
105            transform: Transform::from_translation(Vec3::new(40., -40., 0.)),
106            texture: banner.clone(),
107            image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd),
108        },
109        Rect {
110            size: Vec2::new(100., 200.),
111            text: "Fit Center".to_string(),
112            transform: Transform::from_translation(Vec3::new(210., -40., 0.)),
113            texture: banner.clone(),
114            image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
115        },
116    ];
117
118    for rect in rects {
119        let mut cmd = commands.spawn((
120            Sprite {
121                image: rect.texture,
122                custom_size: Some(rect.size),
123                image_mode: rect.image_mode,
124                ..default()
125            },
126            rect.transform,
127        ));
128
129        cmd.with_children(|builder| {
130            builder.spawn((
131                Text2d::new(rect.text),
132                TextLayout::new_with_justify(JustifyText::Center),
133                TextFont::from_font_size(15.),
134                Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.),
135                bevy::sprite::Anchor::TopCenter,
136            ));
137        });
138    }
139}
140
141fn setup_texture_atlas(
142    mut commands: Commands,
143    asset_server: Res<AssetServer>,
144    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
145) {
146    commands.spawn(Camera2d);
147    let gabe = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
148    let animation_indices_gabe = AnimationIndices { first: 0, last: 6 };
149    let gabe_atlas = TextureAtlas {
150        layout: texture_atlas_layouts.add(TextureAtlasLayout::from_grid(
151            UVec2::splat(24),
152            7,
153            1,
154            None,
155            None,
156        )),
157        index: animation_indices_gabe.first,
158    };
159
160    let sprite_sheets = [
161        SpriteSheet {
162            size: Vec2::new(120., 50.),
163            text: "Stretched".to_string(),
164            transform: Transform::from_translation(Vec3::new(-570., -200., 0.)),
165            texture: gabe.clone(),
166            image_mode: SpriteImageMode::Auto,
167            atlas: gabe_atlas.clone(),
168            indices: animation_indices_gabe.clone(),
169            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
170        },
171        SpriteSheet {
172            size: Vec2::new(120., 50.),
173            text: "Fill Center".to_string(),
174            transform: Transform::from_translation(Vec3::new(-570., -300., 0.)),
175            texture: gabe.clone(),
176            image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
177            atlas: gabe_atlas.clone(),
178            indices: animation_indices_gabe.clone(),
179            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
180        },
181        SpriteSheet {
182            size: Vec2::new(120., 50.),
183            text: "Fill Start".to_string(),
184            transform: Transform::from_translation(Vec3::new(-430., -200., 0.)),
185            texture: gabe.clone(),
186            image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
187            atlas: gabe_atlas.clone(),
188            indices: animation_indices_gabe.clone(),
189            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
190        },
191        SpriteSheet {
192            size: Vec2::new(120., 50.),
193            text: "Fill End".to_string(),
194            transform: Transform::from_translation(Vec3::new(-430., -300., 0.)),
195            texture: gabe.clone(),
196            image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
197            atlas: gabe_atlas.clone(),
198            indices: animation_indices_gabe.clone(),
199            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
200        },
201        SpriteSheet {
202            size: Vec2::new(50., 120.),
203            text: "Fill Center".to_string(),
204            transform: Transform::from_translation(Vec3::new(-300., -250., 0.)),
205            texture: gabe.clone(),
206            image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
207            atlas: gabe_atlas.clone(),
208            indices: animation_indices_gabe.clone(),
209            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
210        },
211        SpriteSheet {
212            size: Vec2::new(50., 120.),
213            text: "Fill Start".to_string(),
214            transform: Transform::from_translation(Vec3::new(-190., -250., 0.)),
215            texture: gabe.clone(),
216            image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
217            atlas: gabe_atlas.clone(),
218            indices: animation_indices_gabe.clone(),
219            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
220        },
221        SpriteSheet {
222            size: Vec2::new(50., 120.),
223            text: "Fill End".to_string(),
224            transform: Transform::from_translation(Vec3::new(-90., -250., 0.)),
225            texture: gabe.clone(),
226            image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
227            atlas: gabe_atlas.clone(),
228            indices: animation_indices_gabe.clone(),
229            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
230        },
231        SpriteSheet {
232            size: Vec2::new(120., 50.),
233            text: "Fit Center".to_string(),
234            transform: Transform::from_translation(Vec3::new(20., -200., 0.)),
235            texture: gabe.clone(),
236            image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
237            atlas: gabe_atlas.clone(),
238            indices: animation_indices_gabe.clone(),
239            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
240        },
241        SpriteSheet {
242            size: Vec2::new(120., 50.),
243            text: "Fit Start".to_string(),
244            transform: Transform::from_translation(Vec3::new(20., -300., 0.)),
245            texture: gabe.clone(),
246            image_mode: SpriteImageMode::Scale(ScalingMode::FitStart),
247            atlas: gabe_atlas.clone(),
248            indices: animation_indices_gabe.clone(),
249            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
250        },
251        SpriteSheet {
252            size: Vec2::new(120., 50.),
253            text: "Fit End".to_string(),
254            transform: Transform::from_translation(Vec3::new(160., -200., 0.)),
255            texture: gabe.clone(),
256            image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd),
257            atlas: gabe_atlas.clone(),
258            indices: animation_indices_gabe.clone(),
259            timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
260        },
261    ];
262
263    for sprite_sheet in sprite_sheets {
264        let mut cmd = commands.spawn((
265            Sprite {
266                image_mode: sprite_sheet.image_mode,
267                custom_size: Some(sprite_sheet.size),
268                ..Sprite::from_atlas_image(sprite_sheet.texture.clone(), sprite_sheet.atlas.clone())
269            },
270            sprite_sheet.indices,
271            sprite_sheet.timer,
272            sprite_sheet.transform,
273        ));
274
275        cmd.with_children(|builder| {
276            builder.spawn((
277                Text2d::new(sprite_sheet.text),
278                TextLayout::new_with_justify(JustifyText::Center),
279                TextFont::from_font_size(15.),
280                Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.),
281                bevy::sprite::Anchor::TopCenter,
282            ));
283        });
284    }
285}
286
287struct Rect {
288    size: Vec2,
289    text: String,
290    transform: Transform,
291    texture: Handle<Image>,
292    image_mode: SpriteImageMode,
293}
294
295struct SpriteSheet {
296    size: Vec2,
297    text: String,
298    transform: Transform,
299    texture: Handle<Image>,
300    image_mode: SpriteImageMode,
301    atlas: TextureAtlas,
302    indices: AnimationIndices,
303    timer: AnimationTimer,
304}
305
306#[derive(Component, Clone)]
307struct AnimationIndices {
308    first: usize,
309    last: usize,
310}
311
312#[derive(Component, Deref, DerefMut)]
313struct AnimationTimer(Timer);
314
315fn animate_sprite(
316    time: Res<Time>,
317    mut query: Query<(&AnimationIndices, &mut AnimationTimer, &mut Sprite)>,
318) {
319    for (indices, mut timer, mut sprite) in &mut query {
320        timer.tick(time.delta());
321
322        if timer.just_finished() {
323            if let Some(atlas) = &mut sprite.texture_atlas {
324                atlas.index = if atlas.index == indices.last {
325                    indices.first
326                } else {
327                    atlas.index + 1
328                };
329            }
330        }
331    }
332}