diff --git a/Cargo.toml b/Cargo.toml index 3708e1d45b..4112429b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2917,6 +2917,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_slice_flip_and_tile" +path = "examples/ui/ui_texture_slice_flip_and_tile.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_slice_flip_and_tile] +name = "UI Texture Slice Flipping and Tiling" +description = "Illustrates how to flip and tile images with 9 Slicing in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "ui_texture_atlas_slice" path = "examples/ui/ui_texture_atlas_slice.rs" diff --git a/assets/textures/fantasy_ui_borders/numbered_slices.png b/assets/textures/fantasy_ui_borders/numbered_slices.png new file mode 100644 index 0000000000..612c3120ac Binary files /dev/null and b/assets/textures/fantasy_ui_borders/numbered_slices.png differ diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 113f28087b..ce60087cf2 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -232,6 +232,8 @@ pub struct ExtractedUiTextureSlice { pub camera_entity: Entity, pub color: LinearRgba, pub image_scale_mode: ImageScaleMode, + pub flip_x: bool, + pub flip_y: bool, } #[derive(Resource, Default)] @@ -294,6 +296,8 @@ pub fn extract_ui_texture_slices( camera_entity, image_scale_mode: image_scale_mode.clone(), atlas_rect, + flip_x: image.flip_x, + flip_y: image.flip_y, }, ); } @@ -546,7 +550,7 @@ pub fn prepare_ui_slices( let color = texture_slices.color.to_f32_array(); - let (image_size, atlas) = if let Some(atlas) = texture_slices.atlas_rect { + let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect { ( atlas.size(), [ @@ -560,6 +564,14 @@ pub fn prepare_ui_slices( (batch_image_size, [0., 0., 1., 1.]) }; + if texture_slices.flip_x { + atlas.swap(0, 2); + } + + if texture_slices.flip_y { + atlas.swap(1, 3); + } + let [slices, border, repeat] = compute_texture_slices( image_size, uinode_rect.size(), @@ -692,7 +704,7 @@ fn compute_texture_slices( ) -> [[f32; 4]; 3] { match image_scale_mode { ImageScaleMode::Sliced(TextureSlicer { - border, + border: border_rect, center_scale_mode, sides_scale_mode, max_corner_scale, @@ -701,31 +713,48 @@ fn compute_texture_slices( .min_element() .min(*max_corner_scale); + // calculate the normalized extents of the nine-patched image slices let slices = [ - border.left / image_size.x, - border.top / image_size.y, - 1. - border.right / image_size.x, - 1. - border.bottom / image_size.y, + border_rect.left / image_size.x, + border_rect.top / image_size.y, + 1. - border_rect.right / image_size.x, + 1. - border_rect.bottom / image_size.y, ]; + // calculate the normalized extents of the target slices let border = [ - (border.left / target_size.x) * min_coeff, - (border.top / target_size.y) * min_coeff, - 1. - (border.right / target_size.x) * min_coeff, - 1. - (border.bottom / target_size.y) * min_coeff, + (border_rect.left / target_size.x) * min_coeff, + (border_rect.top / target_size.y) * min_coeff, + 1. - (border_rect.right / target_size.x) * min_coeff, + 1. - (border_rect.bottom / target_size.y) * min_coeff, ]; - let isx = image_size.x * (1. - slices[0] - slices[2]); - let isy = image_size.y * (1. - slices[1] - slices[3]); - let tsx = target_size.x * (1. - border[0] - border[2]); - let tsy = target_size.y * (1. - border[1] - border[3]); + let image_side_width = image_size.x * (slices[2] - slices[0]); + let image_side_height = image_size.y * (slices[2] - slices[1]); + let target_side_height = target_size.x * (border[2] - border[0]); + let target_side_width = target_size.y * (border[3] - border[1]); - let rx = compute_tiled_subaxis(isx, tsx, sides_scale_mode); - let ry = compute_tiled_subaxis(isy, tsy, sides_scale_mode); - let cx = compute_tiled_subaxis(isx, tsx, center_scale_mode); - let cy = compute_tiled_subaxis(isy, tsy, center_scale_mode); + // compute the number of times to repeat the side and center slices when tiling along each axis + // if the returned value is `1.` the slice will be stretched to fill the axis. + let repeat_side_x = + compute_tiled_subaxis(image_side_width, target_side_height, sides_scale_mode); + let repeat_side_y = + compute_tiled_subaxis(image_side_height, target_side_width, sides_scale_mode); + let repeat_center_x = + compute_tiled_subaxis(image_side_width, target_side_height, center_scale_mode); + let repeat_center_y = + compute_tiled_subaxis(image_side_height, target_side_width, center_scale_mode); - [slices, border, [rx, ry, cx, cy]] + [ + slices, + border, + [ + repeat_side_x, + repeat_side_y, + repeat_center_x, + repeat_center_y, + ], + ] } ImageScaleMode::Tiled { tile_x, @@ -739,21 +768,21 @@ fn compute_texture_slices( } } -fn compute_tiled_axis(tile: bool, is: f32, ts: f32, stretch: f32) -> f32 { +fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 { if tile { - let s = is * stretch; - ts / s + let s = image_extent * stretch; + target_extent / s } else { 1. } } -fn compute_tiled_subaxis(is: f32, ts: f32, mode: &SliceScaleMode) -> f32 { +fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 { match mode { SliceScaleMode::Stretch => 1., SliceScaleMode::Tile { stretch_value } => { - let s = is * *stretch_value; - ts / s + let s = image_extent * *stretch_value; + target_extent / s } } } diff --git a/examples/README.md b/examples/README.md index cadf2f7a54..e2e3bbf426 100644 --- a/examples/README.md +++ b/examples/README.md @@ -491,6 +491,7 @@ Example | Description [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI +[UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. diff --git a/examples/ui/ui_texture_slice_flip_and_tile.rs b/examples/ui/ui_texture_slice_flip_and_tile.rs new file mode 100644 index 0000000000..5d295ade57 --- /dev/null +++ b/examples/ui/ui_texture_slice_flip_and_tile.rs @@ -0,0 +1,78 @@ +//! This example illustrates how to how to flip and tile images with 9-slicing in the UI. + +use bevy::{prelude::*, winit::WinitSettings}; +use bevy_render::texture::{ImageLoaderSettings, ImageSampler}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(UiScale(2.)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use for UI-only apps. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let image = asset_server.load_with_settings( + "textures/fantasy_ui_borders/numbered_slices.png", + |settings: &mut ImageLoaderSettings| { + // Need to use nearest filtering to avoid bleeding between the slices with tiling + settings.sampler = ImageSampler::nearest(); + }, + ); + + let slicer = TextureSlicer { + // `numbered_slices.png` is 48 pixels square. `BorderRect::square(16.)` insets the slicing line from each edge by 16 pixels, resulting in nine slices that are each 16 pixels square. + border: BorderRect::square(16.), + // With `SliceScaleMode::Tile` the side and center slices are tiled to to fill the side and center sections of the target. + // And with a `stretch_value` of `1.` the tiles will have the same size as the corresponding slices in the source image. + center_scale_mode: SliceScaleMode::Tile { stretch_value: 1. }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 1. }, + ..default() + }; + + // ui camera + commands.spawn(Camera2dBundle::default()); + + commands + .spawn(NodeBundle { + style: Style { + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_content: AlignContent::Center, + flex_wrap: FlexWrap::Wrap, + column_gap: Val::Px(10.), + row_gap: Val::Px(10.), + ..default() + }, + ..default() + }) + .with_children(|parent| { + for ([width, height], flip_x, flip_y) in [ + ([160., 160.], false, false), + ([320., 160.], false, true), + ([320., 160.], true, false), + ([160., 160.], true, true), + ] { + parent.spawn(( + NodeBundle { + style: Style { + width: Val::Px(width), + height: Val::Px(height), + ..default() + }, + ..Default::default() + }, + UiImage { + texture: image.clone(), + flip_x, + flip_y, + ..Default::default() + }, + ImageScaleMode::Sliced(slicer.clone()), + )); + } + }); +}