use std::array;
use std::collections::HashMap;
use std::path::Path;
use image::imageops;
use crate::canvas::{Canvas, CopyArea, ToIndex};
use crate::font::SharedFont;
use crate::pyxel::{COLORS, FONT_IMAGE, IMAGES};
use crate::rect_area::RectArea;
use crate::settings::{
FONT_HEIGHT, FONT_WIDTH, MAX_COLORS, MAX_FONT_CODE, MIN_FONT_CODE, NUM_FONT_ROWS, TILE_SIZE,
};
use crate::tilemap::{ImageSource, SharedTilemap};
use crate::utils;
pub type Rgb24 = u32;
pub type Color = u8;
impl ToIndex for Color {
fn to_index(&self) -> usize {
*self as usize
}
}
#[derive(Clone)]
pub struct Image {
pub(crate) canvas: Canvas<Color>,
pub(crate) palette: [Color; MAX_COLORS as usize],
}
pub type SharedImage = shared_type!(Image);
impl Image {
pub fn new(width: u32, height: u32) -> SharedImage {
new_shared_type!(Self {
canvas: Canvas::new(width, height),
palette: array::from_fn(|i| i as Color),
})
}
pub fn from_image(filename: &str, include_colors: Option<bool>) -> Result<SharedImage, String> {
let include_colors = include_colors.unwrap_or(false);
let mut colors = COLORS.lock();
if include_colors {
colors.clear();
}
let file_image = image::open(Path::new(&filename))
.map_err(|_e| format!("Failed to open file '{filename}'"))?
.to_rgb8();
let (width, height) = file_image.dimensions();
let image = Self::new(width, height);
{
let mut image = image.lock();
let mut color_table = HashMap::<(u8, u8, u8), Color>::new();
for y in 0..height {
for x in 0..width {
let p = file_image.get_pixel(x, y);
let src_rgb = (p[0], p[1], p[2]);
if let Some(color) = color_table.get(&src_rgb) {
image.canvas.write_data(x as usize, y as usize, *color);
} else {
let mut closest_color: Color = 0;
if include_colors {
colors.push(
((src_rgb.0 as Rgb24) << 16)
| ((src_rgb.1 as Rgb24) << 8)
| src_rgb.2 as Rgb24,
);
closest_color = colors.len() as Color - 1;
} else {
let mut closest_dist: f32 = f32::MAX;
for (i, pal_color) in colors.iter().enumerate() {
let pal_rgb = (
(pal_color >> 16) as u8,
(pal_color >> 8) as u8,
*pal_color as u8,
);
let dist = Self::color_dist(src_rgb, pal_rgb);
if dist < closest_dist {
closest_color = i as Color;
closest_dist = dist;
}
}
}
color_table.insert(src_rgb, closest_color);
image
.canvas
.write_data(x as usize, y as usize, closest_color);
}
}
}
}
Ok(image)
}
pub const fn width(&self) -> u32 {
self.canvas.width()
}
pub const fn height(&self) -> u32 {
self.canvas.height()
}
pub fn data_ptr(&mut self) -> *mut Color {
self.canvas.data_ptr()
}
pub fn set(&mut self, x: i32, y: i32, data_str: &[&str]) {
let width = utils::simplify_string(data_str[0]).len() as u32;
let height = data_str.len() as u32;
let image = Self::new(width, height);
{
let mut image = image.lock();
for y in 0..height {
let src_data = utils::simplify_string(data_str[y as usize]);
for x in 0..width {
let color =
utils::parse_hex_string(&src_data[x as usize..=x as usize]).unwrap();
image
.canvas
.write_data(x as usize, y as usize, color as Color);
}
}
}
self.blt(
x as f32,
y as f32,
image,
0.0,
0.0,
width as f32,
height as f32,
None,
None,
None,
);
}
pub fn load(
&mut self,
x: i32,
y: i32,
filename: &str,
include_colors: Option<bool>,
) -> Result<(), String> {
let image = Self::from_image(filename, include_colors)?;
let width = image.lock().width();
let height = image.lock().height();
self.blt(
x as f32,
y as f32,
image,
0.0,
0.0,
width as f32,
height as f32,
None,
None,
None,
);
Ok(())
}
pub fn save(&self, filename: &str, scale: u32) -> Result<(), String> {
let colors = COLORS.lock();
let width = self.width();
let height = self.height();
let mut image = image::RgbImage::new(width, height);
for y in 0..height {
for x in 0..width {
let rgb = colors[self.canvas.read_data(x as usize, y as usize) as usize];
let r = (rgb >> 16) as u8;
let g = (rgb >> 8) as u8;
let b = rgb as u8;
image.put_pixel(x, y, image::Rgb([r, g, b]));
}
}
let image = imageops::resize(
&image,
width * scale,
height * scale,
imageops::FilterType::Nearest,
);
let filename = utils::add_file_extension(filename, ".png");
image
.save(&filename)
.map_err(|_e| format!("Failed to open file '{filename}'"))?;
Ok(())
}
pub fn clip(&mut self, x: f32, y: f32, width: f32, height: f32) {
self.canvas.clip(x, y, width, height);
}
pub fn clip0(&mut self) {
self.canvas.clip0();
}
pub fn camera(&mut self, x: f32, y: f32) {
self.canvas.camera(x, y);
}
pub fn camera0(&mut self) {
self.canvas.camera0();
}
pub fn pal(&mut self, src_color: Color, dst_color: Color) {
self.palette[src_color as usize] = dst_color;
}
pub fn pal0(&mut self) {
for i in 0..self.palette.len() {
self.palette[i] = i as Color;
}
}
pub fn dither(&mut self, alpha: f32) {
self.canvas.dither(alpha);
}
pub fn cls(&mut self, color: Color) {
self.canvas.cls(self.palette[color as usize]);
}
pub fn pget(&mut self, x: f32, y: f32) -> Color {
self.canvas.pget(x, y)
}
pub fn pset(&mut self, x: f32, y: f32, color: Color) {
self.canvas.pset(x, y, self.palette[color as usize]);
}
pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color) {
self.canvas
.line(x1, y1, x2, y2, self.palette[color as usize]);
}
pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
self.canvas
.rect(x, y, width, height, self.palette[color as usize]);
}
pub fn rectb(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
self.canvas
.rectb(x, y, width, height, self.palette[color as usize]);
}
pub fn circ(&mut self, x: f32, y: f32, radius: f32, color: Color) {
self.canvas.circ(x, y, radius, self.palette[color as usize]);
}
pub fn circb(&mut self, x: f32, y: f32, radius: f32, color: Color) {
self.canvas
.circb(x, y, radius, self.palette[color as usize]);
}
pub fn elli(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
self.canvas
.elli(x, y, width, height, self.palette[color as usize]);
}
pub fn ellib(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
self.canvas
.ellib(x, y, width, height, self.palette[color as usize]);
}
pub fn tri(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color) {
self.canvas
.tri(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
}
pub fn trib(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color) {
self.canvas
.trib(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
}
pub fn fill(&mut self, x: f32, y: f32, color: Color) {
self.canvas.fill(x, y, self.palette[color as usize]);
}
pub fn blt(
&mut self,
x: f32,
y: f32,
image: SharedImage,
image_x: f32,
image_y: f32,
width: f32,
height: f32,
transparent: Option<Color>,
rotate: Option<f32>,
scale: Option<f32>,
) {
let rotate = rotate.unwrap_or(0.0);
let scale = scale.unwrap_or(1.0);
if rotate != 0.0 || scale != 1.0 {
self.blt_transform(
x,
y,
image,
image_x,
image_y,
width,
height,
transparent,
rotate,
scale,
);
return;
}
if let Some(image) = image.try_lock() {
self.canvas.blt(
x,
y,
&image.canvas,
image_x,
image_y,
width,
height,
transparent,
Some(&self.palette),
);
} else {
let copy_width = utils::f32_to_u32(width.abs());
let copy_height = utils::f32_to_u32(height.abs());
let mut canvas = Canvas::new(copy_width, copy_height);
canvas.blt(
0.0,
0.0,
&self.canvas,
image_x,
image_y,
copy_width as f32,
copy_height as f32,
None,
None,
);
self.canvas.blt(
x,
y,
&canvas,
0.0,
0.0,
width,
height,
transparent,
Some(&self.palette),
);
}
}
fn blt_transform(
&mut self,
x: f32,
y: f32,
image: SharedImage,
image_x: f32,
image_y: f32,
width: f32,
height: f32,
transparent: Option<Color>,
rotate: f32,
scale: f32,
) {
if let Some(image) = image.try_lock() {
self.canvas.blt_transform(
x,
y,
&image.canvas,
image_x,
image_y,
width,
height,
transparent,
Some(&self.palette),
rotate,
scale,
false,
);
} else {
let copy_width = utils::f32_to_u32(width.abs());
let copy_height = utils::f32_to_u32(height.abs());
let mut canvas = Canvas::new(copy_width, copy_height);
canvas.blt(
0.0,
0.0,
&self.canvas,
image_x,
image_y,
copy_width as f32,
copy_height as f32,
None,
None,
);
self.canvas.blt_transform(
x,
y,
&canvas,
0.0,
0.0,
width,
height,
transparent,
Some(&self.palette),
rotate,
scale,
false,
);
}
}
pub fn bltm(
&mut self,
x: f32,
y: f32,
tilemap: SharedTilemap,
tilemap_x: f32,
tilemap_y: f32,
width: f32,
height: f32,
transparent: Option<Color>,
rotate: Option<f32>,
scale: Option<f32>,
) {
let rotate = rotate.unwrap_or(0.0);
let scale = scale.unwrap_or(1.0);
if rotate != 0.0 || scale != 1.0 {
self.bltm_transform(
x,
y,
tilemap,
tilemap_x,
tilemap_y,
width,
height,
transparent,
rotate,
scale,
);
return;
}
let x = utils::f32_to_i32(x) - self.canvas.camera_x;
let y = utils::f32_to_i32(y) - self.canvas.camera_y;
let tilemap_x = utils::f32_to_i32(tilemap_x);
let tilemap_y = utils::f32_to_i32(tilemap_y);
let width = utils::f32_to_i32(width);
let height = utils::f32_to_i32(height);
let tilemap = tilemap.lock();
let tilemap_rect = RectArea::new(
tilemap.canvas.self_rect.left() * TILE_SIZE as i32,
tilemap.canvas.self_rect.top() * TILE_SIZE as i32,
tilemap.canvas.self_rect.width() * TILE_SIZE,
tilemap.canvas.self_rect.height() * TILE_SIZE,
);
let CopyArea {
dst_x,
dst_y,
src_x,
src_y,
sign_x,
sign_y,
offset_x,
offset_y,
width,
height,
} = CopyArea::new(
x,
y,
self.canvas.clip_rect,
tilemap_x,
tilemap_y,
tilemap_rect,
width,
height,
);
if width == 0 || height == 0 {
return;
}
let images = IMAGES.lock();
let image = match &tilemap.imgsrc {
ImageSource::Index(index) => images[*index as usize].lock(),
ImageSource::Image(image) => image.lock(),
};
for yi in 0..height {
for xi in 0..width {
let tilemap_x = src_x + sign_x * xi + offset_x;
let tilemap_y = src_y + sign_y * yi + offset_y;
let tile_x = tilemap_x / TILE_SIZE as i32;
let tile_y = tilemap_y / TILE_SIZE as i32;
let tile = tilemap.canvas.read_data(tile_x as usize, tile_y as usize);
let value_x = tile.0 as i32 * TILE_SIZE as i32 + tilemap_x % TILE_SIZE as i32;
if value_x < 0 || value_x >= image.width() as i32 {
continue;
}
let value_y = tile.1 as i32 * TILE_SIZE as i32 + tilemap_y % TILE_SIZE as i32;
if value_y < 0 || value_y >= image.height() as i32 {
continue;
}
let value = image.canvas.read_data(value_x as usize, value_y as usize);
if let Some(transparent) = transparent {
if value == transparent {
continue;
}
}
let value = self.palette[value.to_index()];
self.canvas
.write_data((dst_x + xi) as usize, (dst_y + yi) as usize, value);
}
}
}
fn bltm_transform(
&mut self,
x: f32,
y: f32,
tilemap: SharedTilemap,
tilemap_x: f32,
tilemap_y: f32,
width: f32,
height: f32,
transparent: Option<Color>,
rotate: f32,
scale: f32,
) {
let copy_width = utils::f32_to_u32(width.abs());
let copy_height = utils::f32_to_u32(height.abs());
let tilemap_width = tilemap.lock().width() as f32;
let tilemap_height = tilemap.lock().height() as f32;
let image = Self::new(copy_width, copy_height);
{
let mut image = image.lock();
image.bltm(
0.0,
0.0,
tilemap,
tilemap_x,
tilemap_y,
width.abs(),
height.abs(),
None,
None,
None,
);
image.clip(
-tilemap_x,
-tilemap_y,
tilemap_width * TILE_SIZE as f32,
tilemap_height * TILE_SIZE as f32,
);
self.canvas.blt_transform(
x,
y,
&image.canvas,
0.0,
0.0,
width,
height,
transparent,
Some(&self.palette),
rotate,
scale,
true,
);
}
}
pub fn text(&mut self, x: f32, y: f32, string: &str, color: Color, font: Option<SharedFont>) {
if let Some(font) = font {
let x = utils::f32_to_i32(x) - self.canvas.camera_x;
let y = utils::f32_to_i32(y) - self.canvas.camera_y;
let color = self.palette[color as usize];
font.lock().draw(&mut self.canvas, x, y, string, color);
return;
}
let mut x = utils::f32_to_i32(x); let mut y = utils::f32_to_i32(y); let color = self.palette[color as usize];
let palette1 = self.palette[1];
self.pal(1, color);
let start_x = x;
for c in string.chars() {
if c == '\n' {
x = start_x;
y += FONT_HEIGHT as i32;
continue;
}
if c < MIN_FONT_CODE || c > MAX_FONT_CODE {
continue;
}
let code = c as i32 - MIN_FONT_CODE as i32;
let src_x = (code % NUM_FONT_ROWS as i32) * FONT_WIDTH as i32;
let src_y = (code / NUM_FONT_ROWS as i32) * FONT_HEIGHT as i32;
self.blt(
x as f32,
y as f32,
FONT_IMAGE.clone(),
src_x as f32,
src_y as f32,
FONT_WIDTH as f32,
FONT_HEIGHT as f32,
Some(0),
Some(0.0),
Some(1.0),
);
x += FONT_WIDTH as i32;
}
self.pal(1, palette1);
}
fn color_dist(rgb1: (u8, u8, u8), rgb2: (u8, u8, u8)) -> f32 {
let (r1, g1, b1) = rgb1;
let (r2, g2, b2) = rgb2;
let dx = (r1 as f32 - r2 as f32) * 0.30;
let dy = (g1 as f32 - g2 as f32) * 0.59;
let dz = (b1 as f32 - b2 as f32) * 0.11;
dx * dx + dy * dy + dz * dz
}
}