bevy_sprite/texture_slice/
slicer.rs

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