Proportional scaling for the sprite's texture. (#17258)
# Objective Bevy sprite image mode lacks proportional scaling for the underlying texture. In many cases, it's required. For example, if it is desired to support a wide variety of screens with a single texture, it's okay to cut off some portion of the original texture. ## Solution I added scaling of the texture during the preparation step. To fill the sprite with the original texture, I scaled UV coordinates accordingly to the sprite size aspect ratio and texture size aspect ratio. To fit texture in a sprite the original `quad` is scaled and then the additional translation is applied to place the scaled quad properly. ## Testing For testing purposes could be used `2d/sprite_scale.rs`. Also, I am thinking that it would be nice to have some tests for a `crates/bevy_sprite/src/render/mod.rs:sprite_scale`. --- ## Showcase <img width="1392" alt="image" src="https://github.com/user-attachments/assets/c2c37b96-2493-4717-825f-7810d921b4bc" />
This commit is contained in:
parent
39a1e2b488
commit
deb135c25c
11
Cargo.toml
11
Cargo.toml
@ -663,6 +663,17 @@ description = "Animates a sprite in response to an event"
|
||||
category = "2D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "sprite_scale"
|
||||
path = "examples/2d/sprite_scale.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.sprite_scale]
|
||||
name = "Sprite Scale"
|
||||
description = "Shows how a sprite can be scaled into a rectangle while keeping the aspect ratio"
|
||||
category = "2D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "sprite_flipping"
|
||||
path = "examples/2d/sprite_flipping.rs"
|
||||
|
@ -25,7 +25,7 @@ pub mod prelude {
|
||||
pub use crate::{
|
||||
sprite::{Sprite, SpriteImageMode},
|
||||
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
|
||||
ColorMaterial, MeshMaterial2d,
|
||||
ColorMaterial, MeshMaterial2d, ScalingMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use core::ops::Range;
|
||||
|
||||
use crate::{ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE};
|
||||
use crate::{ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE};
|
||||
use bevy_asset::{AssetEvent, AssetId, Assets};
|
||||
use bevy_color::{ColorToComponents, LinearRgba};
|
||||
use bevy_core_pipeline::{
|
||||
@ -339,6 +339,7 @@ pub struct ExtractedSprite {
|
||||
/// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the
|
||||
/// entity that caused that creation for use in determining visibility.
|
||||
pub original_entity: Option<Entity>,
|
||||
pub scaling_mode: Option<ScalingMode>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
@ -430,6 +431,7 @@ pub fn extract_sprites(
|
||||
image_handle_id: sprite.image.id(),
|
||||
anchor: sprite.anchor.as_vec(),
|
||||
original_entity: Some(original_entity),
|
||||
scaling_mode: sprite.image_mode.scale(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -700,21 +702,43 @@ pub fn prepare_sprite_image_bind_groups(
|
||||
// By default, the size of the quad is the size of the texture
|
||||
let mut quad_size = batch_image_size;
|
||||
|
||||
// Calculate vertex data for this item
|
||||
let mut uv_offset_scale: Vec4;
|
||||
// Texture size is the size of the image
|
||||
let mut texture_size = batch_image_size;
|
||||
|
||||
// If a rect is specified, adjust UVs and the size of the quad
|
||||
if let Some(rect) = extracted_sprite.rect {
|
||||
let mut uv_offset_scale = if let Some(rect) = extracted_sprite.rect {
|
||||
let rect_size = rect.size();
|
||||
uv_offset_scale = Vec4::new(
|
||||
quad_size = rect_size;
|
||||
// Update texture size to the rect size
|
||||
// It will help scale properly only portion of the image
|
||||
texture_size = rect_size;
|
||||
Vec4::new(
|
||||
rect.min.x / batch_image_size.x,
|
||||
rect.max.y / batch_image_size.y,
|
||||
rect_size.x / batch_image_size.x,
|
||||
-rect_size.y / batch_image_size.y,
|
||||
);
|
||||
quad_size = rect_size;
|
||||
)
|
||||
} else {
|
||||
uv_offset_scale = Vec4::new(0.0, 1.0, 1.0, -1.0);
|
||||
Vec4::new(0.0, 1.0, 1.0, -1.0)
|
||||
};
|
||||
|
||||
// Override the size if a custom one is specified
|
||||
if let Some(custom_size) = extracted_sprite.custom_size {
|
||||
quad_size = custom_size;
|
||||
}
|
||||
|
||||
// Used for translation of the quad if `TextureScale::Fit...` is specified.
|
||||
let mut quad_translation = Vec2::ZERO;
|
||||
|
||||
// Scales the texture based on the `texture_scale` field.
|
||||
if let Some(scaling_mode) = extracted_sprite.scaling_mode {
|
||||
apply_scaling(
|
||||
scaling_mode,
|
||||
texture_size,
|
||||
&mut quad_size,
|
||||
&mut quad_translation,
|
||||
&mut uv_offset_scale,
|
||||
);
|
||||
}
|
||||
|
||||
if extracted_sprite.flip_x {
|
||||
@ -726,15 +750,13 @@ pub fn prepare_sprite_image_bind_groups(
|
||||
uv_offset_scale.w *= -1.0;
|
||||
}
|
||||
|
||||
// Override the size if a custom one is specified
|
||||
if let Some(custom_size) = extracted_sprite.custom_size {
|
||||
quad_size = custom_size;
|
||||
}
|
||||
let transform = extracted_sprite.transform.affine()
|
||||
* Affine3A::from_scale_rotation_translation(
|
||||
quad_size.extend(1.0),
|
||||
Quat::IDENTITY,
|
||||
(quad_size * (-extracted_sprite.anchor - Vec2::splat(0.5))).extend(0.0),
|
||||
((quad_size + quad_translation)
|
||||
* (-extracted_sprite.anchor - Vec2::splat(0.5)))
|
||||
.extend(0.0),
|
||||
);
|
||||
|
||||
// Store the vertex data and add the item to the render phase
|
||||
@ -875,3 +897,89 @@ impl<P: PhaseItem> RenderCommand<P> for DrawSpriteBatch {
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
/// Scales a texture to fit within a given quad size with keeping the aspect ratio.
|
||||
fn apply_scaling(
|
||||
scaling_mode: ScalingMode,
|
||||
texture_size: Vec2,
|
||||
quad_size: &mut Vec2,
|
||||
quad_translation: &mut Vec2,
|
||||
uv_offset_scale: &mut Vec4,
|
||||
) {
|
||||
let quad_ratio = quad_size.x / quad_size.y;
|
||||
let texture_ratio = texture_size.x / texture_size.y;
|
||||
let tex_quad_scale = texture_ratio / quad_ratio;
|
||||
let quad_tex_scale = quad_ratio / texture_ratio;
|
||||
|
||||
match scaling_mode {
|
||||
ScalingMode::FillCenter => {
|
||||
if quad_ratio > texture_ratio {
|
||||
// offset texture to center by y coordinate
|
||||
uv_offset_scale.y += (uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale) * 0.5;
|
||||
// sum up scales
|
||||
uv_offset_scale.w *= tex_quad_scale;
|
||||
} else {
|
||||
// offset texture to center by x coordinate
|
||||
uv_offset_scale.x += (uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale) * 0.5;
|
||||
uv_offset_scale.z *= quad_tex_scale;
|
||||
};
|
||||
}
|
||||
ScalingMode::FillStart => {
|
||||
if quad_ratio > texture_ratio {
|
||||
uv_offset_scale.y += uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale;
|
||||
uv_offset_scale.w *= tex_quad_scale;
|
||||
} else {
|
||||
uv_offset_scale.z *= quad_tex_scale;
|
||||
}
|
||||
}
|
||||
ScalingMode::FillEnd => {
|
||||
if quad_ratio > texture_ratio {
|
||||
uv_offset_scale.w *= tex_quad_scale;
|
||||
} else {
|
||||
uv_offset_scale.x += uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale;
|
||||
uv_offset_scale.z *= quad_tex_scale;
|
||||
}
|
||||
}
|
||||
ScalingMode::FitCenter => {
|
||||
if texture_ratio > quad_ratio {
|
||||
// Scale based on width
|
||||
quad_size.y *= quad_tex_scale;
|
||||
} else {
|
||||
// Scale based on height
|
||||
quad_size.x *= tex_quad_scale;
|
||||
}
|
||||
}
|
||||
ScalingMode::FitStart => {
|
||||
if texture_ratio > quad_ratio {
|
||||
// The quad is scaled to match the image ratio, and the quad translation is adjusted
|
||||
// to start of the quad within the original quad size.
|
||||
let scale = Vec2::new(1.0, quad_tex_scale);
|
||||
let new_quad = *quad_size * scale;
|
||||
let offset = *quad_size - new_quad;
|
||||
*quad_translation = Vec2::new(0.0, -offset.y);
|
||||
*quad_size = new_quad;
|
||||
} else {
|
||||
let scale = Vec2::new(tex_quad_scale, 1.0);
|
||||
let new_quad = *quad_size * scale;
|
||||
let offset = *quad_size - new_quad;
|
||||
*quad_translation = Vec2::new(offset.x, 0.0);
|
||||
*quad_size = new_quad;
|
||||
}
|
||||
}
|
||||
ScalingMode::FitEnd => {
|
||||
if texture_ratio > quad_ratio {
|
||||
let scale = Vec2::new(1.0, quad_tex_scale);
|
||||
let new_quad = *quad_size * scale;
|
||||
let offset = *quad_size - new_quad;
|
||||
*quad_translation = Vec2::new(0.0, offset.y);
|
||||
*quad_size = new_quad;
|
||||
} else {
|
||||
let scale = Vec2::new(tex_quad_scale, 1.0);
|
||||
let new_quad = *quad_size * scale;
|
||||
let offset = *quad_size - new_quad;
|
||||
*quad_translation = Vec2::new(-offset.x, 0.0);
|
||||
*quad_size = new_quad;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +162,9 @@ pub enum SpriteImageMode {
|
||||
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
|
||||
#[default]
|
||||
Auto,
|
||||
/// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`].
|
||||
/// Otherwise no scaling will be applied.
|
||||
Scale(ScalingMode),
|
||||
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
|
||||
Sliced(TextureSlicer),
|
||||
/// The texture will be repeated if stretched beyond `stretched_value`
|
||||
@ -185,6 +188,59 @@ impl SpriteImageMode {
|
||||
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn scale(&self) -> Option<ScalingMode> {
|
||||
if let SpriteImageMode::Scale(scale) = self {
|
||||
Some(*scale)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents various modes for proportional scaling of a texture.
|
||||
///
|
||||
/// Can be used in [`SpriteImageMode::Scale`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
|
||||
#[reflect(Debug)]
|
||||
pub enum ScalingMode {
|
||||
/// Scale the texture uniformly (maintain the texture's aspect ratio)
|
||||
/// so that both dimensions (width and height) of the texture will be equal
|
||||
/// to or larger than the corresponding dimension of the target rectangle.
|
||||
/// Fill sprite with a centered texture.
|
||||
#[default]
|
||||
FillCenter,
|
||||
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
|
||||
/// One dimension of the texture will match the rectangle's size,
|
||||
/// while the other dimension may exceed it.
|
||||
/// The exceeding portion is aligned to the start:
|
||||
/// * Horizontal overflow is left-aligned if the width exceeds the rectangle.
|
||||
/// * Vertical overflow is top-aligned if the height exceeds the rectangle.
|
||||
FillStart,
|
||||
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
|
||||
/// One dimension of the texture will match the rectangle's size,
|
||||
/// while the other dimension may exceed it.
|
||||
/// The exceeding portion is aligned to the end:
|
||||
/// * Horizontal overflow is right-aligned if the width exceeds the rectangle.
|
||||
/// * Vertical overflow is bottom-aligned if the height exceeds the rectangle.
|
||||
FillEnd,
|
||||
/// Scaling the texture will maintain the original aspect ratio
|
||||
/// and ensure that the original texture fits entirely inside the rect.
|
||||
/// At least one axis (x or y) will fit exactly. The result is centered inside the rect.
|
||||
FitCenter,
|
||||
/// Scaling the texture will maintain the original aspect ratio
|
||||
/// and ensure that the original texture fits entirely inside rect.
|
||||
/// At least one axis (x or y) will fit exactly.
|
||||
/// Aligns the result to the left and top edges of rect.
|
||||
FitStart,
|
||||
/// Scaling the texture will maintain the original aspect ratio
|
||||
/// and ensure that the original texture fits entirely inside rect.
|
||||
/// At least one axis (x or y) will fit exactly.
|
||||
/// Aligns the result to the right and bottom edges of rect.
|
||||
FitEnd,
|
||||
}
|
||||
|
||||
/// How a sprite is positioned relative to its [`Transform`].
|
||||
|
@ -53,6 +53,7 @@ impl ComputedTextureSlices {
|
||||
flip_y,
|
||||
image_handle_id: sprite.image.id(),
|
||||
anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice),
|
||||
scaling_mode: sprite.image_mode.scale(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -123,6 +124,9 @@ fn compute_sprite_slices(
|
||||
SpriteImageMode::Auto => {
|
||||
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
|
||||
}
|
||||
SpriteImageMode::Scale(_) => {
|
||||
unreachable!("Slices should not be computed for SpriteImageMode::Scale")
|
||||
}
|
||||
};
|
||||
Some(ComputedTextureSlices(slices))
|
||||
}
|
||||
|
@ -213,6 +213,7 @@ pub fn extract_text2d_sprite(
|
||||
flip_y: false,
|
||||
anchor: Anchor::Center.as_vec(),
|
||||
original_entity: Some(original_entity),
|
||||
scaling_mode: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -814,7 +814,10 @@ fn compute_texture_slices(
|
||||
[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]
|
||||
}
|
||||
SpriteImageMode::Auto => {
|
||||
unreachable!("Slices should not be computed for ImageScaleMode::Stretch")
|
||||
unreachable!("Slices can not be computed for SpriteImageMode::Stretch")
|
||||
}
|
||||
SpriteImageMode::Scale(_) => {
|
||||
unreachable!("Slices can not be computed for SpriteImageMode::Scale")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
332
examples/2d/sprite_scale.rs
Normal file
332
examples/2d/sprite_scale.rs
Normal file
@ -0,0 +1,332 @@
|
||||
//! Shows how to use sprite scaling to fill and fit textures into the sprite.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(
|
||||
Startup,
|
||||
(setup_sprites, setup_texture_atlas).after(setup_camera),
|
||||
)
|
||||
.add_systems(Update, animate_sprite)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup_camera(mut commands: Commands) {
|
||||
commands.spawn(Camera2d);
|
||||
}
|
||||
|
||||
fn setup_sprites(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
let square = asset_server.load("textures/slice_square_2.png");
|
||||
let banner = asset_server.load("branding/banner.png");
|
||||
|
||||
let rects = [
|
||||
Rect {
|
||||
size: Vec2::new(100., 225.),
|
||||
text: "Stretched".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-570., 230., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Auto,
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(100., 225.),
|
||||
text: "Fill Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-450., 230., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(100., 225.),
|
||||
text: "Fill Start".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-330., 230., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(100., 225.),
|
||||
text: "Fill End".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-210., 230., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(300., 100.),
|
||||
text: "Fill Start Horizontal".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(10., 290., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(300., 100.),
|
||||
text: "Fill End Horizontal".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(10., 155., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(200., 200.),
|
||||
text: "Fill Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(280., 230., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(200., 100.),
|
||||
text: "Fill Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(500., 230., 0.)),
|
||||
texture: square.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(100., 100.),
|
||||
text: "Stretched".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-570., -40., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Auto,
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(200., 200.),
|
||||
text: "Fit Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-400., -40., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(200., 200.),
|
||||
text: "Fit Start".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-180., -40., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitStart),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(200., 200.),
|
||||
text: "Fit End".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(40., -40., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd),
|
||||
},
|
||||
Rect {
|
||||
size: Vec2::new(100., 200.),
|
||||
text: "Fit Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(210., -40., 0.)),
|
||||
texture: banner.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
|
||||
},
|
||||
];
|
||||
|
||||
for rect in rects {
|
||||
let mut cmd = commands.spawn((
|
||||
Sprite {
|
||||
image: rect.texture,
|
||||
custom_size: Some(rect.size),
|
||||
image_mode: rect.image_mode,
|
||||
..default()
|
||||
},
|
||||
rect.transform,
|
||||
));
|
||||
|
||||
cmd.with_children(|builder| {
|
||||
builder.spawn((
|
||||
Text2d::new(rect.text),
|
||||
TextLayout::new_with_justify(JustifyText::Center),
|
||||
TextFont::from_font_size(15.),
|
||||
Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.),
|
||||
bevy::sprite::Anchor::TopCenter,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_texture_atlas(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||
) {
|
||||
commands.spawn(Camera2d);
|
||||
let gabe = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
|
||||
let animation_indices_gabe = AnimationIndices { first: 0, last: 6 };
|
||||
let gabe_atlas = TextureAtlas {
|
||||
layout: texture_atlas_layouts.add(TextureAtlasLayout::from_grid(
|
||||
UVec2::splat(24),
|
||||
7,
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
)),
|
||||
index: animation_indices_gabe.first,
|
||||
};
|
||||
|
||||
let sprite_sheets = [
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Stretched".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-570., -200., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Auto,
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fill Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-570., -300., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fill Start".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-430., -200., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fill End".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-430., -300., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(50., 120.),
|
||||
text: "Fill Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-300., -250., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(50., 120.),
|
||||
text: "Fill Start".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-190., -250., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillStart),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(50., 120.),
|
||||
text: "Fill End".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(-90., -250., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fit Center".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(20., -200., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fit Start".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(20., -300., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitStart),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
SpriteSheet {
|
||||
size: Vec2::new(120., 50.),
|
||||
text: "Fit End".to_string(),
|
||||
transform: Transform::from_translation(Vec3::new(160., -200., 0.)),
|
||||
texture: gabe.clone(),
|
||||
image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd),
|
||||
atlas: gabe_atlas.clone(),
|
||||
indices: animation_indices_gabe.clone(),
|
||||
timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||
},
|
||||
];
|
||||
|
||||
for sprite_sheet in sprite_sheets {
|
||||
let mut cmd = commands.spawn((
|
||||
Sprite {
|
||||
image_mode: sprite_sheet.image_mode,
|
||||
custom_size: Some(sprite_sheet.size),
|
||||
..Sprite::from_atlas_image(sprite_sheet.texture.clone(), sprite_sheet.atlas.clone())
|
||||
},
|
||||
sprite_sheet.indices,
|
||||
sprite_sheet.timer,
|
||||
sprite_sheet.transform,
|
||||
));
|
||||
|
||||
cmd.with_children(|builder| {
|
||||
builder.spawn((
|
||||
Text2d::new(sprite_sheet.text),
|
||||
TextLayout::new_with_justify(JustifyText::Center),
|
||||
TextFont::from_font_size(15.),
|
||||
Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.),
|
||||
bevy::sprite::Anchor::TopCenter,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Rect {
|
||||
size: Vec2,
|
||||
text: String,
|
||||
transform: Transform,
|
||||
texture: Handle<Image>,
|
||||
image_mode: SpriteImageMode,
|
||||
}
|
||||
|
||||
struct SpriteSheet {
|
||||
size: Vec2,
|
||||
text: String,
|
||||
transform: Transform,
|
||||
texture: Handle<Image>,
|
||||
image_mode: SpriteImageMode,
|
||||
atlas: TextureAtlas,
|
||||
indices: AnimationIndices,
|
||||
timer: AnimationTimer,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
struct AnimationIndices {
|
||||
first: usize,
|
||||
last: usize,
|
||||
}
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
struct AnimationTimer(Timer);
|
||||
|
||||
fn animate_sprite(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(&AnimationIndices, &mut AnimationTimer, &mut Sprite)>,
|
||||
) {
|
||||
for (indices, mut timer, mut sprite) in &mut query {
|
||||
timer.tick(time.delta());
|
||||
|
||||
if timer.just_finished() {
|
||||
if let Some(atlas) = &mut sprite.texture_atlas {
|
||||
atlas.index = if atlas.index == indices.last {
|
||||
indices.first
|
||||
} else {
|
||||
atlas.index + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -120,6 +120,7 @@ Example | Description
|
||||
[Sprite](../examples/2d/sprite.rs) | Renders a sprite
|
||||
[Sprite Animation](../examples/2d/sprite_animation.rs) | Animates a sprite in response to an event
|
||||
[Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis
|
||||
[Sprite Scale](../examples/2d/sprite_scale.rs) | Shows how a sprite can be scaled into a rectangle while keeping the aspect ratio
|
||||
[Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite
|
||||
[Sprite Slice](../examples/2d/sprite_slice.rs) | Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique
|
||||
[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid
|
||||
|
Loading…
Reference in New Issue
Block a user