diff --git a/Cargo.toml b/Cargo.toml index 2905c2cf27d86..edf88954b8c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,6 +212,26 @@ description = "Renders a sprite" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_tile" +path = "examples/2d/sprite_tile.rs" + +[package.metadata.example.sprite_tile] +name = "Sprite Tile" +description = "Renders a sprite tiled in a grid" +category = "2D Rendering" +wasm = true + +[[example]] +name = "sprite_slice" +path = "examples/2d/sprite_slice.rs" + +[package.metadata.example.sprite_slice] +name = "Sprite Slice" +description = "Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite_flipping" path = "examples/2d/sprite_flipping.rs" diff --git a/assets/textures/slice_sprite.png b/assets/textures/slice_sprite.png new file mode 100644 index 0000000000000..f2bd94d1f66da Binary files /dev/null and b/assets/textures/slice_sprite.png differ diff --git a/assets/textures/slice_square.png b/assets/textures/slice_square.png new file mode 100644 index 0000000000000..bee873c46b5a4 Binary files /dev/null and b/assets/textures/slice_square.png differ diff --git a/assets/textures/slice_square_2.png b/assets/textures/slice_square_2.png new file mode 100644 index 0000000000000..b38d6ee6664be Binary files /dev/null and b/assets/textures/slice_square_2.png differ diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 95aba8c272620..01b704959f958 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -5,6 +5,7 @@ mod render; mod sprite; mod texture_atlas; mod texture_atlas_builder; +mod texture_slice; pub mod collide_aabb; @@ -12,9 +13,9 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ bundle::{SpriteBundle, SpriteSheetBundle}, - rect::{BorderRect, Rect}, - sprite::Sprite, + sprite::{Sprite, SpriteDrawMode}, texture_atlas::{TextureAtlas, TextureAtlasSprite}, + texture_slice::{BorderRect, SliceScaleMode, TextureSlicer}, ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, }; } @@ -26,6 +27,7 @@ pub use render::*; pub use sprite::*; pub use texture_atlas::*; pub use texture_atlas_builder::*; +pub use texture_slice::*; use bevy_app::prelude::*; use bevy_asset::{AddAsset, Assets, HandleUntyped}; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 3fcb8b11f2e91..f3d7b27df8e3e 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, - Rect, Sprite, SpriteSlice, SPRITE_SHADER_HANDLE, + Sprite, SpriteDrawMode, TextureSlice, SPRITE_SHADER_HANDLE, }; use bevy_asset::{AssetEvent, Assets, Handle, HandleId}; use bevy_core_pipeline::{core_2d::Transparent2d, tonemapping::Tonemapping}; @@ -30,7 +30,7 @@ use bevy_render::{ }, Extract, }; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::FloatOrd; use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; @@ -324,7 +324,6 @@ pub fn extract_sprites( &Sprite, &GlobalTransform, &Handle, - Option<&SpriteSlice> )>, >, atlas_query: Extract< @@ -338,48 +337,72 @@ pub fn extract_sprites( >, ) { extracted_sprites.sprites.clear(); - for (entity, visibility, sprite, transform, handle, slice) in sprite_query.iter() { + for (entity, visibility, sprite, transform, handle) in sprite_query.iter() { if !visibility.is_visible() { continue; } - if let Some(slice) = slice { - let image_size = match images.get(handle) { - None => continue, - Some(i) => Vec2::new( - i.texture_descriptor.size.width as f32, - i.texture_descriptor.size.height as f32, - ), - }; - // TODO: remove - let slice: &SpriteSlice = slice; - // - for rect in slice.slice_rects(image_size) { - extracted_sprites.sprites.alloc().init(ExtractedSprite { - entity, - color: sprite.color, - transform: *transform, - rect: Some(rect), - // Pass the custom size - custom_size: None, - flip_x: sprite.flip_x, - flip_y: sprite.flip_y, - image_handle_id: handle.id, - anchor: sprite.anchor.as_vec(), - }); - } - } else { + if let SpriteDrawMode::Simple = sprite.draw_mode { // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive - extracted_sprites.sprites.alloc().init(ExtractedSprite { + extracted_sprites.sprites.push(ExtractedSprite { entity, color: sprite.color, transform: *transform, // Use the full texture rect: None, - // Pass the custom size custom_size: sprite.custom_size, flip_x: sprite.flip_x, flip_y: sprite.flip_y, - image_handle_id: handle.id, + image_handle_id: handle.id(), + anchor: sprite.anchor.as_vec(), + }); + continue; + } + let image_size = match images.get(handle) { + None => continue, + Some(i) => Vec2::new( + i.texture_descriptor.size.width as f32, + i.texture_descriptor.size.height as f32, + ), + }; + + let slices = match &sprite.draw_mode { + SpriteDrawMode::Sliced(slicer) => slicer.compute_slices( + Rect { + min: Vec2::ZERO, + max: image_size, + }, + sprite.custom_size, + ), + SpriteDrawMode::Tiled { + tile_x, + tile_y, + stretch_value, + } => { + let slice = TextureSlice { + texture_rect: Rect { + min: Vec2::ZERO, + max: image_size, + }, + draw_size: sprite.custom_size.unwrap_or(image_size), + offset: Vec2::ZERO, + }; + slice.tiled(*stretch_value, (*tile_x, *tile_y)) + } + SpriteDrawMode::Simple => unreachable!(), + }; + for slice in slices { + let mut transform: GlobalTransform = *transform; + transform = + transform.mul_transform(Transform::from_translation(slice.offset.extend(0.0))); + extracted_sprites.sprites.push(ExtractedSprite { + entity, + color: sprite.color, + transform, + rect: Some(slice.texture_rect), + custom_size: Some(slice.draw_size), + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id(), anchor: sprite.anchor.as_vec(), }); } @@ -575,7 +598,7 @@ pub fn queue_sprites( }; let mut current_batch_entity = Entity::from_raw(u32::MAX); let mut current_image_size = Vec2::ZERO; - // Add a phase item for each sprite, and detect when succesive items can be batched. + // Add a phase item for each sprite, and detect when successive items can be batched. // Spawn an entity with a `SpriteBatch` component for each possible batch. // Compatible items share the same entity. // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 38c0457ad6da2..3998854d7626f 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -1,4 +1,4 @@ -use crate::{BorderRect, Rect}; +use crate::TextureSlicer; use bevy_ecs::component::Component; use bevy_math::{Rect, Vec2}; use bevy_reflect::Reflect; @@ -21,27 +21,27 @@ pub struct Sprite { pub rect: Option, /// [`Anchor`] point of the sprite in the world pub anchor: Anchor, + /// Define how the Sprite scales when its dimensions change + pub draw_mode: SpriteDrawMode, } -/// Defines how the non corner sections of a [`SpriteSlice`] are scaled. -#[derive(Debug, Copy, Clone, Default, Reflect)] -pub enum SliceScaleMode { - /// The sections will stretch +#[derive(Debug, Default, Clone, Reflect)] +pub enum SpriteDrawMode { + /// The entire texture scales when its dimensions change. This is the default option. #[default] - Stretch, - /// The sections will repeat - Tile, -} - -/// Component for [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) sprites. -/// -/// When resizing a 9-sliced sprite the corners will remain unscaled while the other sections will be scaled or tiled -#[derive(Component, Debug, Clone, Default, Reflect)] -pub struct SpriteSlice { - /// The sprite borders, defining the 9 sections of the image - pub border: BorderRect, - /// How do the the non corner sections scale - pub scale_mode: SliceScaleMode, + Simple, + /// 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` + Tiled { + /// Should the image repeat horizontally + tile_x: bool, + /// Should the image repeat vertically + tile_y: bool, + /// The texture will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above this value + stretch_value: f32, + }, } /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). @@ -64,62 +64,9 @@ pub enum Anchor { Custom(Vec2), } -impl SpriteSlice { - /// Computes the 9 [`Rect`] and size values for a [`Sprite`] given its `image_size` - pub fn slice_rects(&self, image_size: Vec2) -> [Rect; 9] { - // corners - let bl_corner = Rect { - min: Vec2::ZERO, - max: Vec2::new(self.border.left, self.border.bottom), - }; - let br_corner = Rect { - min: Vec2::new(image_size.x - self.border.right, 0.0), - max: Vec2::new(image_size.x, self.border.bottom), - }; - let tl_corner = Rect { - min: Vec2::new(0.0, image_size.y - self.border.top), - max: Vec2::new(self.border.left, image_size.y), - }; - let tr_corner = Rect { - min: Vec2::new( - image_size.x - self.border.right, - image_size.y - self.border.top, - ), - max: Vec2::new(image_size.x, image_size.y), - }; - // Sides - let left_side = Rect { - min: Vec2::new(0.0, self.border.bottom), - max: Vec2::new(self.border.left, image_size.y - self.border.top), - }; - let right_side = Rect { - min: Vec2::new(image_size.x - self.border.right, self.border.bottom), - max: Vec2::new(image_size.x, image_size.y - self.border.top), - }; - let bot_side = Rect { - min: Vec2::new(self.border.left, 0.0), - max: Vec2::new(image_size.x - self.border.right, self.border.bottom), - }; - let top_side = Rect { - min: Vec2::new(self.border.left, image_size.y - self.border.top), - max: Vec2::new(image_size.x - self.border.right, image_size.y), - }; - // Center - let center = Rect { - min: Vec2::new(self.border.left, self.border.bottom), - max: Vec2::new( - image_size.x - self.border.right, - image_size.y - self.border.top, - ), - }; - [ - bl_corner, br_corner, tl_corner, tr_corner, left_side, right_side, bot_side, top_side, - center, - ] - } -} - impl Anchor { + #[inline] + #[must_use] pub fn as_vec(&self) -> Vec2 { match self { Anchor::Center => Vec2::ZERO, diff --git a/crates/bevy_sprite/src/texture_slice.rs b/crates/bevy_sprite/src/texture_slice.rs new file mode 100644 index 0000000000000..099e78b068bb1 --- /dev/null +++ b/crates/bevy_sprite/src/texture_slice.rs @@ -0,0 +1,387 @@ +use bevy_math::{Rect, Vec2}; +use bevy_reflect::{FromReflect, Reflect}; + +/// Struct defining a [`Sprite`](crate::Sprite) border with padding values +#[derive(Default, Clone, Copy, Debug, Reflect, FromReflect)] +pub struct BorderRect { + /// Pixel padding to the left + pub left: f32, + /// Pixel padding to the right + pub right: f32, + /// Pixel padding to the top + pub top: f32, + /// Pixel padding to the bottom + pub bottom: f32, +} + +impl BorderRect { + /// Creates a new border as a square, with identical pixel padding values on every direction + #[must_use] + #[inline] + pub const fn square(value: f32) -> Self { + Self { + left: value, + right: value, + top: value, + bottom: value, + } + } + + /// Creates a new border as a rectangle, with: + /// - `horizontal` for left and right pixel padding + /// - `vertical` for top and bottom pixel padding + #[must_use] + #[inline] + pub const fn rectangle(horizontal: f32, vertical: f32) -> Self { + Self { + left: horizontal, + right: horizontal, + top: vertical, + bottom: vertical, + } + } +} + +impl From for BorderRect { + fn from(v: f32) -> Self { + Self::square(v) + } +} + +impl From<[f32; 4]> for BorderRect { + fn from([left, right, top, bottom]: [f32; 4]) -> Self { + Self { + left, + right, + top, + bottom, + } + } +} + +/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes +/// without needing to prepare multiple assets. The associated texture will be split into nine portions, +/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion. +/// +/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other +/// sections will be scaled or tiled. +/// +/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures. +#[derive(Debug, Clone, Reflect, FromReflect)] +pub struct TextureSlicer { + /// The sprite borders, defining the 9 sections of the image + pub border: BorderRect, + /// Defines how the center part of the 9 slices will scale + pub center_scale_mode: SliceScaleMode, + /// Defines how the 4 side parts of the 9 slices will scale + pub sides_scale_mode: SliceScaleMode, + /// Defines the maximum scale of the 4 corner slices (default to `1.0`) + pub max_corner_scale: f32, +} + +/// Defines how a texture slice scales when resized +#[derive(Debug, Copy, Clone, Default, Reflect, FromReflect)] +pub enum SliceScaleMode { + /// The slice will be stretched to fit the area + #[default] + Stretch, + /// The slice will be tiled to fit the area + Tile { + /// The slice will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above `stretch_value`. + /// + /// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels. + /// `2.0` means it would repeat after 20 screen pixels. + /// + /// Note: The value should be inferior or equal to `1.0` to avoid quality loss. + /// + /// Note: the value will be clamped to `0.001` if lower + stretch_value: f32, + }, +} + +#[derive(Debug, Clone)] +pub(crate) struct TextureSlice { + /// texture area to draw + pub texture_rect: Rect, + /// slice draw size + pub draw_size: Vec2, + /// offset of the slice + pub offset: Vec2, +} + +impl TextureSlicer { + /// Computes the 4 corner slices + fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] { + let coef = render_size / base_rect.size(); + let min_coef = coef.x.min(coef.y).min(self.max_corner_scale); + [ + // Top Left Corner + TextureSlice { + texture_rect: Rect { + min: base_rect.min, + max: base_rect.min + Vec2::new(self.border.left, self.border.top), + }, + draw_size: Vec2::new(self.border.left, self.border.top) * min_coef, + offset: Vec2::new( + -render_size.x + self.border.left * min_coef, + render_size.y - self.border.top * min_coef, + ) / 2.0, + }, + // Top Right Corner + TextureSlice { + texture_rect: Rect { + min: Vec2::new(base_rect.max.x - self.border.right, base_rect.min.y), + max: Vec2::new(base_rect.max.x, self.border.top), + }, + draw_size: Vec2::new(self.border.right, self.border.top) * min_coef, + offset: Vec2::new( + render_size.x - self.border.right * min_coef, + render_size.y - self.border.top * min_coef, + ) / 2.0, + }, + // Bottom Left + TextureSlice { + texture_rect: Rect { + min: Vec2::new(base_rect.min.x, base_rect.max.y - self.border.bottom), + max: Vec2::new(base_rect.min.x + self.border.left, base_rect.max.y), + }, + draw_size: Vec2::new(self.border.left, self.border.bottom) * min_coef, + offset: Vec2::new( + -render_size.x + self.border.left * min_coef, + -render_size.y + self.border.bottom * min_coef, + ) / 2.0, + }, + // Bottom Right Corner + TextureSlice { + texture_rect: Rect { + min: Vec2::new( + base_rect.max.x - self.border.right, + base_rect.max.y - self.border.bottom, + ), + max: base_rect.max, + }, + draw_size: Vec2::new(self.border.right, self.border.bottom) * min_coef, + offset: Vec2::new( + render_size.x - self.border.right * min_coef, + -render_size.y + self.border.bottom * min_coef, + ) / 2.0, + }, + ] + } + + /// Computes the 2 horizontal side slices (left and right borders) + fn horizontal_side_slices( + &self, + [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], + base_rect: Rect, + render_size: Vec2, + ) -> [TextureSlice; 2] { + // left + let left_side = TextureSlice { + texture_rect: Rect { + min: base_rect.min + Vec2::new(0.0, self.border.top), + max: Vec2::new( + base_rect.min.x + self.border.left, + base_rect.max.y - self.border.bottom, + ), + }, + draw_size: Vec2::new( + bl_corner.draw_size.x, + render_size.y - bl_corner.draw_size.y - tl_corner.draw_size.y, + ), + offset: Vec2::new(-render_size.x + bl_corner.draw_size.x, 0.0) / 2.0, + }; + + // right + let right_side = TextureSlice { + texture_rect: Rect { + min: Vec2::new( + base_rect.max.x - self.border.right, + base_rect.min.y + self.border.bottom, + ), + max: Vec2::new(base_rect.max.x, base_rect.max.y - self.border.top), + }, + draw_size: Vec2::new( + br_corner.draw_size.x, + render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y), + ), + offset: Vec2::new(render_size.x - br_corner.draw_size.x, 0.0) / 2.0, + }; + [left_side, right_side] + } + + /// Computes the 2 vertical side slices (top and bottom borders) + fn vertical_side_slices( + &self, + [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], + base_rect: Rect, + render_size: Vec2, + ) -> [TextureSlice; 2] { + // Bottom + let bot_side = TextureSlice { + texture_rect: Rect { + min: Vec2::new( + base_rect.min.x + self.border.left, + base_rect.max.y - self.border.bottom, + ), + max: Vec2::new(base_rect.max.x - self.border.right, base_rect.max.y), + }, + draw_size: Vec2::new( + render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x), + bl_corner.draw_size.y, + ), + offset: Vec2::new(0.0, bl_corner.offset.y), + }; + + // Top + let top_side = TextureSlice { + texture_rect: Rect { + min: base_rect.min + Vec2::new(self.border.left, 0.0), + max: Vec2::new( + base_rect.max.x - self.border.right, + base_rect.min.y + self.border.top, + ), + }, + draw_size: Vec2::new( + render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x), + tl_corner.draw_size.y, + ), + offset: Vec2::new(0.0, tl_corner.offset.y), + }; + [bot_side, top_side] + } + + /// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile, + /// a bigger number of sections will be computed. + /// + /// # Arguments + /// + /// * `rect` - The section of the texture to slice in 9 parts + /// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used. + pub(crate) fn compute_slices( + &self, + rect: Rect, + render_size: Option, + ) -> Vec { + let render_size = render_size.unwrap_or_else(|| rect.size()); + let mut slices = Vec::with_capacity(9); + // Corners + let corners = self.corner_slices(rect, render_size); + // Sides + let vertical_sides = self.vertical_side_slices(&corners, rect, render_size); + let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size); + // Center + let center = TextureSlice { + texture_rect: Rect { + min: rect.min + Vec2::new(self.border.left, self.border.bottom), + max: Vec2::new(rect.max.x - self.border.right, rect.max.y - self.border.top), + }, + draw_size: Vec2::new( + render_size.x - (corners[2].draw_size.x + corners[3].draw_size.x), + render_size.y - (corners[2].draw_size.y + corners[0].draw_size.y), + ), + offset: Vec2::ZERO, + }; + + slices.extend(corners); + match self.center_scale_mode { + SliceScaleMode::Stretch => { + slices.push(center); + } + SliceScaleMode::Tile { stretch_value } => { + slices.extend(center.tiled(stretch_value, (true, true))); + } + } + match self.sides_scale_mode { + SliceScaleMode::Stretch => { + slices.extend(horizontal_sides); + slices.extend(vertical_sides); + } + SliceScaleMode::Tile { stretch_value } => { + slices.extend( + horizontal_sides + .into_iter() + .flat_map(|s| s.tiled(stretch_value, (false, true))), + ); + slices.extend( + vertical_sides + .into_iter() + .flat_map(|s| s.tiled(stretch_value, (true, false))), + ); + } + } + slices + } +} + +impl TextureSlice { + /// Transforms the given slice in an collection of tiled subdivisions. + /// + /// # Arguments + /// + /// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above `stretch_value`. + /// - `tile_x` - should the slice be tiled horizontally + /// - `tile_y` - should the slice be tiled vertically + pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec { + if !tile_x && !tile_y { + return vec![self]; + } + let stretch_value = stretch_value.max(0.001); + let rect_size = self.texture_rect.size(); + let expected_size = Vec2::new( + if tile_x { + rect_size.x * stretch_value + } else { + self.draw_size.x + }, + if tile_y { + rect_size.y * stretch_value + } else { + self.draw_size.y + }, + ); + let mut slices = Vec::new(); + let base_offset = -self.draw_size / 2.0; + let mut offset = base_offset; + + let mut remaining_columns = self.draw_size.y; + while remaining_columns > 0.0 { + let size_y = expected_size.y.min(remaining_columns); + offset.x = base_offset.x; + offset.y += size_y / 2.0; + let mut remaining_rows = self.draw_size.x; + while remaining_rows > 0.0 { + let size_x = expected_size.x.min(remaining_rows); + offset.x += size_x / 2.0; + let draw_size = Vec2::new(size_x, size_y); + let delta = draw_size / expected_size; + slices.push(Self { + texture_rect: Rect { + min: self.texture_rect.min, + max: self.texture_rect.min + self.texture_rect.size() * delta, + }, + draw_size, + offset: self.offset + offset, + }); + offset.x += size_x / 2.0; + remaining_rows -= size_x; + } + offset.y += size_y / 2.0; + remaining_columns -= size_y; + } + slices + } +} + +impl Default for TextureSlicer { + fn default() -> Self { + Self { + border: Default::default(), + center_scale_mode: Default::default(), + sides_scale_mode: Default::default(), + max_corner_scale: 1.0, + } + } +} diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs new file mode 100644 index 0000000000000..e61f99a0d552c --- /dev/null +++ b/examples/2d/sprite_slice.rs @@ -0,0 +1,135 @@ +//! Showcases sprite 9 slice scaling + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + window: WindowDescriptor { + width: 1350.0, + height: 700.0, + ..default() + }, + ..default() + })) + .add_startup_system(setup) + .run(); +} + +fn spawn_sprites( + commands: &mut Commands, + texture_handle: Handle, + base_pos: Vec3, + slice_border: f32, +) { + // Reference sprite + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::splat(100.0)), + ..default() + }, + ..default() + }); + + // Scaled regular sprite + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 150.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(100.0, 200.0)), + ..default() + }, + ..default() + }); + + // Stretched Scaled sliced sprite + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 300.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(100.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Stretch, + ..default() + }), + ..default() + }, + ..default() + }); + + // Scaled sliced sprite + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 450.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(100.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, + ..default() + }), + ..default() + }, + ..default() + }); + + // Scaled sliced sprite horizontally + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 700.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(300.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 }, + ..default() + }), + ..default() + }, + ..default() + }); + + // Scaled sliced sprite horizontally with max scale + commands.spawn(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 1050.0), + texture: texture_handle, + sprite: Sprite { + custom_size: Some(Vec2::new(300.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, + max_corner_scale: 0.2, + }), + ..default() + }, + ..default() + }); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + // Load textures + let handle_1 = asset_server.load("textures/slice_square.png"); + let handle_2 = asset_server.load("textures/slice_square_2.png"); + let handle_3 = asset_server.load("textures/slice_sprite.png"); + + spawn_sprites( + &mut commands, + handle_1, + Vec3::new(-600.0, 200.0, 0.0), + 200.0, + ); + spawn_sprites(&mut commands, handle_2, Vec3::new(-600.0, 0.0, 0.0), 80.0); + spawn_sprites( + &mut commands, + handle_3, + Vec3::new(-600.0, -200.0, 0.0), + 55.0, + ); +} diff --git a/examples/2d/sprite_tile.rs b/examples/2d/sprite_tile.rs new file mode 100644 index 0000000000000..f7cdd32cdf99f --- /dev/null +++ b/examples/2d/sprite_tile.rs @@ -0,0 +1,27 @@ +//! Displays a single [`Sprite`] tiled in a grid + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn_bundle(Camera2dBundle::default()); + commands.spawn_bundle(SpriteBundle { + texture: asset_server.load("branding/icon.png"), + sprite: Sprite { + custom_size: Some(Vec2::splat(512.0)), // The image size is 256px + draw_mode: SpriteDrawMode::Tiled { + tile_x: true, + tile_y: true, + stretch_value: 1.0, // The image will tile every 256px + }, + ..default() + }, + ..default() + }); +} diff --git a/examples/README.md b/examples/README.md index 31b986abdbe05..6b3c8da00d939 100644 --- a/examples/README.md +++ b/examples/README.md @@ -96,6 +96,8 @@ Example | Description [Sprite](../examples/2d/sprite.rs) | Renders a sprite [Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis [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 [Text 2D](../examples/2d/text2d.rs) | Generates text in 2D [Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites [Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d