import { render } from "preact"; import type { ReactElement } from "preact/compat"; import { clamp, range } from "@common/utils"; import clsx from "clsx"; import styles from './assets/brick.module.css'; import "./assets/lcd.font.css"; const FIELD_WIDTH = 10; const FIELD_HEIGHT = 20; const MINI_FIELD_WIDTH = 4; const MINI_FIELD_HEIGHT = 4; export interface BrickDisplayImage { image: boolean[]; width: number; height: number; } export class BrickDisplay { #field: boolean[] = new Array(FIELD_HEIGHT * FIELD_WIDTH); #miniField: boolean[] = new Array(MINI_FIELD_HEIGHT * MINI_FIELD_WIDTH); #score: number = 0; #speed: number = 1; #level: number = 1; public pause: boolean = false; public gameOver: boolean = false; public helpText: string | ReactElement = ''; init() { this.update(); } get score() { return this.#score; } set score(value) { this.#score = Math.max(0, (value | 0) % 1000000000); } get speed() { return this.#speed; } set speed(value) { this.#speed = clamp(value | 0, 1, 15); } get level() { return this.#level; } set level(value) { this.#level = clamp(value | 0, 1, 15); } get width() { return FIELD_WIDTH; } get height() { return FIELD_HEIGHT; } update() { render(this.#display, document.body); } setPixel(x: number, y: number, value: boolean, miniDisplay = false) { const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH; const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT; x = x | 0; y = y | 0; if (x < 0 || x >= w || y < 0 || y >= h) return; const field = miniDisplay ? this.#miniField : this.#field; field[y * w + x] = value; } getPixel(x: number, y: number, miniDisplay = false): boolean { const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH; const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT; x = x | 0; y = y | 0; if (x < 0 || x >= w || y < 0 || y >= h) return false; const field = miniDisplay ? this.#miniField : this.#field; return field[y * w + x]; } togglePixel(x: number, y: number, miniDisplay = false) { const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH; const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT; x = x | 0; y = y | 0; if (x < 0 || x >= w || y < 0 || y >= h) return; const field = miniDisplay ? this.#miniField : this.#field; field[y * w + x] = !field[y * w + x]; } clear(miniDisplay = false) { const length = miniDisplay ? this.#miniField.length : this.#field.length; const field = miniDisplay ? this.#miniField : this.#field; for (let i = 0; i < length; i++) { field[i] = false; } } drawVLine(x: number, y1: number, y2: number, value = true, miniDisplay = false) { const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH; const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT; if (x < 0 || x >= w) return; x = x | 0; y1 = y1 | 0; y2 = y2 | 0; if (y2 < y1) { const t = y2; y2 = y1; y1 = t; } if (y2 < 0 || y1 >= h) return; for (let y = Math.max(y1, 0); y <= Math.min(y2, h); y++) { this.setPixel(x, y, value, miniDisplay); } } drawHLine(x1: number, x2: number, y: number, value = true, miniDisplay = false) { const w = miniDisplay ? MINI_FIELD_WIDTH : FIELD_WIDTH; const h = miniDisplay ? MINI_FIELD_HEIGHT : FIELD_HEIGHT; if (y < 0 || y >= h) return; x1 = x1 | 0; x2 = x2 | 0; y = y | 0; if (x2 < x1) { const t = x2; x2 = x1; x1 = t; } if (x2 < 0 || x1 >= w) return; for (let x = Math.max(x1, 0); x <= Math.min(x2, w); x++) { this.setPixel(x, y, value, miniDisplay); } } drawRect(x1: number, y1: number, x2: number, y2: number, value = true, miniDisplay = false) { this.drawHLine(x1, x2, y1, value, miniDisplay); this.drawHLine(x1, x2, y2, value, miniDisplay); this.drawVLine(x1, y1, y2, value, miniDisplay); this.drawVLine(x2, y1, y2, value, miniDisplay); } fillRect(x1: number, y1: number, x2: number, y2: number, value = true, miniDisplay = false) { y1 = y1 | 0; y2 = y2 | 0; if (y2 < y1) { const t = y2; y2 = y1; y1 = t; } for (let y = Math.max(0, y1); y <= y2; y++) { this.drawHLine(x1, x2, y, value, miniDisplay); } } drawImage(image: BrickDisplayImage, x: number, y: number, miniDisplay = false, xor = false) { for (let j = 0; j < image.height; j++) { for (let i = 0; i < image.width; i++) { const px = image.image[j * image.width + i]; if (px) { if (xor) { this.togglePixel(x + i, y + j, miniDisplay); } else { this.setPixel(x + i, y + j, px, miniDisplay); } } } } } get #display() { return (
{range(FIELD_WIDTH * FIELD_HEIGHT).map(i => (
))}
{this.#score}
{range(MINI_FIELD_WIDTH * MINI_FIELD_HEIGHT).map(i => (
))}
{this.#speed}
Speed
{this.#level}
Level
{this.helpText}
Pause
Game over
); } static convertImage(image: HTMLImageElement): BrickDisplayImage { const result: BrickDisplayImage = { image: [], width: 0, height: 0, } const canvas = document.createElement('canvas'); result.width = canvas.width = image.naturalWidth; result.height = canvas.height = image.naturalHeight; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(image, 0, 0); const pxData = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < pxData.data.length; i += 4) { result.image[i >> 2] = pxData.data[i] < 128; } } return result; } static extractSprite(image: BrickDisplayImage, x: number, y: number, w: number, h: number): BrickDisplayImage { if (w <= 0 || h <= 0 || x >= image.width || y >= image.height) { return { image: [], width: 0, height: 0 }; } x = clamp(x | 0, 0, image.width); y = clamp(y | 0, 0, image.height); w = clamp(w | 0, 1, image.width - x); h = clamp(h | 0, 1, image.height - y); const result: BrickDisplayImage = { image: new Array(w * h), width: w, height: h, } for (let j = 0; j < h; j++) { for (let i = 0; i < w; i++) { const px = image.image[(y + j) * image.width + (x + i)]; result.image[j * w + i] = px; } } return result; } static autoCrop(image: BrickDisplayImage): BrickDisplayImage { let minX = image.width; let minY = image.height; let maxX = 0; let maxY = 0; for (let y = 0; y < image.height; y++) { for (let x = 0; x < image.width; x++) { const i = y * image.width + x; if (image.image[i]) { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); } } } return this.extractSprite(image, minX, minY, maxX - minX + 1, maxY - minY + 1); } static copySprite(image: BrickDisplayImage): BrickDisplayImage { return this.extractSprite(image, 0, 0, image.width, image.height); } static rotateSprite(image: BrickDisplayImage, angle: 0 | 90 | 180 | 270): BrickDisplayImage { if (angle === 0) return this.copySprite(image); const newImage: BrickDisplayImage = { image: new Array(image.width * image.height), width: angle === 180 ? image.width : image.height, height: angle === 180 ? image.height : image.width, } for (let j = 0; j < image.height; j++) { for (let i = 0; i < image.width; i++) { const originalPixel = image.image[j * image.width + i]; let x = i; let y = j; if (angle === 90) { const tmp = y; y = x; x = image.height - tmp - 1; } else if (angle === 180) { x = image.width - x - 1; y = image.height - y - 1; } else if (angle === 270) { const tmp = x; x = y; y = image.width - tmp - 1; } newImage.image[y * newImage.width + x] = originalPixel } } return newImage; } }