354 lines
11 KiB
TypeScript
354 lines
11 KiB
TypeScript
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 (
|
|
<div class={styles.root}>
|
|
<div className={styles.field}>
|
|
{range(FIELD_WIDTH * FIELD_HEIGHT).map(i => (
|
|
<div
|
|
key={i}
|
|
class={clsx(styles.pixel, {
|
|
[styles.active]: this.#field[i],
|
|
})}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className={styles.sidebar}>
|
|
<div className={styles.score}>{this.#score}</div>
|
|
<div className={styles.miniField}>
|
|
{range(MINI_FIELD_WIDTH * MINI_FIELD_HEIGHT).map(i => (
|
|
<div
|
|
key={i}
|
|
class={clsx(styles.pixel, {
|
|
[styles.active]: this.#miniField[i],
|
|
})}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className={styles.speedLevel}>
|
|
<div className={styles.speedLevelColumn}>
|
|
<div className={styles.value}>{this.#speed}</div>
|
|
<div className={styles.text}>Speed</div>
|
|
</div>
|
|
<div className={styles.speedLevelColumn}>
|
|
<div className={styles.value}>{this.#level}</div>
|
|
<div className={styles.text}>Level</div>
|
|
</div>
|
|
</div>
|
|
<div className={styles.helpText}>
|
|
{this.helpText}
|
|
</div>
|
|
<div className={styles.footer}>
|
|
<div className={clsx(styles.text, this.pause && styles.active)}>Pause</div>
|
|
<div className={clsx(styles.text, this.gameOver && styles.active)}>Game over</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |