2D Rendering / Sprite Scale

Back to examples View in GitHub
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
                };
            }
        }
    }
}