
# Objective - Bevy 0.15 added support for custom cursor images in https://github.com/bevyengine/bevy/pull/14284. - However, to do animated cursors using the initial support shipped in 0.15 means you'd have to animate the `Handle<Image>`: You can't use a `TextureAtlas` like you can with sprites and UI images. - For my use case, my cursors are spritesheets. To animate them, I'd have to break them down into multiple `Image` assets, but that seems less than ideal. ## Solution - Allow users to specify a `TextureAtlas` field when creating a custom cursor image. - To create parity with Bevy's `TextureAtlas` support on `Sprite`s and `ImageNode`s, this also allows users to specify `rect`, `flip_x` and `flip_y`. In fact, for my own use case, I need to `flip_y`. ## Testing - I added unit tests for `calculate_effective_rect` and `extract_and_transform_rgba_pixels`. - I added a brand new example for custom cursor images. It has controls to toggle fields on and off. I opted to add a new example because the existing cursor example (`window_settings`) would be far too messy for showcasing these custom cursor features (I did start down that path but decided to stop and make a brand new example). - The new example uses a [Kenny cursor icon] sprite sheet. I included the licence even though it's not required (and it's CC0). - I decided to make the example just loop through all cursor icons for its animation even though it's not a _realistic_ in-game animation sequence. - I ran the PNG through https://tinypng.com. Looks like it's about 35KB. - I'm open to adjusting the example spritesheet if required, but if it's fine as is, great. [Kenny cursor icon]: https://kenney-assets.itch.io/crosshair-pack --- ## Showcase https://github.com/user-attachments/assets/8f6be8d7-d1d4-42f9-b769-ef8532367749 ## Migration Guide The `CustomCursor::Image` enum variant has some new fields. Update your code to set them. Before: ```rust CustomCursor::Image { handle: asset_server.load("branding/icon.png"), hotspot: (128, 128), } ``` After: ```rust CustomCursor::Image { handle: asset_server.load("branding/icon.png"), texture_atlas: None, flip_x: false, flip_y: false, rect: None, hotspot: (128, 128), } ``` ## References - Feature request [originally raised in Discord]. [originally raised in Discord]: https://discord.com/channels/691052431525675048/692572690833473578/1319836362219847681
214 lines
7.4 KiB
Rust
214 lines
7.4 KiB
Rust
use bevy_app::prelude::*;
|
|
use bevy_asset::{Asset, AssetApp as _, AssetId, Assets, Handle};
|
|
use bevy_math::{URect, UVec2};
|
|
#[cfg(feature = "bevy_reflect")]
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
#[cfg(feature = "serialize")]
|
|
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
|
use bevy_utils::HashMap;
|
|
|
|
use crate::Image;
|
|
|
|
/// Adds support for texture atlases.
|
|
pub struct TextureAtlasPlugin;
|
|
|
|
impl Plugin for TextureAtlasPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_asset::<TextureAtlasLayout>();
|
|
|
|
#[cfg(feature = "bevy_reflect")]
|
|
app.register_asset_reflect::<TextureAtlasLayout>()
|
|
.register_type::<TextureAtlas>();
|
|
}
|
|
}
|
|
|
|
/// Stores a mapping from sub texture handles to the related area index.
|
|
///
|
|
/// Generated by [`TextureAtlasBuilder`].
|
|
///
|
|
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
|
|
#[derive(Debug)]
|
|
pub struct TextureAtlasSources {
|
|
/// Maps from a specific image handle to the index in `textures` where they can be found.
|
|
pub texture_ids: HashMap<AssetId<Image>, usize>,
|
|
}
|
|
impl TextureAtlasSources {
|
|
/// Retrieves the texture *section* index of the given `texture` handle.
|
|
pub fn texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
|
|
let id = texture.into();
|
|
self.texture_ids.get(&id).cloned()
|
|
}
|
|
|
|
/// Creates a [`TextureAtlas`] handle for the given `texture` handle.
|
|
pub fn handle(
|
|
&self,
|
|
layout: Handle<TextureAtlasLayout>,
|
|
texture: impl Into<AssetId<Image>>,
|
|
) -> Option<TextureAtlas> {
|
|
Some(TextureAtlas {
|
|
layout,
|
|
index: self.texture_index(texture)?,
|
|
})
|
|
}
|
|
|
|
/// Retrieves the texture *section* rectangle of the given `texture` handle.
|
|
pub fn texture_rect(
|
|
&self,
|
|
layout: &TextureAtlasLayout,
|
|
texture: impl Into<AssetId<Image>>,
|
|
) -> Option<URect> {
|
|
layout.textures.get(self.texture_index(texture)?).cloned()
|
|
}
|
|
}
|
|
|
|
/// Stores a map used to lookup the position of a texture in a [`TextureAtlas`].
|
|
/// This can be used to either use and look up a specific section of a texture, or animate frame-by-frame as a sprite sheet.
|
|
///
|
|
/// Optionally it can store a mapping from sub texture handles to the related area index (see
|
|
/// [`TextureAtlasBuilder`]).
|
|
///
|
|
/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
|
|
/// [Example usage animating sprite in response to an event.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
|
/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
|
///
|
|
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
|
|
#[derive(Asset, PartialEq, Eq, Debug, Clone)]
|
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
|
#[cfg_attr(
|
|
feature = "serialize",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
reflect(Serialize, Deserialize)
|
|
)]
|
|
pub struct TextureAtlasLayout {
|
|
/// Total size of texture atlas.
|
|
pub size: UVec2,
|
|
/// The specific areas of the atlas where each texture can be found
|
|
pub textures: Vec<URect>,
|
|
}
|
|
|
|
impl TextureAtlasLayout {
|
|
/// Create a new empty layout with custom `dimensions`
|
|
pub fn new_empty(dimensions: UVec2) -> Self {
|
|
Self {
|
|
size: dimensions,
|
|
textures: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Generate a [`TextureAtlasLayout`] as a grid where each
|
|
/// `tile_size` by `tile_size` grid-cell is one of the *section* in the
|
|
/// atlas. Grid cells are separated by some `padding`, and the grid starts
|
|
/// at `offset` pixels from the top left corner. Resulting layout is
|
|
/// indexed left to right, top to bottom.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `tile_size` - Each layout grid cell size
|
|
/// * `columns` - Grid column count
|
|
/// * `rows` - Grid row count
|
|
/// * `padding` - Optional padding between cells
|
|
/// * `offset` - Optional global grid offset
|
|
pub fn from_grid(
|
|
tile_size: UVec2,
|
|
columns: u32,
|
|
rows: u32,
|
|
padding: Option<UVec2>,
|
|
offset: Option<UVec2>,
|
|
) -> Self {
|
|
let padding = padding.unwrap_or_default();
|
|
let offset = offset.unwrap_or_default();
|
|
let mut sprites = Vec::new();
|
|
let mut current_padding = UVec2::ZERO;
|
|
|
|
for y in 0..rows {
|
|
if y > 0 {
|
|
current_padding.y = padding.y;
|
|
}
|
|
for x in 0..columns {
|
|
if x > 0 {
|
|
current_padding.x = padding.x;
|
|
}
|
|
|
|
let cell = UVec2::new(x, y);
|
|
let rect_min = (tile_size + current_padding) * cell + offset;
|
|
|
|
sprites.push(URect {
|
|
min: rect_min,
|
|
max: rect_min + tile_size,
|
|
});
|
|
}
|
|
}
|
|
|
|
let grid_size = UVec2::new(columns, rows);
|
|
|
|
Self {
|
|
size: ((tile_size + current_padding) * grid_size) - current_padding,
|
|
textures: sprites,
|
|
}
|
|
}
|
|
|
|
/// Add a *section* to the list in the layout and returns its index
|
|
/// which can be used with [`TextureAtlas`]
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `rect` - The section of the texture to be added
|
|
///
|
|
/// [`TextureAtlas`]: crate::TextureAtlas
|
|
pub fn add_texture(&mut self, rect: URect) -> usize {
|
|
self.textures.push(rect);
|
|
self.textures.len() - 1
|
|
}
|
|
|
|
/// The number of textures in the [`TextureAtlasLayout`]
|
|
pub fn len(&self) -> usize {
|
|
self.textures.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.textures.is_empty()
|
|
}
|
|
}
|
|
|
|
/// An index into a [`TextureAtlasLayout`], which corresponds to a specific section of a texture.
|
|
///
|
|
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
|
|
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
|
|
/// image file for either sprite animation or global mapping.
|
|
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
|
|
/// for efficient rendering of related game objects.
|
|
///
|
|
/// Check the following examples for usage:
|
|
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
|
|
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
|
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
|
#[cfg_attr(
|
|
feature = "bevy_reflect",
|
|
derive(Reflect),
|
|
reflect(Default, Debug, PartialEq, Hash)
|
|
)]
|
|
pub struct TextureAtlas {
|
|
/// Texture atlas layout handle
|
|
pub layout: Handle<TextureAtlasLayout>,
|
|
/// Texture atlas section index
|
|
pub index: usize,
|
|
}
|
|
|
|
impl TextureAtlas {
|
|
/// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
|
|
pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
|
|
let atlas = texture_atlases.get(&self.layout)?;
|
|
atlas.textures.get(self.index).copied()
|
|
}
|
|
}
|
|
|
|
impl From<Handle<TextureAtlasLayout>> for TextureAtlas {
|
|
fn from(texture_atlas: Handle<TextureAtlasLayout>) -> Self {
|
|
Self {
|
|
layout: texture_atlas,
|
|
index: 0,
|
|
}
|
|
}
|
|
}
|