Path: blob/main/crates/bevy_sprite/src/texture_slice/slicer.rs
6600 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 {53left,54right,55top,56bottom,57} = self.border;58let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);59[60// Top Left Corner61TextureSlice {62texture_rect: Rect {63min: base_rect.min,64max: base_rect.min + vec2(left, top),65},66draw_size: vec2(left, top) * min_coef,67offset: vec2(68-render_size.x + left * min_coef,69render_size.y - top * min_coef,70) / 2.0,71},72// Top Right Corner73TextureSlice {74texture_rect: Rect {75min: vec2(base_rect.max.x - right, base_rect.min.y),76max: vec2(base_rect.max.x, base_rect.min.y + top),77},78draw_size: vec2(right, top) * min_coef,79offset: vec2(80render_size.x - right * min_coef,81render_size.y - top * min_coef,82) / 2.0,83},84// Bottom Left85TextureSlice {86texture_rect: Rect {87min: vec2(base_rect.min.x, base_rect.max.y - bottom),88max: vec2(base_rect.min.x + left, base_rect.max.y),89},90draw_size: vec2(left, bottom) * min_coef,91offset: vec2(92-render_size.x + left * min_coef,93-render_size.y + bottom * min_coef,94) / 2.0,95},96// Bottom Right Corner97TextureSlice {98texture_rect: Rect {99min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),100max: base_rect.max,101},102draw_size: vec2(right, bottom) * min_coef,103offset: vec2(104render_size.x - right * min_coef,105-render_size.y + bottom * min_coef,106) / 2.0,107},108]109}110111/// Computes the 2 horizontal side slices (left and right borders)112#[must_use]113fn horizontal_side_slices(114&self,115[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],116base_rect: Rect,117render_size: Vec2,118) -> [TextureSlice; 2] {119[120// Left121TextureSlice {122texture_rect: Rect {123min: base_rect.min + vec2(0.0, self.border.top),124max: vec2(125base_rect.min.x + self.border.left,126base_rect.max.y - self.border.bottom,127),128},129draw_size: vec2(130tl_corner.draw_size.x,131render_size.y - (tl_corner.draw_size.y + bl_corner.draw_size.y),132),133offset: vec2(134tl_corner.draw_size.x - render_size.x,135bl_corner.draw_size.y - tl_corner.draw_size.y,136) / 2.0,137},138// Right139TextureSlice {140texture_rect: Rect {141min: vec2(142base_rect.max.x - self.border.right,143base_rect.min.y + self.border.top,144),145max: base_rect.max - vec2(0.0, self.border.bottom),146},147draw_size: vec2(148tr_corner.draw_size.x,149render_size.y - (tr_corner.draw_size.y + br_corner.draw_size.y),150),151offset: vec2(152render_size.x - tr_corner.draw_size.x,153br_corner.draw_size.y - tr_corner.draw_size.y,154) / 2.0,155},156]157}158159/// Computes the 2 vertical side slices (top and bottom borders)160#[must_use]161fn vertical_side_slices(162&self,163[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],164base_rect: Rect,165render_size: Vec2,166) -> [TextureSlice; 2] {167[168// Top169TextureSlice {170texture_rect: Rect {171min: base_rect.min + vec2(self.border.left, 0.0),172max: vec2(173base_rect.max.x - self.border.right,174base_rect.min.y + self.border.top,175),176},177draw_size: vec2(178render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),179tl_corner.draw_size.y,180),181offset: vec2(182tl_corner.draw_size.x - tr_corner.draw_size.x,183render_size.y - tl_corner.draw_size.y,184) / 2.0,185},186// Bottom187TextureSlice {188texture_rect: Rect {189min: vec2(190base_rect.min.x + self.border.left,191base_rect.max.y - self.border.bottom,192),193max: base_rect.max - vec2(self.border.right, 0.0),194},195draw_size: vec2(196render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),197bl_corner.draw_size.y,198),199offset: vec2(200bl_corner.draw_size.x - br_corner.draw_size.x,201bl_corner.draw_size.y - render_size.y,202) / 2.0,203},204]205}206207/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,208/// a bigger number of sections will be computed.209///210/// # Arguments211///212/// * `rect` - The section of the texture to slice in 9 parts213/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.214// TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`)215#[must_use]216pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {217let render_size = render_size.unwrap_or_else(|| rect.size());218if self.border.left + self.border.right >= rect.size().x219|| self.border.top + self.border.bottom >= rect.size().y220{221tracing::error!(222"TextureSlicer::border has out of bounds values. No slicing will be applied"223);224return vec![TextureSlice {225texture_rect: rect,226draw_size: render_size,227offset: Vec2::ZERO,228}];229}230let mut slices = Vec::with_capacity(9);231// Corners are in this order: [TL, TR, BL, BR]232let corners = self.corner_slices(rect, render_size);233// Vertical Sides: [T, B]234let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);235// Horizontal Sides: [L, R]236let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);237// Center238let center = TextureSlice {239texture_rect: Rect {240min: rect.min + vec2(self.border.left, self.border.top),241max: rect.max - vec2(self.border.right, self.border.bottom),242},243draw_size: vec2(244render_size.x - (corners[0].draw_size.x + corners[1].draw_size.x),245render_size.y - (corners[0].draw_size.y + corners[2].draw_size.y),246),247offset: vec2(vertical_sides[0].offset.x, horizontal_sides[0].offset.y),248};249250slices.extend(corners);251match self.center_scale_mode {252SliceScaleMode::Stretch => {253slices.push(center);254}255SliceScaleMode::Tile { stretch_value } => {256slices.extend(center.tiled(stretch_value, (true, true)));257}258}259match self.sides_scale_mode {260SliceScaleMode::Stretch => {261slices.extend(horizontal_sides);262slices.extend(vertical_sides);263}264SliceScaleMode::Tile { stretch_value } => {265slices.extend(266horizontal_sides267.into_iter()268.flat_map(|s| s.tiled(stretch_value, (false, true))),269);270slices.extend(271vertical_sides272.into_iter()273.flat_map(|s| s.tiled(stretch_value, (true, false))),274);275}276}277slices278}279}280281impl Default for TextureSlicer {282fn default() -> Self {283Self {284border: Default::default(),285center_scale_mode: Default::default(),286sides_scale_mode: Default::default(),287max_corner_scale: 1.0,288}289}290}291292#[cfg(test)]293mod test {294use super::*;295#[test]296fn test_horizontal_sizes_uniform() {297let slicer = TextureSlicer {298border: BorderRect {299left: 10.,300right: 10.,301top: 10.,302bottom: 10.,303},304center_scale_mode: SliceScaleMode::Stretch,305sides_scale_mode: SliceScaleMode::Stretch,306max_corner_scale: 1.0,307};308let base_rect = Rect {309min: Vec2::ZERO,310max: Vec2::splat(50.),311};312let render_rect = Vec2::splat(100.);313let slices = slicer.corner_slices(base_rect, render_rect);314assert_eq!(315slices[0],316TextureSlice {317texture_rect: Rect {318min: Vec2::ZERO,319max: Vec2::splat(10.0)320},321draw_size: Vec2::new(10.0, 10.0),322offset: Vec2::new(-45.0, 45.0),323}324);325}326327#[test]328fn test_horizontal_sizes_non_uniform_bigger() {329let slicer = TextureSlicer {330border: BorderRect {331left: 20.,332right: 10.,333top: 10.,334bottom: 10.,335},336center_scale_mode: SliceScaleMode::Stretch,337sides_scale_mode: SliceScaleMode::Stretch,338max_corner_scale: 1.0,339};340let base_rect = Rect {341min: Vec2::ZERO,342max: Vec2::splat(50.),343};344let render_rect = Vec2::splat(100.);345let slices = slicer.corner_slices(base_rect, render_rect);346assert_eq!(347slices[0],348TextureSlice {349texture_rect: Rect {350min: Vec2::ZERO,351max: Vec2::new(20.0, 10.0)352},353draw_size: Vec2::new(20.0, 10.0),354offset: Vec2::new(-40.0, 45.0),355}356);357}358359#[test]360fn test_horizontal_sizes_non_uniform_smaller() {361let slicer = TextureSlicer {362border: BorderRect {363left: 5.,364right: 10.,365top: 10.,366bottom: 10.,367},368center_scale_mode: SliceScaleMode::Stretch,369sides_scale_mode: SliceScaleMode::Stretch,370max_corner_scale: 1.0,371};372let rect = Rect {373min: Vec2::ZERO,374max: Vec2::splat(50.),375};376let render_size = Vec2::splat(100.);377let corners = slicer.corner_slices(rect, render_size);378379let vertical_sides = slicer.vertical_side_slices(&corners, rect, render_size);380assert_eq!(381corners[0],382TextureSlice {383texture_rect: Rect {384min: Vec2::ZERO,385max: Vec2::new(5.0, 10.0)386},387draw_size: Vec2::new(5.0, 10.0),388offset: Vec2::new(-47.5, 45.0),389}390);391assert_eq!(392vertical_sides[0], // top393TextureSlice {394texture_rect: Rect {395min: Vec2::new(5.0, 0.0),396max: Vec2::new(40.0, 10.0)397},398draw_size: Vec2::new(85.0, 10.0),399offset: Vec2::new(-2.5, 45.0),400}401);402}403404#[test]405fn test_horizontal_sizes_non_uniform_zero() {406let slicer = TextureSlicer {407border: BorderRect {408left: 0.,409right: 10.,410top: 10.,411bottom: 10.,412},413center_scale_mode: SliceScaleMode::Stretch,414sides_scale_mode: SliceScaleMode::Stretch,415max_corner_scale: 1.0,416};417let base_rect = Rect {418min: Vec2::ZERO,419max: Vec2::splat(50.),420};421let render_rect = Vec2::splat(100.);422let slices = slicer.corner_slices(base_rect, render_rect);423assert_eq!(424slices[0],425TextureSlice {426texture_rect: Rect {427min: Vec2::ZERO,428max: Vec2::new(0.0, 10.0)429},430draw_size: Vec2::new(0.0, 10.0),431offset: Vec2::new(-50.0, 45.0),432}433);434}435}436437438