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
6600 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
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
282
impl 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)]
294
mod 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
}
437
438