1
0
Fork 0
tsgames/src/common/display/brick.tsx

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;
}
}