Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_sprite/src/texture_slice/slicer.rs
9412 views
1
use super::{BorderRect, TextureSlice};
2
use bevy_math::{vec2, Rect, Vec2};
3
use 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)]
15
pub 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)]
29
pub 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
48
impl 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
284
impl 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)]
296
mod 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
}
428
429