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