1use crate::{
9 assets::{MapData, SpriteSheet, SPRITES_PER_ROW, SPRITE_COUNT, SPRITE_SIZE},
10 font, palette,
11};
12
13pub const WIDTH: i32 = 128;
15pub const HEIGHT: i32 = 128;
17
18const IDENTITY_PALETTE: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
20const DEFAULT_TRANSPARENT: u16 = 0x0001;
22
23pub struct Framebuffer {
25 pixels: Vec<u8>,
26 camera_x: i32,
27 camera_y: i32,
28 clip: (i32, i32, i32, i32),
29 draw_pal: [u8; 16],
30 display_pal: [u8; 16],
31 transparent: u16,
32 fill_pattern: u16,
33 fill_secondary: u8,
34 fill_transparent: bool,
35 pen_color: u8,
36 cursor_x: i32,
37 cursor_y: i32,
38}
39
40impl Default for Framebuffer {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl Framebuffer {
47 pub fn new() -> Self {
48 Self {
49 pixels: vec![0; (WIDTH * HEIGHT) as usize],
50 camera_x: 0,
51 camera_y: 0,
52 clip: (0, 0, WIDTH, HEIGHT),
53 draw_pal: IDENTITY_PALETTE,
54 display_pal: IDENTITY_PALETTE,
55 transparent: DEFAULT_TRANSPARENT,
56 fill_pattern: 0,
57 fill_secondary: 0,
58 fill_transparent: false,
59 pen_color: 6,
60 cursor_x: 0,
61 cursor_y: 0,
62 }
63 }
64
65 pub fn pixels(&self) -> &[u8] {
67 &self.pixels
68 }
69
70 pub fn display_palette(&self) -> &[u8; 16] {
74 &self.display_pal
75 }
76
77 pub fn write_rgba(&self, out: &mut [u8]) {
79 let mut lut = [[0u8; 4]; 16];
84 for (i, entry) in lut.iter_mut().enumerate() {
85 *entry = palette::rgba(self.display_pal[i]);
86 }
87 for (chunk, &c) in out.chunks_exact_mut(4).zip(self.pixels.iter()) {
88 chunk.copy_from_slice(&lut[(c & 0x0f) as usize]);
89 }
90 }
91
92 pub fn camera(&mut self, x: i32, y: i32) {
94 self.camera_x = x;
95 self.camera_y = y;
96 }
97
98 pub fn clip(&mut self, x: i32, y: i32, w: i32, h: i32) {
100 let x0 = x.clamp(0, WIDTH);
101 let y0 = y.clamp(0, HEIGHT);
102 let x1 = (x + w).clamp(0, WIDTH);
103 let y1 = (y + h).clamp(0, HEIGHT);
104 self.clip = (x0, y0, x1, y1);
105 }
106
107 pub fn clip_reset(&mut self) {
109 self.clip = (0, 0, WIDTH, HEIGHT);
110 }
111
112 pub fn reset_state(&mut self) {
114 self.camera_x = 0;
115 self.camera_y = 0;
116 self.clip_reset();
117 self.draw_pal = IDENTITY_PALETTE;
118 self.display_pal = IDENTITY_PALETTE;
119 self.transparent = DEFAULT_TRANSPARENT;
120 self.fill_pattern = 0;
121 self.fill_secondary = 0;
122 self.fill_transparent = false;
123 self.pen_color = 6;
124 self.cursor_x = 0;
125 self.cursor_y = 0;
126 }
127
128 pub fn set_transparent_color(&mut self, color: u8, transparent: bool) {
130 let bit = 1u16 << (color & 0x0f);
131 if transparent {
132 self.transparent |= bit;
133 } else {
134 self.transparent &= !bit;
135 }
136 }
137
138 pub fn reset_transparency(&mut self) {
140 self.transparent = DEFAULT_TRANSPARENT;
141 }
142
143 pub fn remap_color(&mut self, from: u8, to: u8) {
145 self.draw_pal[(from & 0x0f) as usize] = to & 0x0f;
146 }
147
148 pub fn remap_display_color(&mut self, from: u8, to: u8) {
150 self.display_pal[(from & 0x0f) as usize] = to & 0x0f;
151 }
152
153 pub fn reset_palette(&mut self) {
155 self.draw_pal = IDENTITY_PALETTE;
156 self.display_pal = IDENTITY_PALETTE;
157 }
158
159 pub fn set_fill_pattern(&mut self, pattern: u16, secondary: u8, transparent: bool) {
164 self.fill_pattern = pattern;
165 self.fill_secondary = secondary & 0x0f;
166 self.fill_transparent = transparent;
167 }
168
169 fn fill_color_at(&self, x: i32, y: i32, primary: u8) -> Option<u8> {
172 if self.fill_pattern == 0 {
173 return Some(primary);
174 }
175 let idx = ((y & 3) * 4 + (x & 3)) as u16;
176 if (self.fill_pattern >> (15 - idx)) & 1 == 0 {
177 Some(primary)
178 } else if self.fill_transparent {
179 None
180 } else {
181 Some(self.fill_secondary)
182 }
183 }
184
185 fn raw_pset_fill(&mut self, x: i32, y: i32, primary: u8) {
187 if let Some(c) = self.fill_color_at(x, y, primary) {
188 self.raw_pset(x, y, c);
189 }
190 }
191
192 fn fill_span(&mut self, x0: i32, x1: i32, y: i32, color: u8) {
197 let (cx0, cy0, cx1, cy1) = self.clip;
198 if y < cy0 || y >= cy1 {
199 return;
200 }
201 let xa = x0.max(cx0);
202 let xb = x1.min(cx1 - 1);
203 if xa > xb {
204 return;
205 }
206 let c = self.draw_pal[(color & 0x0f) as usize] & 0x0f;
207 let start = (y * WIDTH + xa) as usize;
208 let end = (y * WIDTH + xb + 1) as usize;
209 self.pixels[start..end].fill(c);
210 }
211
212 pub fn cls(&mut self, color: u8) {
214 self.pixels.fill(color & 0x0f);
215 }
216
217 #[inline]
218 fn raw_pset(&mut self, x: i32, y: i32, color: u8) {
219 let (cx0, cy0, cx1, cy1) = self.clip;
220 if x >= cx0 && x < cx1 && y >= cy0 && y < cy1 {
221 let c = self.draw_pal[(color & 0x0f) as usize] & 0x0f;
222 self.pixels[(y * WIDTH + x) as usize] = c;
223 }
224 }
225
226 pub fn pset(&mut self, x: i32, y: i32, color: u8) {
228 self.raw_pset(x - self.camera_x, y - self.camera_y, color);
229 }
230
231 pub fn pget(&self, x: i32, y: i32) -> u8 {
233 if (0..WIDTH).contains(&x) && (0..HEIGHT).contains(&y) {
234 self.pixels[(y * WIDTH + x) as usize]
235 } else {
236 0
237 }
238 }
239
240 pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
242 let (mut x0, mut y0) = (x0 - self.camera_x, y0 - self.camera_y);
243 let (x1, y1) = (x1 - self.camera_x, y1 - self.camera_y);
244 let dx = (x1 - x0).abs();
245 let dy = -(y1 - y0).abs();
246 let sx = if x0 < x1 { 1 } else { -1 };
247 let sy = if y0 < y1 { 1 } else { -1 };
248 let mut err = dx + dy;
249 loop {
250 self.raw_pset(x0, y0, color);
251 if x0 == x1 && y0 == y1 {
252 break;
253 }
254 let e2 = 2 * err;
255 if e2 >= dy {
256 err += dy;
257 x0 += sx;
258 }
259 if e2 <= dx {
260 err += dx;
261 y0 += sy;
262 }
263 }
264 }
265
266 pub fn rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
268 let (xa, xb) = (x0.min(x1), x0.max(x1));
269 let (ya, yb) = (y0.min(y1), y0.max(y1));
270 self.line(xa, ya, xb, ya, color);
271 self.line(xa, yb, xb, yb, color);
272 self.line(xa, ya, xa, yb, color);
273 self.line(xb, ya, xb, yb, color);
274 }
275
276 pub fn rectfill(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
278 let (xa, xb) = (x0.min(x1), x0.max(x1));
279 let (ya, yb) = (y0.min(y1), y0.max(y1));
280 if self.fill_pattern == 0 {
281 for y in ya..=yb {
283 self.fill_span(
284 xa - self.camera_x,
285 xb - self.camera_x,
286 y - self.camera_y,
287 color,
288 );
289 }
290 } else {
291 for y in ya..=yb {
292 for x in xa..=xb {
293 self.raw_pset_fill(x - self.camera_x, y - self.camera_y, color);
294 }
295 }
296 }
297 }
298
299 pub fn circ(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
301 self.circle_impl(cx, cy, r.max(0), color, false);
302 }
303
304 pub fn circfill(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
306 self.circle_impl(cx, cy, r.max(0), color, true);
307 }
308
309 fn circle_impl(&mut self, cx: i32, cy: i32, r: i32, color: u8, fill: bool) {
310 let (cx, cy) = (cx - self.camera_x, cy - self.camera_y);
311 let mut x = r;
312 let mut y = 0;
313 let mut err = 1 - r;
314 while x >= y {
315 if fill && self.fill_pattern == 0 {
316 self.fill_span(cx - x, cx + x, cy + y, color);
319 self.fill_span(cx - x, cx + x, cy - y, color);
320 self.fill_span(cx - y, cx + y, cy + x, color);
321 self.fill_span(cx - y, cx + y, cy - x, color);
322 } else if fill {
323 for px in (cx - x)..=(cx + x) {
324 self.raw_pset_fill(px, cy + y, color);
325 self.raw_pset_fill(px, cy - y, color);
326 }
327 for px in (cx - y)..=(cx + y) {
328 self.raw_pset_fill(px, cy + x, color);
329 self.raw_pset_fill(px, cy - x, color);
330 }
331 } else {
332 for (px, py) in [
333 (cx + x, cy + y),
334 (cx - x, cy + y),
335 (cx + x, cy - y),
336 (cx - x, cy - y),
337 (cx + y, cy + x),
338 (cx - y, cy + x),
339 (cx + y, cy - x),
340 (cx - y, cy - x),
341 ] {
342 self.raw_pset(px, py, color);
343 }
344 }
345 y += 1;
346 if err < 0 {
347 err += 2 * y + 1;
348 } else {
349 x -= 1;
350 err += 2 * (y - x) + 1;
351 }
352 }
353 }
354
355 pub fn oval(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
357 self.oval_impl(x0, y0, x1, y1, color, false);
358 }
359
360 pub fn ovalfill(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
362 self.oval_impl(x0, y0, x1, y1, color, true);
363 }
364
365 fn oval_impl(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8, fill: bool) {
366 let (xa, xb) = (x0.min(x1), x0.max(x1));
367 let (ya, yb) = (y0.min(y1), y0.max(y1));
368 let cx = (xa + xb) as f32 / 2.0;
369 let cy = (ya + yb) as f32 / 2.0;
370 let a = (xb - xa) as f32 / 2.0;
371 let b = (yb - ya) as f32 / 2.0;
372 if fill {
373 for y in ya..=yb {
374 let dy = if b > 0.0 { (y as f32 - cy) / b } else { 0.0 };
375 let s = 1.0 - dy * dy;
376 if s < 0.0 {
377 continue;
378 }
379 let dx = a * s.sqrt();
380 let left = (cx - dx).round() as i32;
381 let right = (cx + dx).round() as i32;
382 if self.fill_pattern == 0 {
383 self.fill_span(
385 left - self.camera_x,
386 right - self.camera_x,
387 y - self.camera_y,
388 color,
389 );
390 } else {
391 for x in left..=right {
392 self.raw_pset_fill(x - self.camera_x, y - self.camera_y, color);
393 }
394 }
395 }
396 } else {
397 for y in ya..=yb {
399 let dy = if b > 0.0 { (y as f32 - cy) / b } else { 0.0 };
400 let s = 1.0 - dy * dy;
401 if s < 0.0 {
402 continue;
403 }
404 let dx = a * s.sqrt();
405 self.pset((cx - dx).round() as i32, y, color);
406 self.pset((cx + dx).round() as i32, y, color);
407 }
408 for x in xa..=xb {
409 let dx = if a > 0.0 { (x as f32 - cx) / a } else { 0.0 };
410 let s = 1.0 - dx * dx;
411 if s < 0.0 {
412 continue;
413 }
414 let dy = b * s.sqrt();
415 self.pset(x, (cy - dy).round() as i32, color);
416 self.pset(x, (cy + dy).round() as i32, color);
417 }
418 }
419 }
420
421 pub fn print(&mut self, text: &str, x: i32, y: i32, color: u8) -> i32 {
424 let mut cx = x;
425 let mut cy = y;
426 for ch in text.chars() {
427 if ch == '\n' {
428 cx = x;
429 cy += font::GLYPH_H;
430 continue;
431 }
432 let rows = font::glyph(ch);
433 for (ry, row) in rows.iter().enumerate() {
434 for rx in 0..3 {
435 if row & (0b100 >> rx) != 0 {
436 self.pset(cx + rx, cy + ry as i32, color);
437 }
438 }
439 }
440 cx += font::GLYPH_W;
441 }
442 cx
443 }
444
445 pub fn set_pen_color(&mut self, color: u8) {
447 self.pen_color = color & 0x0f;
448 }
449
450 pub fn set_cursor(&mut self, x: i32, y: i32) {
452 self.cursor_x = x;
453 self.cursor_y = y;
454 }
455
456 pub fn print_pen(&mut self, text: &str) -> i32 {
459 let (x, y) = (self.cursor_x, self.cursor_y);
460 let end = self.print(text, x, y, self.pen_color);
461 self.cursor_y = y + font::GLYPH_H;
462 end
463 }
464
465 #[allow(clippy::too_many_arguments)]
469 pub fn spr(
470 &mut self,
471 sheet: &SpriteSheet,
472 n: u32,
473 x: i32,
474 y: i32,
475 w: i32,
476 h: i32,
477 flip_x: bool,
478 flip_y: bool,
479 ) {
480 let pw = w.max(0);
482 let ph = h.max(0);
483
484 let (dx0, dy0) = (x - self.camera_x, y - self.camera_y);
491 let (cx0, cy0, cx1, cy1) = self.clip;
492 let px_lo = (cx0 - dx0).max(0);
493 let px_hi = (cx1 - dx0).min(pw);
494 let py_lo = (cy0 - dy0).max(0);
495 let py_hi = (cy1 - dy0).min(ph);
496 if px_lo >= px_hi || py_lo >= py_hi {
497 return;
498 }
499
500 let n = (n as usize) % SPRITE_COUNT;
505 let base_sx = (n % SPRITES_PER_ROW * SPRITE_SIZE) as i32;
506 let base_sy = (n / SPRITES_PER_ROW * SPRITE_SIZE) as i32;
507 for py in py_lo..py_hi {
508 let sy = if flip_y { ph - 1 - py } else { py };
509 for px in px_lo..px_hi {
510 let sx = if flip_x { pw - 1 - px } else { px };
511 let c = sheet.get(base_sx + sx, base_sy + sy);
512 if ((self.transparent >> c) & 1) == 0 {
513 let px_dst = dx0 + px;
515 let py_dst = dy0 + py;
516 self.pixels[(py_dst * WIDTH + px_dst) as usize] =
517 self.draw_pal[(c & 0x0f) as usize] & 0x0f;
518 }
519 }
520 }
521 }
522
523 #[allow(clippy::too_many_arguments)]
527 pub fn sspr(
528 &mut self,
529 sheet: &SpriteSheet,
530 sx: i32,
531 sy: i32,
532 sw: i32,
533 sh: i32,
534 dx: i32,
535 dy: i32,
536 dw: i32,
537 dh: i32,
538 flip_x: bool,
539 flip_y: bool,
540 ) {
541 if sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 {
542 return;
543 }
544 for py in 0..dh {
545 for px in 0..dw {
546 let fx = if flip_x { dw - 1 - px } else { px };
547 let fy = if flip_y { dh - 1 - py } else { py };
548 let c = sheet.get(sx + fx * sw / dw, sy + fy * sh / dh);
549 if ((self.transparent >> c) & 1) == 0 {
550 self.pset(dx + px, dy + py, c);
551 }
552 }
553 }
554 }
555
556 #[allow(clippy::too_many_arguments)]
559 pub fn map(
560 &mut self,
561 map: &MapData,
562 sheet: &SpriteSheet,
563 cel_x: i32,
564 cel_y: i32,
565 sx: i32,
566 sy: i32,
567 cel_w: i32,
568 cel_h: i32,
569 layers: u8,
570 ) {
571 for ty in 0..cel_h {
572 for tx in 0..cel_w {
573 let tile = map.get(cel_x + tx, cel_y + ty);
574 if tile == 0 {
575 continue;
576 }
577 if layers != 0 && sheet.flags(tile as u32) & layers == 0 {
578 continue;
579 }
580 self.spr(
581 sheet,
582 tile as u32,
583 sx + tx * SPRITE_SIZE as i32,
584 sy + ty * SPRITE_SIZE as i32,
585 SPRITE_SIZE as i32,
586 SPRITE_SIZE as i32,
587 false,
588 false,
589 );
590 }
591 }
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn cls_fills_screen() {
601 let mut fb = Framebuffer::new();
602 fb.cls(7);
603 assert!(fb.pixels().iter().all(|&p| p == 7));
604 }
605
606 #[test]
607 fn pset_pget_roundtrip() {
608 let mut fb = Framebuffer::new();
609 fb.pset(10, 20, 8);
610 assert_eq!(fb.pget(10, 20), 8);
611 assert_eq!(fb.pget(11, 20), 0);
612 }
613
614 #[test]
615 fn out_of_bounds_is_safe() {
616 let mut fb = Framebuffer::new();
617 fb.pset(-1, 0, 5);
618 fb.pset(0, 99999, 5);
619 fb.line(-50, -50, 200, 200, 6);
620 fb.circfill(0, 0, 300, 3);
621 assert_eq!(fb.pget(-1, 0), 0);
622 }
623
624 #[test]
625 fn camera_offsets_draws() {
626 let mut fb = Framebuffer::new();
627 fb.camera(10, 0);
628 fb.pset(15, 5, 9);
629 assert_eq!(fb.pget(5, 5), 9);
630 fb.reset_state();
631 fb.pset(15, 5, 9);
632 assert_eq!(fb.pget(15, 5), 9);
633 }
634
635 #[test]
636 fn clip_constrains_drawing() {
637 let mut fb = Framebuffer::new();
638 fb.clip(0, 0, 4, 4);
639 fb.rectfill(0, 0, 127, 127, 7);
640 assert_eq!(fb.pget(3, 3), 7);
641 assert_eq!(fb.pget(4, 4), 0);
642 }
643
644 #[test]
645 fn rect_outline_is_hollow() {
646 let mut fb = Framebuffer::new();
647 fb.rect(0, 0, 4, 4, 7);
648 assert_eq!(fb.pget(0, 0), 7);
649 assert_eq!(fb.pget(4, 4), 7);
650 assert_eq!(fb.pget(2, 2), 0);
651 }
652
653 #[test]
654 fn print_advances_cursor() {
655 let mut fb = Framebuffer::new();
656 let end = fb.print("abc", 0, 0, 7);
657 assert_eq!(end, 3 * font::GLYPH_W);
658 }
659
660 #[test]
661 fn partial_pixel_sprite_draws_a_partial_slice() {
662 let mut fb = Framebuffer::new();
663 let mut sheet = SpriteSheet::default();
664 for y in 0..8 {
666 for x in 0..8 {
667 sheet.set(x, y, 7);
668 }
669 }
670 fb.spr(&sheet, 0, 0, 0, 4, 8, false, false);
672 assert_eq!(fb.pget(3, 4), 7, "left half is drawn");
673 assert_eq!(fb.pget(4, 4), 0, "right half is untouched");
674 assert_eq!(fb.pget(7, 7), 0, "bottom-right corner is untouched");
675 }
676
677 #[test]
678 fn spr_clipped_flip_mirrors_about_full_sprite() {
679 let mut fb = Framebuffer::new();
684 let mut sheet = SpriteSheet::default();
685 sheet.set(0, 0, 8); sheet.set(7, 0, 9); sheet.set(0, 7, 11); sheet.set(7, 7, 12); fb.clip(0, 0, 4, 4);
690 fb.spr(&sheet, 0, 0, 0, 8, 8, true, true);
691 assert_eq!(
692 fb.pget(0, 0),
693 12,
694 "source bottom-right mirrors to dest (0,0) and survives the clip"
695 );
696 assert_eq!(fb.pget(7, 7), 0, "dest (7,7) is outside the 4x4 clip");
697 assert_eq!(fb.pget(0, 7), 0, "dest (0,7) is outside the 4x4 clip");
698 assert_eq!(fb.pget(7, 0), 0, "dest (7,0) is outside the 4x4 clip");
699 }
700
701 #[test]
702 fn spr_partly_offscreen_under_camera_aligns_source() {
703 let mut fb = Framebuffer::new();
707 let mut sheet = SpriteSheet::default();
708 for y in 0..8 {
709 for x in 0..8 {
710 sheet.set(x, y, 7);
711 }
712 }
713 sheet.set(2, 3, 9); fb.camera(2, 3);
715 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
716 assert_eq!(
717 fb.pget(0, 0),
718 9,
719 "source (2,3) lands at the top-left corner"
720 );
721 assert_eq!(fb.pget(1, 0), 7, "source (3,3) is the next column");
722 assert_eq!(fb.pget(5, 4), 7, "the rest of the on-screen part is drawn");
723 assert_eq!(fb.pget(127, 127), 0, "no wrap to the opposite corner");
725 }
726
727 #[test]
728 fn spr_partial_pixel_width_meets_clip_edge() {
729 let mut fb = Framebuffer::new();
732 let mut sheet = SpriteSheet::default();
733 for y in 0..8 {
734 for x in 0..8 {
735 sheet.set(x, y, 7);
736 }
737 }
738 fb.clip(0, 0, 2, 128);
739 fb.spr(&sheet, 0, 0, 0, 4, 8, false, false);
740 assert_eq!(fb.pget(1, 3), 7, "inside the clip and the partial width");
741 assert_eq!(fb.pget(2, 3), 0, "clipped away at x=2");
742 assert_eq!(fb.pget(4, 3), 0, "beyond the 4px width anyway");
743 }
744
745 #[test]
746 fn transparency_mask_controls_sprite_pixels() {
747 let mut fb = Framebuffer::new();
748 let mut sheet = SpriteSheet::default();
749 for y in 0..8 {
750 for x in 0..8 {
751 sheet.set(x, y, 8); }
753 }
754 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
756 assert_eq!(fb.pget(1, 1), 8);
757 fb.cls(3);
759 fb.set_transparent_color(8, true);
760 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
761 assert_eq!(fb.pget(1, 1), 3, "red made transparent");
762 fb.reset_transparency();
764 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
765 assert_eq!(fb.pget(1, 1), 8);
766 }
767
768 #[test]
769 fn color_zero_can_be_made_opaque() {
770 let mut fb = Framebuffer::new();
771 let sheet = SpriteSheet::default(); fb.cls(7);
773 fb.set_transparent_color(0, false);
774 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
775 assert_eq!(fb.pget(3, 3), 0, "color 0 now drawn over white");
776 }
777
778 #[test]
779 fn draw_palette_remaps_writes() {
780 let mut fb = Framebuffer::new();
781 fb.remap_color(8, 12); fb.pset(5, 5, 8);
783 assert_eq!(fb.pget(5, 5), 12);
784 fb.reset_palette();
785 fb.pset(6, 6, 8);
786 assert_eq!(fb.pget(6, 6), 8);
787 }
788
789 #[test]
790 fn cls_ignores_draw_palette() {
791 let mut fb = Framebuffer::new();
792 fb.remap_color(0, 8);
793 fb.cls(0);
794 assert_eq!(fb.pget(10, 10), 0, "cls clears to the literal color");
795 }
796
797 #[test]
798 fn display_palette_remaps_at_upload() {
799 let mut fb = Framebuffer::new();
800 fb.pset(0, 0, 8); fb.remap_display_color(8, 12); let mut out = vec![0u8; (WIDTH * HEIGHT * 4) as usize];
803 fb.write_rgba(&mut out);
804 assert_eq!(&out[0..4], &palette::rgba(12), "pixel uploaded as blue");
805 assert_eq!(fb.pget(0, 0), 8, "stored index is unchanged");
806 }
807
808 #[test]
809 fn ovalfill_fills_center_not_corner() {
810 let mut fb = Framebuffer::new();
811 fb.ovalfill(0, 0, 10, 6, 7);
812 assert_eq!(fb.pget(5, 3), 7, "center filled");
813 assert_eq!(fb.pget(0, 0), 0, "bounding-box corner stays empty");
814 }
815
816 #[test]
817 fn oval_outline_is_hollow() {
818 let mut fb = Framebuffer::new();
819 fb.oval(0, 0, 10, 10, 7);
820 assert_eq!(fb.pget(5, 0), 7, "top of the outline is set");
821 assert_eq!(fb.pget(5, 5), 0, "center is hollow");
822 }
823
824 #[test]
825 fn two_color_fill_pattern_alternates() {
826 let mut fb = Framebuffer::new();
827 fb.set_fill_pattern(0b1010_0101_1010_0101, 12, false);
829 fb.rectfill(0, 0, 3, 3, 7);
830 assert_eq!(
831 fb.pget(0, 0),
832 12,
833 "pattern-1 pixel uses the secondary color"
834 );
835 assert_eq!(fb.pget(1, 0), 7, "pattern-0 pixel uses the primary color");
836 }
837
838 #[test]
839 fn transparent_fill_pattern_skips_pixels() {
840 let mut fb = Framebuffer::new();
841 fb.cls(3);
842 fb.set_fill_pattern(0xffff, 0, true); fb.rectfill(0, 0, 3, 3, 7);
844 assert_eq!(
845 fb.pget(1, 1),
846 3,
847 "all pattern-1 pixels skipped; background shows"
848 );
849 }
850
851 #[test]
852 fn zero_pattern_fills_solid() {
853 let mut fb = Framebuffer::new();
854 fb.set_fill_pattern(0, 0, false);
855 fb.rectfill(0, 0, 3, 3, 7);
856 assert_eq!(fb.pget(2, 2), 7);
857 }
858
859 #[test]
860 fn sspr_upscales_with_nearest_neighbor() {
861 let mut fb = Framebuffer::new();
862 let mut sheet = SpriteSheet::default();
863 sheet.set(0, 0, 8); fb.sspr(&sheet, 0, 0, 1, 1, 10, 10, 4, 4, false, false);
865 assert_eq!(fb.pget(10, 10), 8);
866 assert_eq!(
867 fb.pget(13, 13),
868 8,
869 "the whole 4x4 block is the source pixel"
870 );
871 }
872
873 #[test]
874 fn sspr_respects_transparency() {
875 let mut fb = Framebuffer::new();
876 let sheet = SpriteSheet::default(); fb.cls(3);
878 fb.sspr(&sheet, 0, 0, 2, 2, 0, 0, 4, 4, false, false);
879 assert_eq!(fb.pget(1, 1), 3, "color 0 is transparent by default");
880 }
881
882 #[test]
883 fn sspr_flips_horizontally() {
884 let mut fb = Framebuffer::new();
885 let mut sheet = SpriteSheet::default();
886 sheet.set(0, 0, 8);
887 sheet.set(1, 0, 9);
888 fb.sspr(&sheet, 0, 0, 2, 1, 0, 0, 2, 1, true, false);
889 assert_eq!(
890 fb.pget(1, 0),
891 8,
892 "flip puts the source-left pixel on the right"
893 );
894 assert_eq!(fb.pget(0, 0), 9);
895 }
896
897 #[test]
898 fn print_pen_matches_print_at_cursor() {
899 let mut a = Framebuffer::new();
900 let mut b = Framebuffer::new();
901 a.set_pen_color(9);
902 a.set_cursor(10, 20);
903 let end_a = a.print_pen("hi");
904 let end_b = b.print("hi", 10, 20, 9);
905 assert_eq!(end_a, end_b);
906 assert_eq!(
907 a.pixels(),
908 b.pixels(),
909 "print_pen draws identically to print"
910 );
911 }
912
913 #[test]
914 fn print_pen_advances_cursor_one_line() {
915 let mut fb = Framebuffer::new();
916 fb.set_cursor(5, 5);
917 fb.print_pen("x");
918 fb.print_pen("y");
919 let mut expect = Framebuffer::new();
920 expect.print("x", 5, 5, 6); expect.print("y", 5, 5 + font::GLYPH_H, 6);
922 assert_eq!(fb.pixels(), expect.pixels());
923 }
924}