Path: blob/main/crates/bevy_sprite/src/texture_slice/slicer.rs
9412 views
use super::{BorderRect, TextureSlice};1use bevy_math::{vec2, Rect, Vec2};2use bevy_reflect::{std_traits::ReflectDefault, Reflect};34/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes5/// without needing to prepare multiple assets. The associated texture will be split into nine portions,6/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion.7///8/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other9/// sections will be scaled or tiled.10///11/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.12#[derive(Debug, Clone, Reflect, PartialEq)]13#[reflect(Clone, PartialEq)]14pub struct TextureSlicer {15/// Inset values in pixels that define the four slicing lines dividing the texture into nine sections.16pub border: BorderRect,17/// Defines how the center part of the 9 slices will scale18pub center_scale_mode: SliceScaleMode,19/// Defines how the 4 side parts of the 9 slices will scale20pub sides_scale_mode: SliceScaleMode,21/// Defines the maximum scale of the 4 corner slices (default to `1.0`)22pub max_corner_scale: f32,23}2425/// Defines how a texture slice scales when resized26#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]27#[reflect(Clone, PartialEq, Default)]28pub enum SliceScaleMode {29/// The slice will be stretched to fit the area30#[default]31Stretch,32/// The slice will be tiled to fit the area33Tile {34/// The slice will repeat when the ratio between the *drawing dimensions* of texture and the35/// *original texture size* are above `stretch_value`.36///37/// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels.38/// `2.0` means it would repeat after 20 screen pixels.39///40/// Note: The value should be inferior or equal to `1.0` to avoid quality loss.41///42/// Note: the value will be clamped to `0.001` if lower43stretch_value: f32,44},45}4647impl TextureSlicer {48/// Computes the 4 corner slices: top left, top right, bottom left, bottom right.49#[must_use]50fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {51let coef = render_size / base_rect.size();52let BorderRect {53min_inset: Vec2 { x: left, y: top },54max_inset: Vec2 {55x: right,56y: bottom,57},58} = self.border;59let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);60[61// Top Left Corner62TextureSlice {63texture_rect: Rect {64min: base_rect.min,65max: base_rect.min + vec2(left, top),66},67draw_size: vec2(left, top) * min_coef,68offset: vec2(69-render_size.x + left * min_coef,70render_size.y - top * min_coef,71) / 2.0,72},73// Top Right Corner74TextureSlice {75texture_rect: Rect {76min: vec2(base_rect.max.x - right, base_rect.min.y),77max: vec2(base_rect.max.x, base_rect.min.y + top),78},79draw_size: vec2(right, top) * min_coef,80offset: vec2(81render_size.x - right * min_coef,82render_size.y - top * min_coef,83) / 2.0,84},85// Bottom Left86TextureSlice {87texture_rect: Rect {88min: vec2(base_rect.min.x, base_rect.max.y - bottom),89max: vec2(base_rect.min.x + left, base_rect.max.y),90},91draw_size: vec2(left, bottom) * min_coef,92offset: vec2(93-render_size.x + left * min_coef,94-render_size.y + bottom * min_coef,95) / 2.0,96},97// Bottom Right Corner98TextureSlice {99texture_rect: Rect {100min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),101max: base_rect.max,102},103draw_size: vec2(right, bottom) * min_coef,104offset: vec2(105render_size.x - right * min_coef,106-render_size.y + bottom * min_coef,107) / 2.0,108},109]110}111112/// Computes the 2 horizontal side slices (left and right borders)113#[must_use]114fn horizontal_side_slices(115&self,116[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],117base_rect: Rect,118render_size: Vec2,119) -> [TextureSlice; 2] {120[121// Left122TextureSlice {123texture_rect: Rect {124min: base_rect.min + vec2(0.0, self.border.min_inset.y),125max: vec2(126base_rect.min.x + self.border.min_inset.x,127base_rect.max.y - self.border.max_inset.y,128),129},130draw_size: vec2(131tl_corner.draw_size.x,132render_size.y - (tl_corner.draw_size.y + bl_corner.draw_size.y),133),134offset: vec2(135tl_corner.draw_size.x - render_size.x,136bl_corner.draw_size.y - tl_corner.draw_size.y,137) / 2.0,138},139// Right140TextureSlice {141texture_rect: Rect {142min: vec2(143base_rect.max.x - self.border.max_inset.x,144base_rect.min.y + self.border.min_inset.y,145),146max: base_rect.max - vec2(0.0, self.border.max_inset.y),147},148draw_size: vec2(149tr_corner.draw_size.x,150render_size.y - (tr_corner.draw_size.y + br_corner.draw_size.y),151),152offset: vec2(153render_size.x - tr_corner.draw_size.x,154br_corner.draw_size.y - tr_corner.draw_size.y,155) / 2.0,156},157]158}159160/// Computes the 2 vertical side slices (top and bottom borders)161#[must_use]162fn vertical_side_slices(163&self,164[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],165base_rect: Rect,166render_size: Vec2,167) -> [TextureSlice; 2] {168[169// Top170TextureSlice {171texture_rect: Rect {172min: base_rect.min + vec2(self.border.min_inset.x, 0.0),173max: vec2(174base_rect.max.x - self.border.max_inset.x,175base_rect.min.y + self.border.min_inset.y,176),177},178draw_size: vec2(179render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),180tl_corner.draw_size.y,181),182offset: vec2(183tl_corner.draw_size.x - tr_corner.draw_size.x,184render_size.y - tl_corner.draw_size.y,185) / 2.0,186},187// Bottom188TextureSlice {189texture_rect: Rect {190min: vec2(191base_rect.min.x + self.border.min_inset.x,192base_rect.max.y - self.border.max_inset.y,193),194max: base_rect.max - vec2(self.border.max_inset.x, 0.0),195},196draw_size: vec2(197render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),198bl_corner.draw_size.y,199),200offset: vec2(201bl_corner.draw_size.x - br_corner.draw_size.x,202bl_corner.draw_size.y - render_size.y,203) / 2.0,204},205]206}207208/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,209/// a bigger number of sections will be computed.210///211/// # Arguments212///213/// * `rect` - The section of the texture to slice in 9 parts214/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.215// TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`)216#[must_use]217pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {218let render_size = render_size.unwrap_or_else(|| rect.size());219if (self.border.min_inset + self.border.max_inset)220.cmpge(rect.size())221.any()222{223tracing::error!(224"TextureSlicer::border has out of bounds values. No slicing will be applied"225);226return vec![TextureSlice {227texture_rect: rect,228draw_size: render_size,229offset: Vec2::ZERO,230}];231}232let mut slices = Vec::with_capacity(9);233// Corners are in this order: [TL, TR, BL, BR]234let corners = self.corner_slices(rect, render_size);235// Vertical Sides: [T, B]236let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);237// Horizontal Sides: [L, R]238let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);239// Center240let center = TextureSlice {241texture_rect: Rect {242min: rect.min + self.border.min_inset,243max: rect.max - self.border.max_inset,244},245draw_size: vec2(246render_size.x - (corners[0].draw_size.x + corners[1].draw_size.x),247render_size.y - (corners[0].draw_size.y + corners[2].draw_size.y),248),249offset: vec2(vertical_sides[0].offset.x, horizontal_sides[0].offset.y),250};251252slices.extend(corners);253match self.center_scale_mode {254SliceScaleMode::Stretch => {255slices.push(center);256}257SliceScaleMode::Tile { stretch_value } => {258slices.extend(center.tiled(stretch_value, (true, true)));259}260}261match self.sides_scale_mode {262SliceScaleMode::Stretch => {263slices.extend(horizontal_sides);264slices.extend(vertical_sides);265}266SliceScaleMode::Tile { stretch_value } => {267slices.extend(268horizontal_sides269.into_iter()270.flat_map(|s| s.tiled(stretch_value, (false, true))),271);272slices.extend(273vertical_sides274.into_iter()275.flat_map(|s| s.tiled(stretch_value, (true, false))),276);277}278}279slices280}281}282283impl Default for TextureSlicer {284fn default() -> Self {285Self {286border: Default::default(),287center_scale_mode: Default::default(),288sides_scale_mode: Default::default(),289max_corner_scale: 1.0,290}291}292}293294#[cfg(test)]295mod test {296use super::*;297#[test]298fn test_horizontal_sizes_uniform() {299let slicer = TextureSlicer {300border: BorderRect::all(10.),301center_scale_mode: SliceScaleMode::Stretch,302sides_scale_mode: SliceScaleMode::Stretch,303max_corner_scale: 1.0,304};305let base_rect = Rect {306min: Vec2::ZERO,307max: Vec2::splat(50.),308};309let render_rect = Vec2::splat(100.);310let slices = slicer.corner_slices(base_rect, render_rect);311assert_eq!(312slices[0],313TextureSlice {314texture_rect: Rect {315min: Vec2::ZERO,316max: Vec2::splat(10.0)317},318draw_size: Vec2::new(10.0, 10.0),319offset: Vec2::new(-45.0, 45.0),320}321);322}323324#[test]325fn test_horizontal_sizes_non_uniform_bigger() {326let slicer = TextureSlicer {327border: BorderRect {328min_inset: Vec2::new(20., 10.),329max_inset: Vec2::splat(10.),330},331center_scale_mode: SliceScaleMode::Stretch,332sides_scale_mode: SliceScaleMode::Stretch,333max_corner_scale: 1.0,334};335let base_rect = Rect {336min: Vec2::ZERO,337max: Vec2::splat(50.),338};339let render_rect = Vec2::splat(100.);340let slices = slicer.corner_slices(base_rect, render_rect);341assert_eq!(342slices[0],343TextureSlice {344texture_rect: Rect {345min: Vec2::ZERO,346max: Vec2::new(20.0, 10.0)347},348draw_size: Vec2::new(20.0, 10.0),349offset: Vec2::new(-40.0, 45.0),350}351);352}353354#[test]355fn test_horizontal_sizes_non_uniform_smaller() {356let slicer = TextureSlicer {357border: BorderRect {358min_inset: Vec2::new(5., 10.),359max_inset: Vec2::splat(10.),360},361center_scale_mode: SliceScaleMode::Stretch,362sides_scale_mode: SliceScaleMode::Stretch,363max_corner_scale: 1.0,364};365let rect = Rect {366min: Vec2::ZERO,367max: Vec2::splat(50.),368};369let render_size = Vec2::splat(100.);370let corners = slicer.corner_slices(rect, render_size);371372let vertical_sides = slicer.vertical_side_slices(&corners, rect, render_size);373assert_eq!(374corners[0],375TextureSlice {376texture_rect: Rect {377min: Vec2::ZERO,378max: Vec2::new(5.0, 10.0)379},380draw_size: Vec2::new(5.0, 10.0),381offset: Vec2::new(-47.5, 45.0),382}383);384assert_eq!(385vertical_sides[0], // top386TextureSlice {387texture_rect: Rect {388min: Vec2::new(5.0, 0.0),389max: Vec2::new(40.0, 10.0)390},391draw_size: Vec2::new(85.0, 10.0),392offset: Vec2::new(-2.5, 45.0),393}394);395}396397#[test]398fn test_horizontal_sizes_non_uniform_zero() {399let slicer = TextureSlicer {400border: BorderRect {401min_inset: Vec2::new(0., 10.),402max_inset: Vec2::splat(10.),403},404center_scale_mode: SliceScaleMode::Stretch,405sides_scale_mode: SliceScaleMode::Stretch,406max_corner_scale: 1.0,407};408let base_rect = Rect {409min: Vec2::ZERO,410max: Vec2::splat(50.),411};412let render_rect = Vec2::splat(100.);413let slices = slicer.corner_slices(base_rect, render_rect);414assert_eq!(415slices[0],416TextureSlice {417texture_rect: Rect {418min: Vec2::ZERO,419max: Vec2::new(0.0, 10.0)420},421draw_size: Vec2::new(0.0, 10.0),422offset: Vec2::new(-50.0, 45.0),423}424);425}426}427428429