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