Skip to main content

bevy_sprite/texture_slice/
slicer.rs

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