1use std::array;
2use std::collections::HashMap;
3use std::path::Path;
4
5use image::imageops;
6
7use crate::canvas::{Canvas, CopyArea, ToIndex};
8use crate::font::SharedFont;
9use crate::pyxel::{COLORS, FONT_IMAGE, IMAGES};
10use crate::rect_area::RectArea;
11use crate::settings::{
12 FONT_HEIGHT, FONT_WIDTH, MAX_COLORS, MAX_FONT_CODE, MIN_FONT_CODE, NUM_FONT_ROWS, TILE_SIZE,
13};
14use crate::tilemap::{ImageSource, SharedTilemap};
15use crate::utils;
16
17pub type Rgb24 = u32;
18pub type Color = u8;
19
20impl ToIndex for Color {
21 fn to_index(&self) -> usize {
22 *self as usize
23 }
24}
25
26pub struct Image {
27 pub(crate) canvas: Canvas<Color>,
28 pub(crate) palette: [Color; MAX_COLORS as usize],
29}
30
31pub type SharedImage = shared_type!(Image);
32
33impl Image {
34 pub fn new(width: u32, height: u32) -> SharedImage {
35 new_shared_type!(Self {
36 canvas: Canvas::new(width, height),
37 palette: array::from_fn(|i| i as Color),
38 })
39 }
40
41 pub fn from_image(filename: &str, include_colors: Option<bool>) -> SharedImage {
42 let include_colors = include_colors.unwrap_or(false);
43 let mut colors = COLORS.lock();
44 if include_colors {
45 colors.clear();
46 }
47
48 let file = image::open(Path::new(&filename));
49 if file.is_err() {
50 println!("Failed to open file '{filename}'");
51 return Self::new(1, 1);
52 }
53
54 let file_image = file.unwrap().to_rgb8();
55 let (width, height) = file_image.dimensions();
56 let image = Self::new(width, height);
57
58 {
59 let mut image = image.lock();
60 let mut color_table = HashMap::<(u8, u8, u8), Color>::new();
61
62 for y in 0..height {
63 for x in 0..width {
64 let p = file_image.get_pixel(x, y);
65 let src_rgb = (p[0], p[1], p[2]);
66
67 if let Some(color) = color_table.get(&src_rgb) {
68 image.canvas.write_data(x as usize, y as usize, *color);
69 } else {
70 let mut closest_color: Color = 0;
71
72 if include_colors {
73 colors.push(
74 ((src_rgb.0 as u32) << 16)
75 | ((src_rgb.1 as u32) << 8)
76 | src_rgb.2 as u32,
77 );
78 closest_color = colors.len() as Color - 1;
79 } else {
80 let mut closest_dist: f64 = f64::MAX;
81 for (i, pal_color) in colors.iter().enumerate() {
82 let pal_rgb = (
83 (pal_color >> 16) as u8,
84 (pal_color >> 8) as u8,
85 *pal_color as u8,
86 );
87 let dist = Self::color_dist(src_rgb, pal_rgb);
88 if dist < closest_dist {
89 closest_color = i as Color;
90 closest_dist = dist;
91 }
92 }
93 }
94
95 color_table.insert(src_rgb, closest_color);
96 image
97 .canvas
98 .write_data(x as usize, y as usize, closest_color);
99 }
100 }
101 }
102 }
103
104 image
105 }
106
107 pub const fn width(&self) -> u32 {
108 self.canvas.width()
109 }
110
111 pub const fn height(&self) -> u32 {
112 self.canvas.height()
113 }
114
115 pub fn data_ptr(&mut self) -> *mut Color {
116 self.canvas.data_ptr()
117 }
118
119 pub fn set(&mut self, x: i32, y: i32, data_str: &[&str]) {
120 let width = utils::simplify_string(data_str[0]).len() as u32;
121 let height = data_str.len() as u32;
122 let image = Self::new(width, height);
123
124 {
125 let mut image = image.lock();
126 for y in 0..height {
127 let src_data = utils::simplify_string(data_str[y as usize]);
128 for x in 0..width {
129 let color =
130 utils::parse_hex_string(&src_data[x as usize..=x as usize]).unwrap();
131 image
132 .canvas
133 .write_data(x as usize, y as usize, color as Color);
134 }
135 }
136 }
137
138 self.blt(
139 x as f64,
140 y as f64,
141 image,
142 0.0,
143 0.0,
144 width as f64,
145 height as f64,
146 None,
147 None,
148 None,
149 );
150 }
151
152 pub fn load(&mut self, x: i32, y: i32, filename: &str, include_colors: Option<bool>) {
153 let image = Self::from_image(filename, include_colors);
154 let width = image.lock().width();
155 let height = image.lock().height();
156
157 self.blt(
158 x as f64,
159 y as f64,
160 image,
161 0.0,
162 0.0,
163 width as f64,
164 height as f64,
165 None,
166 None,
167 None,
168 );
169 }
170
171 pub fn save(&self, filename: &str, scale: u32) {
172 let colors = COLORS.lock();
173 let width = self.width();
174 let height = self.height();
175 let mut image = image::RgbImage::new(width, height);
176
177 for y in 0..height {
178 for x in 0..width {
179 let rgb = colors[self.canvas.read_data(x as usize, y as usize) as usize];
180 let r = (rgb >> 16) as u8;
181 let g = (rgb >> 8) as u8;
182 let b = rgb as u8;
183 image.put_pixel(x, y, image::Rgb([r, g, b]));
184 }
185 }
186
187 let image = imageops::resize(
188 &image,
189 width * scale,
190 height * scale,
191 imageops::FilterType::Nearest,
192 );
193 let filename = utils::add_file_extension(filename, ".png");
194 image
195 .save(&filename)
196 .unwrap_or_else(|_| panic!("Failed to open file '{filename}'"));
197 }
198
199 pub fn clip(&mut self, x: f64, y: f64, width: f64, height: f64) {
200 self.canvas.clip(x, y, width, height);
201 }
202
203 pub fn clip0(&mut self) {
204 self.canvas.clip0();
205 }
206
207 pub fn camera(&mut self, x: f64, y: f64) {
208 self.canvas.camera(x, y);
209 }
210
211 pub fn camera0(&mut self) {
212 self.canvas.camera0();
213 }
214
215 pub fn pal(&mut self, src_color: Color, dst_color: Color) {
216 self.palette[src_color as usize] = dst_color;
217 }
218
219 pub fn pal0(&mut self) {
220 for i in 0..self.palette.len() {
221 self.palette[i] = i as Color;
222 }
223 }
224
225 pub fn dither(&mut self, alpha: f32) {
226 self.canvas.dither(alpha);
227 }
228
229 pub fn cls(&mut self, color: Color) {
230 self.canvas.cls(self.palette[color as usize]);
231 }
232
233 pub fn pget(&mut self, x: f64, y: f64) -> Color {
234 self.canvas.pget(x, y)
235 }
236
237 pub fn pset(&mut self, x: f64, y: f64, color: Color) {
238 self.canvas.pset(x, y, self.palette[color as usize]);
239 }
240
241 pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, color: Color) {
242 self.canvas
243 .line(x1, y1, x2, y2, self.palette[color as usize]);
244 }
245
246 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
247 self.canvas
248 .rect(x, y, width, height, self.palette[color as usize]);
249 }
250
251 pub fn rectb(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
252 self.canvas
253 .rectb(x, y, width, height, self.palette[color as usize]);
254 }
255
256 pub fn circ(&mut self, x: f64, y: f64, radius: f64, color: Color) {
257 self.canvas.circ(x, y, radius, self.palette[color as usize]);
258 }
259
260 pub fn circb(&mut self, x: f64, y: f64, radius: f64, color: Color) {
261 self.canvas
262 .circb(x, y, radius, self.palette[color as usize]);
263 }
264
265 pub fn elli(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
266 self.canvas
267 .elli(x, y, width, height, self.palette[color as usize]);
268 }
269
270 pub fn ellib(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
271 self.canvas
272 .ellib(x, y, width, height, self.palette[color as usize]);
273 }
274
275 pub fn tri(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, color: Color) {
276 self.canvas
277 .tri(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
278 }
279
280 pub fn trib(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, color: Color) {
281 self.canvas
282 .trib(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
283 }
284
285 pub fn fill(&mut self, x: f64, y: f64, color: Color) {
286 self.canvas.fill(x, y, self.palette[color as usize]);
287 }
288
289 pub fn blt(
290 &mut self,
291 x: f64,
292 y: f64,
293 image: SharedImage,
294 image_x: f64,
295 image_y: f64,
296 width: f64,
297 height: f64,
298 transparent: Option<Color>,
299 rotate: Option<f64>,
300 scale: Option<f64>,
301 ) {
302 let rotate = rotate.unwrap_or(0.0);
303 let scale = scale.unwrap_or(1.0);
304 if rotate != 0.0 || scale != 1.0 {
305 self.blt_transform(
306 x,
307 y,
308 image,
309 image_x,
310 image_y,
311 width,
312 height,
313 transparent,
314 rotate,
315 scale,
316 );
317 return;
318 }
319
320 if let Some(image) = image.try_lock() {
321 self.canvas.blt(
322 x,
323 y,
324 &image.canvas,
325 image_x,
326 image_y,
327 width,
328 height,
329 transparent,
330 Some(&self.palette),
331 );
332 } else {
333 let copy_width = utils::f64_to_u32(width.abs());
334 let copy_height = utils::f64_to_u32(height.abs());
335 let mut canvas = Canvas::new(copy_width, copy_height);
336
337 canvas.blt(
338 0.0,
339 0.0,
340 &self.canvas,
341 image_x,
342 image_y,
343 copy_width as f64,
344 copy_height as f64,
345 None,
346 None,
347 );
348
349 self.canvas.blt(
350 x,
351 y,
352 &canvas,
353 0.0,
354 0.0,
355 width,
356 height,
357 transparent,
358 Some(&self.palette),
359 );
360 }
361 }
362
363 fn blt_transform(
364 &mut self,
365 x: f64,
366 y: f64,
367 image: SharedImage,
368 image_x: f64,
369 image_y: f64,
370 width: f64,
371 height: f64,
372 transparent: Option<Color>,
373 rotate: f64,
374 scale: f64,
375 ) {
376 if let Some(image) = image.try_lock() {
377 self.canvas.blt_transform(
378 x,
379 y,
380 &image.canvas,
381 image_x,
382 image_y,
383 width,
384 height,
385 transparent,
386 Some(&self.palette),
387 rotate,
388 scale,
389 false,
390 );
391 } else {
392 let copy_width = utils::f64_to_u32(width.abs());
393 let copy_height = utils::f64_to_u32(height.abs());
394 let mut canvas = Canvas::new(copy_width, copy_height);
395
396 canvas.blt(
397 0.0,
398 0.0,
399 &self.canvas,
400 image_x,
401 image_y,
402 copy_width as f64,
403 copy_height as f64,
404 None,
405 None,
406 );
407
408 self.canvas.blt_transform(
409 x,
410 y,
411 &canvas,
412 0.0,
413 0.0,
414 width,
415 height,
416 transparent,
417 Some(&self.palette),
418 rotate,
419 scale,
420 false,
421 );
422 }
423 }
424
425 pub fn bltm(
426 &mut self,
427 x: f64,
428 y: f64,
429 tilemap: SharedTilemap,
430 tilemap_x: f64,
431 tilemap_y: f64,
432 width: f64,
433 height: f64,
434 transparent: Option<Color>,
435 rotate: Option<f64>,
436 scale: Option<f64>,
437 ) {
438 let rotate = rotate.unwrap_or(0.0);
439 let scale = scale.unwrap_or(1.0);
440 if rotate != 0.0 || scale != 1.0 {
441 self.bltm_transform(
442 x,
443 y,
444 tilemap,
445 tilemap_x,
446 tilemap_y,
447 width,
448 height,
449 transparent,
450 rotate,
451 scale,
452 );
453 return;
454 }
455
456 let x = utils::f64_to_i32(x) - self.canvas.camera_x;
457 let y = utils::f64_to_i32(y) - self.canvas.camera_y;
458 let tilemap_x = utils::f64_to_i32(tilemap_x);
459 let tilemap_y = utils::f64_to_i32(tilemap_y);
460 let width = utils::f64_to_i32(width);
461 let height = utils::f64_to_i32(height);
462
463 let tilemap = tilemap.lock();
464 let tilemap_rect = RectArea::new(
465 tilemap.canvas.self_rect.left() * TILE_SIZE as i32,
466 tilemap.canvas.self_rect.top() * TILE_SIZE as i32,
467 tilemap.canvas.self_rect.width() * TILE_SIZE,
468 tilemap.canvas.self_rect.height() * TILE_SIZE,
469 );
470
471 let CopyArea {
472 dst_x,
473 dst_y,
474 src_x,
475 src_y,
476 sign_x,
477 sign_y,
478 offset_x,
479 offset_y,
480 width,
481 height,
482 } = CopyArea::new(
483 x,
484 y,
485 self.canvas.clip_rect,
486 tilemap_x,
487 tilemap_y,
488 tilemap_rect,
489 width,
490 height,
491 );
492 if width == 0 || height == 0 {
493 return;
494 }
495
496 let images = IMAGES.lock();
497 let image = match &tilemap.imgsrc {
498 ImageSource::Index(index) => images[*index as usize].lock(),
499 ImageSource::Image(image) => image.lock(),
500 };
501
502 for yi in 0..height {
503 for xi in 0..width {
504 let tilemap_x = src_x + sign_x * xi + offset_x;
505 let tilemap_y = src_y + sign_y * yi + offset_y;
506
507 let tile_x = tilemap_x / TILE_SIZE as i32;
508 let tile_y = tilemap_y / TILE_SIZE as i32;
509 let tile = tilemap.canvas.read_data(tile_x as usize, tile_y as usize);
510
511 let value_x = tile.0 as i32 * TILE_SIZE as i32 + tilemap_x % TILE_SIZE as i32;
512 if value_x < 0 || value_x >= image.width() as i32 {
513 continue;
514 }
515 let value_y = tile.1 as i32 * TILE_SIZE as i32 + tilemap_y % TILE_SIZE as i32;
516 if value_y < 0 || value_y >= image.height() as i32 {
517 continue;
518 }
519 let value = image.canvas.read_data(value_x as usize, value_y as usize);
520
521 if let Some(transparent) = transparent {
522 if value == transparent {
523 continue;
524 }
525 }
526 let value = self.palette[value.to_index()];
527 self.canvas
528 .write_data((dst_x + xi) as usize, (dst_y + yi) as usize, value);
529 }
530 }
531 }
532
533 fn bltm_transform(
534 &mut self,
535 x: f64,
536 y: f64,
537 tilemap: SharedTilemap,
538 tilemap_x: f64,
539 tilemap_y: f64,
540 width: f64,
541 height: f64,
542 transparent: Option<Color>,
543 rotate: f64,
544 scale: f64,
545 ) {
546 let copy_width = utils::f64_to_u32(width.abs());
547 let copy_height = utils::f64_to_u32(height.abs());
548 let tilemap_width = tilemap.lock().width() as f64;
549 let tilemap_height = tilemap.lock().height() as f64;
550 let image = Self::new(copy_width, copy_height);
551
552 {
553 let mut image = image.lock();
554 image.bltm(
555 0.0,
556 0.0,
557 tilemap,
558 tilemap_x,
559 tilemap_y,
560 width.abs(),
561 height.abs(),
562 None,
563 None,
564 None,
565 );
566 image.clip(
567 -tilemap_x,
568 -tilemap_y,
569 tilemap_width * TILE_SIZE as f64,
570 tilemap_height * TILE_SIZE as f64,
571 );
572 self.canvas.blt_transform(
573 x,
574 y,
575 &image.canvas,
576 0.0,
577 0.0,
578 width,
579 height,
580 transparent,
581 Some(&self.palette),
582 rotate,
583 scale,
584 true,
585 );
586 }
587 }
588
589 pub fn text(&mut self, x: f64, y: f64, string: &str, color: Color, font: Option<SharedFont>) {
590 if let Some(font) = font {
591 let x = utils::f64_to_i32(x) - self.canvas.camera_x;
592 let y = utils::f64_to_i32(y) - self.canvas.camera_y;
593 let color = self.palette[color as usize];
594 font.lock().draw(&mut self.canvas, x, y, string, color);
595 return;
596 }
597
598 let mut x = utils::f64_to_i32(x); let mut y = utils::f64_to_i32(y); let color = self.palette[color as usize];
601 let palette1 = self.palette[1];
602 self.pal(1, color);
603
604 let start_x = x;
605 for c in string.chars() {
606 if c == '\n' {
607 x = start_x;
608 y += FONT_HEIGHT as i32;
609 continue;
610 }
611 if c < MIN_FONT_CODE || c > MAX_FONT_CODE {
612 continue;
613 }
614
615 let code = c as i32 - MIN_FONT_CODE as i32;
616 let src_x = (code % NUM_FONT_ROWS as i32) * FONT_WIDTH as i32;
617 let src_y = (code / NUM_FONT_ROWS as i32) * FONT_HEIGHT as i32;
618
619 self.blt(
620 x as f64,
621 y as f64,
622 FONT_IMAGE.clone(),
623 src_x as f64,
624 src_y as f64,
625 FONT_WIDTH as f64,
626 FONT_HEIGHT as f64,
627 Some(0),
628 Some(0.0),
629 Some(1.0),
630 );
631 x += FONT_WIDTH as i32;
632 }
633 self.pal(1, palette1);
634 }
635
636 fn color_dist(rgb1: (u8, u8, u8), rgb2: (u8, u8, u8)) -> f64 {
637 let (r1, g1, b1) = rgb1;
638 let (r2, g2, b2) = rgb2;
639 let dx = (r1 as f64 - r2 as f64) * 0.30;
640 let dy = (g1 as f64 - g2 as f64) * 0.59;
641 let dz = (b1 as f64 - b2 as f64) * 0.11;
642
643 dx * dx + dy * dy + dz * dz
644 }
645}