use bevy::asset::RenderAssetUsages;
use bevy::{
asset::{AssetLoader, LoadContext, io::Reader},
image::ImageSampler,
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
};
#[derive(Asset, Debug, Clone, Reflect)]
pub struct Palette {
pub image: Handle<Image>,
pub access: PaletteAccess,
}
#[derive(Debug, Default, Clone, Copy, Reflect)]
pub enum PaletteAccess {
#[default]
LinearByRow,
LinearByColumn,
FromRow(u32),
FromColumn(u32),
}
pub fn palette_len(access: &PaletteAccess, width: u32, height: u32) -> usize {
match access {
PaletteAccess::LinearByRow | PaletteAccess::LinearByColumn => {
(width as usize).saturating_mul(height as usize)
}
PaletteAccess::FromRow(_) => width as usize,
PaletteAccess::FromColumn(_) => height as usize,
}
}
pub fn palette_index_to_xy(
access: &PaletteAccess,
width: u32,
height: u32,
index: usize,
) -> Option<(u32, u32)> {
let w = width as usize;
let h = height as usize;
match access {
PaletteAccess::LinearByRow => {
if index >= w * h {
return None;
}
Some(((index % w) as u32, (index / w) as u32))
}
PaletteAccess::LinearByColumn => {
if index >= w * h {
return None;
}
Some(((index / h) as u32, (index % h) as u32))
}
PaletteAccess::FromRow(row) => {
if index >= w || *row >= height {
return None;
}
Some((index as u32, *row))
}
PaletteAccess::FromColumn(col) => {
if index >= h || *col >= width {
return None;
}
Some((*col, index as u32))
}
}
}
pub const PALETTE_ACCESS_LINEAR_BY_ROW: u32 = 0;
pub const PALETTE_ACCESS_LINEAR_BY_COLUMN: u32 = 1;
pub const PALETTE_ACCESS_FROM_ROW: u32 = 2;
pub const PALETTE_ACCESS_FROM_COLUMN: u32 = 3;
pub fn palette_access_to_gpu(access: &PaletteAccess) -> (u32, u32) {
match access {
PaletteAccess::LinearByRow => (PALETTE_ACCESS_LINEAR_BY_ROW, 0),
PaletteAccess::LinearByColumn => (PALETTE_ACCESS_LINEAR_BY_COLUMN, 0),
PaletteAccess::FromRow(r) => (PALETTE_ACCESS_FROM_ROW, *r),
PaletteAccess::FromColumn(c) => (PALETTE_ACCESS_FROM_COLUMN, *c),
}
}
#[allow(clippy::enum_variant_names)]
#[derive(thiserror::Error, Debug)]
pub enum PalError {
#[error("no such palette {index} only {count} palettes")]
NoSuchPalette { index: usize, count: usize },
#[error("no such color {0}")]
NoSuchColor(usize),
#[error("no such color {color} in palette {palette}")]
NoSuchPaletteColor { color: usize, palette: usize },
#[error("no Pico8Asset")]
NoPico8Asset,
}
pub(crate) fn plugin(app: &mut App) {
app.init_asset::<Palette>()
.init_asset_loader::<PaletteLoader>();
}
pub(crate) fn strip_image_from_data(data: &[[u8; 4]]) -> Image {
let n = data.len();
let pixel_bytes: Vec<u8> = data.iter().flat_map(|c| c.iter().copied()).collect();
let mut image = Image::new(
Extent3d {
width: n as u32,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
pixel_bytes,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
);
image.sampler = ImageSampler::nearest();
image
}
pub fn palette_data_from_image(image: &Image, access: &PaletteAccess) -> Vec<[u8; 4]> {
let size = image.size();
let n = palette_len(access, size.x, size.y);
let mut data = Vec::with_capacity(n);
for index in 0..n {
if let Some((x, y)) = palette_index_to_xy(access, size.x, size.y, index)
&& let Ok(color) = image.get_color_at(x, y)
{
let srgba: Srgba = color.into();
data.push(srgba.to_u8_array());
}
}
data
}
impl Palette {
pub fn image(&self) -> &Handle<Image> {
&self.image
}
pub fn len_in(&self, image: &Image) -> usize {
let size = image.size();
palette_len(&self.access, size.x, size.y)
}
pub fn get_color_in(&self, index: usize, image: &Image) -> Result<Srgba, PalError> {
let size = image.size();
let (x, y) = palette_index_to_xy(&self.access, size.x, size.y, index)
.ok_or(PalError::NoSuchColor(index))?;
image
.get_color_at(x, y)
.map(|c| c.into())
.map_err(|_| PalError::NoSuchColor(index))
}
pub fn write_color_in(
&self,
index: usize,
image: &Image,
pixel_bytes: &mut [u8],
) -> Result<(), PalError> {
let color = self.get_color_in(index, image)?;
let arr = color.to_u8_array();
pixel_bytes.copy_from_slice(&arr[0..pixel_bytes.len()]);
Ok(())
}
pub fn from_png_palette(bytes: &[u8]) -> Result<Option<Vec<[u8; 4]>>, png::DecodingError> {
let cursor = std::io::Cursor::new(bytes);
let decoder = png::Decoder::new(cursor);
let reader = decoder.read_info()?;
let info = reader.info();
Ok(Self::from_png_palette_info(info))
}
pub fn from_png_palette_info(info: &png::Info<'static>) -> Option<Vec<[u8; 4]>> {
info.palette.as_ref().map(|png_palette| {
let colors = png_palette.chunks(3);
let mut data = vec![[0x00, 0x00, 0x00, 0xff]; colors.len()];
for (i, rgb) in colors.enumerate() {
data[i][0..3].copy_from_slice(rgb);
}
data
})
}
pub fn from_image_column(image: &Image, column: u32) -> Vec<[u8; 4]> {
let size = image.size();
let mut data = vec![[0; 4]; size.y as usize];
for j in 0..size.y {
let color: Srgba = image.get_color_at(column, j).unwrap().into();
data[j as usize] = color.to_u8_array();
}
data
}
pub fn from_image_row(image: &Image, row: u32) -> Vec<[u8; 4]> {
let size = image.size();
let mut data = vec![[0; 4]; size.x as usize];
for i in 0..size.x {
let color: Srgba = image.get_color_at(i, row).unwrap().into();
data[i as usize] = color.to_u8_array();
}
data
}
pub fn from_image(image: &Image) -> Vec<[u8; 4]> {
let size = image.size();
let mut data = vec![[0; 4]; (size.x * size.y) as usize];
for j in 0..size.y {
for i in 0..size.x {
let color: Srgba = image.get_color_at(i, j).unwrap().into();
data[(j * size.x + i) as usize] = color.to_u8_array();
}
}
data
}
pub fn from_slice_with_context(slice: &[[u8; 4]], load_context: &mut LoadContext<'_>) -> Self {
let strip = strip_image_from_data(slice);
let image = load_context.add_labeled_asset("palette_image".into(), strip);
Palette {
image,
access: PaletteAccess::default(),
}
}
}
#[derive(Default, bevy::reflect::TypePath)]
struct PaletteLoader;
#[allow(clippy::enum_variant_names)]
#[derive(Default, serde::Serialize, serde::Deserialize, Clone, Copy)]
pub enum PaletteSettings {
#[default]
FromImage,
FromIndex,
FromRow(u32),
FromColumn(u32),
}
#[derive(Debug, thiserror::Error)]
pub enum PaletteError {
#[error("No color palette, not an indexed image")]
NoIndex,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("Could not read asset: {0}")]
AssetBytes(#[from] bevy::asset::ReadAssetBytesError),
#[error("Decoding error: {0}")]
Decoding(#[from] png::DecodingError),
#[error("Could not load image: {0}")]
Image(#[from] bevy::image::ImageLoaderError),
}
impl AssetLoader for PaletteLoader {
type Asset = Palette;
type Settings = PaletteSettings;
type Error = PaletteError;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
if matches!(settings, PaletteSettings::FromIndex) {
let mut bytes = Vec::new();
let _ = reader.read_to_end(&mut bytes).await?;
let data = Palette::from_png_palette(&bytes)?.ok_or(PaletteError::NoIndex)?;
let strip = strip_image_from_data(&data);
trace!(
"Loading palette from image for path {:?}",
load_context.path()
);
let image = load_context.add_labeled_asset("palette_image".into(), strip);
return Ok(Palette {
image,
access: PaletteAccess::default(),
});
}
let loader = bevy::image::ImageLoader::new(bevy::image::CompressedImageFormats::all());
let mut image_settings = bevy::image::ImageLoaderSettings::default();
if let Some(sampler) = crate::pico8::image::image_sampler() {
image_settings.sampler = sampler;
}
let mut image_context = load_context.begin_labeled_asset();
let image = loader
.load(reader, &image_settings, &mut image_context)
.await?;
let access = match settings {
PaletteSettings::FromIndex => unreachable!(),
PaletteSettings::FromImage => PaletteAccess::LinearByRow,
PaletteSettings::FromRow(row) => PaletteAccess::FromRow(*row),
PaletteSettings::FromColumn(column) => PaletteAccess::FromColumn(*column),
};
let image_handle = load_context.add_labeled_asset("palette_image".into(), image);
Ok(Palette {
image: image_handle,
access,
})
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &["png"];
EXTENSIONS
}
}