diff --git a/assets/frame/runner.px_frame.png b/assets/frame/runner.px_frame.png new file mode 100644 index 0000000..5ae397d Binary files /dev/null and b/assets/frame/runner.px_frame.png differ diff --git a/assets/frame/runner.px_frame.png.meta b/assets/frame/runner.px_frame.png.meta new file mode 100644 index 0000000..91f0f83 --- /dev/null +++ b/assets/frame/runner.px_frame.png.meta @@ -0,0 +1,14 @@ +( + meta_format_version: "1.0", + asset: Load( + loader: "seldom_pixel::frame::PxFrameLoader", + settings: ( + image_loader_settings: ( + format: FromExtension, + is_srgb: true, + sampler: Default, + asset_usage: ("MAIN_WORLD | RENDER_WORLD"), + ), + ), + ), +) diff --git a/examples/frame.rs b/examples/frame.rs new file mode 100644 index 0000000..31cd76e --- /dev/null +++ b/examples/frame.rs @@ -0,0 +1,46 @@ +// In this program, two frames are spawned + +use bevy::prelude::*; +use seldom_pixel::{frame::PxFrameBundle, prelude::*}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + resolution: Vec2::new(512., 384.).into(), + ..default() + }), + ..default() + }), + PxPlugin::::new(UVec2::new(32, 24), "palette/palette_1.palette.png"), + )) + .insert_resource(ClearColor(Color::BLACK)) + .add_systems(Startup, init) + .run(); +} + +fn init(assets: Res, mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + commands.spawn(PxFrameBundle:: { + frame: assets.load("frame/runner.px_frame.png"), + offset: UVec2::new(2, 0).into(), + size: UVec2::new(10, 15).into(), + position: IVec2::new(1, 1).into(), + anchor: PxAnchor::BottomLeft, + ..default() + }); + + commands.spawn(PxFrameBundle:: { + frame: assets.load("frame/runner.px_frame.png"), + offset: UVec2::new(0, 18).into(), + size: UVec2::new(11, 16).into(), + position: IVec2::new(24, 20).into(), + anchor: PxAnchor::TopCenter, + ..default() + }); +} + +#[px_layer] +struct Layer; diff --git a/src/animation.rs b/src/animation.rs index 7316df3..3b97367 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -5,7 +5,7 @@ use std::time::Duration; use bevy::render::extract_resource::{ExtractResource, ExtractResourcePlugin}; use bevy::utils::Instant; -use crate::position::Spatial; +use crate::position::{PxOffset, PxSize, Spatial}; use crate::{ image::{PxImage, PxImageSliceMut}, pixel::Pixel, @@ -135,6 +135,19 @@ pub(crate) trait Animation { ); } +pub(crate) trait Drawable { + type Param; + + fn draw( + &self, + param: Self::Param, + image: &mut PxImageSliceMut, + offset: UVec2, + size: UVec2, + filter: impl Fn(u8) -> u8, + ); +} + pub(crate) trait AnimationAsset: Asset { fn max_frame_count(&self) -> usize; } @@ -259,6 +272,40 @@ pub(crate) fn draw_animation<'a, A: Animation>( } } +pub(crate) fn draw_frame<'a, D: Drawable>( + drawable: &D, + param: ::Param, + image: &mut PxImage, + position: PxPosition, + PxOffset(offset): PxOffset, + PxSize(size): PxSize, + anchor: PxAnchor, + canvas: PxCanvas, + filters: impl IntoIterator, + camera: PxCamera, +) { + // let size = spatial.frame_size(); + let position = *position - anchor.pos(size).as_ivec2(); + let position = match canvas { + PxCanvas::World => position - *camera, + PxCanvas::Camera => position, + }; + + let mut image_slice = image.slice_mut(IRect { + min: position, + max: position + size.as_ivec2(), + }); + + let mut filter: Box u8> = Box::new(|pixel| pixel); + for filter_part in filters { + let filter_part = filter_part.as_fn(); + filter = Box::new(move |pixel| filter_part(filter(pixel))); + } + + // drawable.draw(param, &mut image_slice, |_| 0, filter); + drawable.draw(param, &mut image_slice, offset, size, filter); +} + pub(crate) fn draw_spatial<'a, A: Animation + Spatial>( spatial: &A, param: ::Param, diff --git a/src/frame.rs b/src/frame.rs new file mode 100644 index 0000000..0d51d12 --- /dev/null +++ b/src/frame.rs @@ -0,0 +1,556 @@ +//! Frames + +use anyhow::{Error, Result}; +use bevy::{ + asset::{io::Reader, AssetLoader, LoadContext}, + render::{ + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, + texture::{ImageLoader, ImageLoaderSettings}, + Extract, RenderApp, + }, + tasks::{ComputeTaskPool, ParallelSliceMut}, +}; +use kiddo::{ImmutableKdTree, SquaredEuclidean}; +use serde::{Deserialize, Serialize}; + +use crate::{ + animation::{AnimationComponents, Drawable}, + image::{PxImage, PxImageSliceMut}, + palette::{asset_palette, PaletteParam}, + pixel::Pixel, + position::{PxLayer, PxOffset, PxSize, Spatial}, + prelude::*, +}; + +pub(crate) fn plug(app: &mut App) { + app.add_plugins(RenderAssetPlugin::::default()) + .init_asset::() + .init_asset_loader::() + .add_systems(PostUpdate, image_to_frame) + .sub_app_mut(RenderApp) + .add_systems(ExtractSchedule, extract_frames::); +} + +#[derive(Serialize, Deserialize)] +struct PxFrameLoaderSettings { + image_loader_settings: ImageLoaderSettings, +} + +impl Default for PxFrameLoaderSettings { + fn default() -> Self { + Self { + image_loader_settings: default(), + } + } +} + +struct PxFrameLoader(ImageLoader); + +impl FromWorld for PxFrameLoader { + fn from_world(world: &mut World) -> Self { + Self(ImageLoader::from_world(world)) + } +} + +impl AssetLoader for PxFrameLoader { + type Asset = PxFrame; + type Settings = PxFrameLoaderSettings; + type Error = Error; + + async fn load<'a>( + &'a self, + reader: &'a mut Reader<'_>, + settings: &'a PxFrameLoaderSettings, + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let Self(image_loader) = self; + let image = image_loader + .load(reader, &settings.image_loader_settings, load_context) + .await?; + let palette = asset_palette().await; + let data = PxImage::palette_indices(palette, &image)?; + + Ok(PxFrame { data }) + } + + fn extensions(&self) -> &[&str] { + &["px_frame.png"] + } +} + +/// A frame. Create a [`Handle`] with a [`PxAssets`] and an image. +/// If the frame is animated, the frames should be laid out from bottom to top. +/// See `assets/frame/runner.png` for an example of an animated frame. +#[derive(Asset, Serialize, Deserialize, Clone, Reflect, Debug)] +pub struct PxFrame { + // TODO Use 0 for transparency + pub(crate) data: PxImage>, +} + +impl RenderAsset for PxFrame { + type SourceAsset = Self; + type Param = (); + + fn prepare_asset( + source_asset: Self, + &mut (): &mut (), + ) -> Result> { + Ok(source_asset) + } +} + +impl Drawable for PxFrame { + type Param = (); + + fn draw( + &self, + _: (), + canvas_slice: &mut PxImageSliceMut, + offset: UVec2, + size: UVec2, + filter: impl Fn(u8) -> u8, + ) { + let image_width = self.data.width(); + let image_height = self.data.height(); + + let offset_x = offset.x; + let offset_y = offset.y; + let size_x = size.x as usize; + let size_y = size.y as usize; + + if offset_x as usize + size_x > image_width as usize { + eprintln!( + "Error: Requested offset + size on X axis ({} + {}) exceeds the image width ({})", + offset_x, size_x, image_width + ); + return; + } + + if offset_y as usize + size_y > image_height as usize { + eprintln!( + "Error: Requested offset + size on Y axis ({} + {}) exceeds the image height ({})", + offset_y, size_y, image_height + ); + return; + } + + canvas_slice.for_each_mut(|slice_i, _, pixel| { + let slice_x = (slice_i % size_x) as u32; + let slice_y = (slice_i / size_x) as u32; + + let pixel_pos = IVec2::new(slice_x + offset_x, slice_y + offset_y); + + if let Some(Some(value)) = self.data.get_pixel(pixel_pos) { + pixel.set_value(filter(value)); + } + }); + } +} + +/// Spawns a sprite +#[derive(Bundle, Debug, Default)] +pub struct PxFrameBundle { + /// A [`Handle`] component + pub frame: Handle, + /// A [`PxOffset`] component + pub offset: PxOffset, + /// A [`PxSize`] component + pub size: PxSize, + /// A [`PxPosition`] component + pub position: PxPosition, + /// A [`PxAnchor`] component + pub anchor: PxAnchor, + /// A layer component + pub layer: L, + /// A [`PxCanvas`] component + pub canvas: PxCanvas, + /// A [`Visibility`] component + pub visibility: Visibility, + /// An [`InheritedVisibility`] component + pub inherited_visibility: InheritedVisibility, +} + +fn srgb_to_linear(c: f32) -> f32 { + if c >= 0.04045 { + ((c + 0.055) / (1. + 0.055)).powf(2.4) + } else { + c / 12.92 + } +} + +#[allow(clippy::excessive_precision)] +fn srgb_to_oklab(rd: f32, gn: f32, bu: f32) -> (f32, f32, f32) { + let rd = srgb_to_linear(rd); + let gn = srgb_to_linear(gn); + let bu = srgb_to_linear(bu); + + let l = 0.4122214708 * rd + 0.5363325363 * gn + 0.0514459929 * bu; + let m = 0.2119034982 * rd + 0.6806995451 * gn + 0.1073969566 * bu; + let s = 0.0883024619 * rd + 0.2817188376 * gn + 0.6299787005 * bu; + + let lp = l.cbrt(); + let mp = m.cbrt(); + let sp = s.cbrt(); + + ( + 0.2104542553 * lp + 0.7936177850 * mp - 0.0040720468 * sp, + 1.9779984951 * lp - 2.4285922050 * mp + 0.4505937099 * sp, + 0.0259040371 * lp + 0.7827717662 * mp - 0.8086757660 * sp, + ) +} + +/// Size of threshold map to use for dithering. The image is tiled with dithering according to this +/// map, so smaller sizes will have more visible repetition and worse color approximation, but +/// larger sizes are much, much slower with pattern dithering. +#[derive(Clone, Copy)] +pub enum ThresholdMap { + /// 2x2 + X2_2, + /// 4x4 + X4_4, + /// 8x8 + X8_8, +} + +/// Dithering algorithm. Perf measurements are for 10,000 pixels with a 4x4 threshold map on a +/// pretty old machine. +#[derive(Clone, Copy)] +pub enum DitherAlgorithm { + /// Almost as fast as undithered. 16.0 ms in debug mode and 1.23 ms in release mode. Doesn't + /// make very good use of the color palette. + Ordered, + /// Slow, but mixes colors very well. 219 ms in debug mode and 6.81 ms in release mode. Consider + /// only using this algorithm with some optimizations enabled. + Pattern, +} + +/// Info needed to dither an image +pub struct Dither { + /// Dithering algorithm + pub algorithm: DitherAlgorithm, + /// How much to dither. Lower values leave solid color areas. Should range from 0 to 1. + pub threshold: f32, + /// Threshold map size + pub threshold_map: ThresholdMap, +} + +/// Renders the contents of an image to a sprite every tick. The image is interpreted as +/// `Rgba8UnormSrgb`. +#[derive(Component)] +pub struct ImageToFrame { + /// Image to render + pub image: Handle, + /// Dithering + pub dither: Option, +} + +trait MapSize { + const WIDTH: usize; + const MAP: [usize; SIZE]; +} + +impl MapSize<1> for () { + const WIDTH: usize = 1; + const MAP: [usize; 1] = [0]; +} + +impl MapSize<4> for () { + const WIDTH: usize = 2; + #[rustfmt::skip] + const MAP: [usize; 4] = [ + 0, 2, + 3, 1, + ]; +} + +impl MapSize<16> for () { + const WIDTH: usize = 4; + #[rustfmt::skip] + const MAP: [usize; 16] = [ + 0, 8, 2, 10, + 12, 4, 14, 6, + 3, 11, 1, 9, + 15, 7, 13, 5, + ]; +} + +impl MapSize<64> for () { + const WIDTH: usize = 8; + #[rustfmt::skip] + const MAP: [usize; 64] = [ + 0, 48, 12, 60, 3, 51, 15, 63, + 32, 16, 44, 28, 35, 19, 47, 31, + 8, 56, 4, 52, 11, 59, 7, 55, + 40, 24, 36, 20, 43, 27, 39, 23, + 2, 50, 14, 62, 1, 49, 13, 61, + 34, 18, 46, 30, 33, 17, 45, 29, + 10, 58, 6, 54, 9, 57, 5, 53, + 42, 26, 38, 22, 41, 25, 37, 21, + ]; +} + +trait Algorithm { + fn compute( + color: Vec3, + threshold: Vec3, + threshold_index: usize, + candidates: &mut [usize; MAP_SIZE], + palette_tree: &ImmutableKdTree, + palette: &[Vec3], + ) -> u8; +} + +enum ClosestAlg {} + +impl Algorithm for ClosestAlg { + fn compute( + color: Vec3, + _: Vec3, + _: usize, + _: &mut [usize; MAP_SIZE], + palette_tree: &ImmutableKdTree, + _: &[Vec3], + ) -> u8 { + palette_tree + .approx_nearest_one::(&color.into()) + .item as usize as u8 + } +} + +enum OrderedAlg {} + +impl Algorithm for OrderedAlg { + fn compute( + color: Vec3, + threshold: Vec3, + threshold_index: usize, + _: &mut [usize; MAP_SIZE], + palette_tree: &ImmutableKdTree, + _: &[Vec3], + ) -> u8 { + palette_tree + .approx_nearest_one::( + &(color + threshold * (threshold_index as f32 / MAP_SIZE as f32 - 0.5)).into(), + ) + .item as u8 + } +} + +enum PatternAlg {} + +impl Algorithm for PatternAlg { + fn compute( + color: Vec3, + threshold: Vec3, + threshold_index: usize, + candidates: &mut [usize; MAP_SIZE], + palette_tree: &ImmutableKdTree, + palette: &[Vec3], + ) -> u8 { + let mut error = Vec3::ZERO; + for candidate_ref in &mut *candidates { + let sample = color + error * threshold; + let candidate = palette_tree + .approx_nearest_one::(&sample.into()) + .item as usize; + + *candidate_ref = candidate; + error += color - palette[candidate]; + } + + candidates.sort_unstable_by(|&candidate_1, &candidate_2| { + palette[candidate_1][0].total_cmp(&palette[candidate_2][0]) + }); + + candidates[threshold_index] as u8 + } +} + +fn dither_slice, const MAP_SIZE: usize>( + pixels: &mut [(usize, (&[u8], &mut Option))], + threshold: f32, + size: UVec2, + palette_tree: &ImmutableKdTree, + palette: &[Vec3], +) where + (): MapSize, +{ + let mut candidates = [0; MAP_SIZE]; + + for &mut (i, (color, ref mut pixel)) in pixels { + let i = i as u32; + let pos = UVec2::new(i % size.x, i / size.x); + + if color[3] == 0 { + **pixel = None; + continue; + } + + **pixel = Some(A::compute( + Vec3::from(srgb_to_oklab( + color[0] as f32 / 255., + color[1] as f32 / 255., + color[2] as f32 / 255., + )), + Vec3::splat(threshold), + <() as MapSize>::MAP[pos.x as usize % <() as MapSize>::WIDTH + * <() as MapSize>::WIDTH + + pos.y as usize % <() as MapSize>::WIDTH], + &mut candidates, + palette_tree, + palette, + )); + } +} + +// TODO Use more helpers +// TODO Feature gate +// TODO Immediate function version +fn image_to_frame( + mut to_frames: Query<(&ImageToFrame, &mut Handle)>, + images: Res>, + palette: PaletteParam, + mut sprites: ResMut>, +) { + if to_frames.iter().next().is_none() { + return; + } + + let Some(palette) = palette.get() else { + return; + }; + + let palette = palette + .colors + .iter() + .map(|&[r, g, b]| srgb_to_oklab(r as f32 / 255., g as f32 / 255., b as f32 / 255.).into()) + .collect::>(); + + let palette_tree = ImmutableKdTree::from( + &palette + .iter() + .map(|&color| color.into()) + .collect::>()[..], + ); + + to_frames.iter_mut().for_each(|(image, mut sprite)| { + let dither = &image.dither; + let image = images.get(&image.image).unwrap(); + + if *sprite == Handle::default() { + let data = PxImage::empty_from_image(image); + + *sprite = sprites.add(PxFrame { data }); + } + + let sprite = sprites.get_mut(&*sprite).unwrap(); + + let size = image.texture_descriptor.size; + let size = UVec2::new(size.width, size.height); + if sprite.data.size() != size { + let data = PxImage::empty_from_image(image); + + // sprite.frame_size = data.area(); + sprite.data = data; + } + + let mut pixels = image + .data + .chunks_exact(4) + .zip(sprite.data.iter_mut()) + .enumerate() + .collect::>(); + + pixels.par_chunk_map_mut(ComputeTaskPool::get(), 20, |_, pixels| { + use DitherAlgorithm::*; + use ThresholdMap::*; + + match *dither { + None => dither_slice::(pixels, 0., size, &palette_tree, &palette), + Some(Dither { + algorithm: Ordered, + threshold, + threshold_map: X2_2, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + Some(Dither { + algorithm: Ordered, + threshold, + threshold_map: X4_4, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + Some(Dither { + algorithm: Ordered, + threshold, + threshold_map: X8_8, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + Some(Dither { + algorithm: Pattern, + threshold, + threshold_map: X2_2, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + Some(Dither { + algorithm: Pattern, + threshold, + threshold_map: X4_4, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + Some(Dither { + algorithm: Pattern, + threshold, + threshold_map: X8_8, + }) => { + dither_slice::(pixels, threshold, size, &palette_tree, &palette) + } + } + }); + }); +} + +pub(crate) type FrameComponents = ( + &'static Handle, + &'static PxPosition, + &'static PxOffset, + &'static PxSize, + &'static PxAnchor, + &'static L, + &'static PxCanvas, + Option<&'static Handle>, +); + +fn extract_frames( + frames: Extract, &InheritedVisibility)>>, + mut cmd: Commands, +) { + for ((frame, &position, &offset, &size, &anchor, layer, &canvas, filter), visibility) in &frames + { + if !visibility.get() { + continue; + } + + let mut frame = cmd.spawn(( + frame.clone(), + position, + offset, + size, + anchor, + layer.clone(), + canvas, + )); + + // if let Some((&direction, &duration, &on_finish, &frame_transition, &start)) = animation { + // frame.insert((direction, duration, on_finish, frame_transition, start)); + // } + + if let Some(filter) = filter { + frame.insert(filter.clone()); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 876a87b..4a7cead 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod button; mod camera; pub mod cursor; pub mod filter; +pub mod frame; mod image; #[cfg(feature = "line")] mod line; @@ -65,6 +66,7 @@ impl Plugin for PxPlugin { camera::plug, cursor::plug, filter::plug::, + frame::plug::, #[cfg(feature = "line")] line::plug::, map::plug::, diff --git a/src/position.rs b/src/position.rs index 23f48e3..cecdca2 100644 --- a/src/position.rs +++ b/src/position.rs @@ -73,6 +73,15 @@ impl Spatial for &'_ T { (*self).frame_size() } } +// The offset of an entity +#[derive(Clone, Component, Copy, Debug, Default, Deref, DerefMut)] +pub struct PxOffset(pub UVec2); + +impl From for PxOffset { + fn from(offset: UVec2) -> Self { + Self(offset) + } +} /// The position of an entity #[derive(ExtractComponent, Component, Deref, DerefMut, Clone, Copy, Default, Debug)] @@ -84,6 +93,16 @@ impl From for PxPosition { } } +// The size of an entity +#[derive(Clone, Component, Copy, Debug, Default, Deref, DerefMut)] +pub struct PxSize(pub UVec2); + +impl From for PxSize { + fn from(size: UVec2) -> Self { + Self(size) + } +} + /// Trait implemented for your game's custom layer type. Use the [`px_layer`] attribute /// or derive/implement the required traits manually. The layers will be rendered in the order /// defined by the [`PartialOrd`] implementation. So, lower values will be in the back diff --git a/src/screen.rs b/src/screen.rs index 56910f8..c10f6f9 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -29,14 +29,15 @@ use bevy::{ #[cfg(feature = "line")] use crate::line::{draw_line, LineComponents}; use crate::{ - animation::{copy_animation_params, draw_spatial, LastUpdate}, + animation::{copy_animation_params, draw_frame, draw_spatial, LastUpdate}, cursor::{CursorState, PxCursorPosition}, filter::{draw_filter, FilterComponents}, + frame::{FrameComponents, PxFrame}, image::{PxImage, PxImageSliceMut}, map::{MapComponents, PxTile, TileComponents}, math::RectExt, palette::{PaletteHandle, PaletteParam}, - position::PxLayer, + position::{PxLayer, PxSize}, prelude::*, sprite::SpriteComponents, text::TextComponents, @@ -282,6 +283,7 @@ struct PxRender; struct PxRenderNode { maps: QueryState>, tiles: QueryState, + frames: QueryState>, sprites: QueryState>, texts: QueryState>, #[cfg(feature = "line")] @@ -294,6 +296,7 @@ impl FromWorld for PxRenderNode { Self { maps: world.query(), tiles: world.query(), + frames: world.query(), sprites: world.query(), texts: world.query(), #[cfg(feature = "line")] @@ -309,6 +312,7 @@ impl ViewNode for PxRenderNode { fn update(&mut self, world: &mut World) { self.maps.update_archetypes(world); self.tiles.update_archetypes(world); + self.frames.update_archetypes(world); self.sprites.update_archetypes(world); self.texts.update_archetypes(world); #[cfg(feature = "line")] @@ -340,16 +344,27 @@ impl ViewNode for PxRenderNode { ); #[cfg(feature = "line")] - let mut layer_contents = - BTreeMap::<_, (Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>)>::default(); + let mut layer_contents = BTreeMap::< + _, + ( + Vec<_>, + Vec<_>, + Vec<_>, + Vec<_>, + Vec<_>, + Vec<_>, + Vec<_>, + Vec<_>, + ), + >::default(); #[cfg(not(feature = "line"))] let mut layer_contents = - BTreeMap::<_, (Vec<_>, Vec<_>, Vec<_>, (), Vec<_>, (), Vec<_>)>::default(); + BTreeMap::<_, (Vec<_>, Vec<_>, Vec<_>, Vec<_>, (), Vec<_>, (), Vec<_>)>::default(); for (map, tileset, position, layer, canvas, animation, filter) in self.maps.iter_manual(world) { - if let Some((maps, _, _, _, _, _, _)) = layer_contents.get_mut(layer) { + if let Some((maps, _, _, _, _, _, _, _)) = layer_contents.get_mut(layer) { maps.push((map, tileset, position, canvas, animation, filter)); } else { layer_contents.insert( @@ -362,6 +377,7 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), ), ); } @@ -370,7 +386,7 @@ impl ViewNode for PxRenderNode { for (sprite, position, anchor, layer, canvas, animation, filter) in self.sprites.iter_manual(world) { - if let Some((_, sprites, _, _, _, _, _)) = layer_contents.get_mut(layer) { + if let Some((_, sprites, _, _, _, _, _, _)) = layer_contents.get_mut(layer) { sprites.push((sprite, position, anchor, canvas, animation, filter)); } else { layer_contents.insert( @@ -383,6 +399,29 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), + ), + ); + } + } + + for (frame, position, offset, size, anchor, layer, canvas, filter) in + self.frames.iter_manual(world) + { + if let Some((_, _, frames, _, _, _, _, _)) = layer_contents.get_mut(layer) { + frames.push((frame, position, offset, size, anchor, canvas, filter)); + } else { + layer_contents.insert( + layer.clone(), + ( + default(), + default(), + vec![(frame, position, offset, size, anchor, canvas, filter)], + default(), + default(), + default(), + default(), + default(), ), ); } @@ -391,12 +430,13 @@ impl ViewNode for PxRenderNode { for (text, typeface, rect, alignment, layer, canvas, animation, filter) in self.texts.iter_manual(world) { - if let Some((_, _, texts, _, _, _, _)) = layer_contents.get_mut(layer) { + if let Some((_, _, _, texts, _, _, _, _)) = layer_contents.get_mut(layer) { texts.push((text, typeface, rect, alignment, canvas, animation, filter)); } else { layer_contents.insert( layer.clone(), ( + default(), default(), default(), vec![(text, typeface, rect, alignment, canvas, animation, filter)], @@ -439,6 +479,7 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), lines, default(), default(), @@ -451,6 +492,7 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), lines, default(), ) @@ -461,6 +503,7 @@ impl ViewNode for PxRenderNode { } let tilesets = world.resource::>(); + let frame_assets = world.resource::>(); let sprite_assets = world.resource::>(); let typefaces = world.resource::>(); let filters = world.resource::>(); @@ -479,7 +522,7 @@ impl ViewNode for PxRenderNode { } .into_iter() { - if let Some((_, _, _, _, clip_filters, _, over_filters)) = + if let Some((_, _, _, _, _, clip_filters, _, over_filters)) = layer_contents.get_mut(&layer) { if clip { clip_filters } else { over_filters }.push((filter, animation)); @@ -494,6 +537,7 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), filters, default(), default(), @@ -506,6 +550,7 @@ impl ViewNode for PxRenderNode { default(), default(), default(), + default(), filters, ) }, @@ -518,8 +563,10 @@ impl ViewNode for PxRenderNode { let mut image_slice = PxImageSliceMut::from_image_mut(&mut image); #[allow(unused_variables)] - for (_, (maps, sprites, texts, clip_lines, clip_filters, over_lines, over_filters)) in - layer_contents.into_iter() + for ( + _, + (maps, sprites, frames, texts, clip_lines, clip_filters, over_lines, over_filters), + ) in layer_contents.into_iter() { layer_image.clear(); @@ -569,6 +616,25 @@ impl ViewNode for PxRenderNode { } } + for (frame, position, offset, size, anchor, canvas, filter) in frames { + let Some(frame) = frame_assets.get(frame) else { + continue; + }; + + draw_frame( + frame, + (), + &mut layer_image, + *position, + *offset, + *size, + *anchor, + *canvas, + filter.and_then(|filter| filters.get(filter)), + camera, + ); + } + for (sprite, position, anchor, canvas, animation, filter) in sprites { let Some(sprite) = sprite_assets.get(sprite) else { continue;