1
0
Fork 0

Brick game display

This commit is contained in:
Pabloader 2024-07-07 20:58:33 +00:00
parent 877742cafb
commit 655ecf6858
20 changed files with 518 additions and 16 deletions

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$TITLE$</title>
<style>
html, body, #canvas {
html, body {
width: 100vw;
height: 100vh;
margin: 0;
@ -30,7 +30,6 @@
$ICON$
</head>
<body>
<canvas id="canvas"></canvas>
$SCRIPT$
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,150 @@
body {
--pixel-size: calc(min(100vh, 133vw) / 20);
--bg-color: #a2bf8f;
--inactive-color: #9eb98c;
--active-color: #333b2d;
--color: var(--inactive-color);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-color);
}
.root {
display: flex;
flex-direction: row;
align-items: center;
height: calc(var(--pixel-size) * 20);
width: calc(var(--pixel-size) * 15);
border: 1px solid black;
}
.field {
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(20, 1fr);
height: 100%;
max-width: 66%;
border: 1px solid black;
}
.pixel {
width: calc(var(--pixel-size) * 0.8);
height: calc(var(--pixel-size) * 0.8);
margin: calc(var(--pixel-size) * 0.05);
color: var(--color);
border-width: calc(var(--pixel-size) * 0.05);
border-style: solid;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
width: calc(var(--pixel-size) * 0.4);
height: calc(var(--pixel-size) * 0.4);
background-color: var(--color);
}
&.active {
--color: var(--active-color);
}
}
.sidebar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
height: 100%;
width: 100%;
.score {
width: calc(100% - var(--pixel-size) * 0.4);
text-align: right;
font-size: calc(var(--pixel-size) * 0.8);
margin: calc(var(--pixel-size) * 0.5) 0;
font-family: 'LCD', monospace;
font-style: italic;
position: relative;
&::before {
content: '888888888';
color: var(--inactive-color);
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.miniField {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.speedLevel {
display: flex;
flex-direction: row;
gap: calc(var(--pixel-size) * 0.4);
font-size: calc(var(--pixel-size) * 0.6);
.value {
margin: calc(var(--pixel-size) * 0.4);
text-align: right;
position: relative;
font-family: 'LCD', monospace;
font-style: italic;
position: relative;
&::before {
content: '18';
color: var(--inactive-color);
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
}
}
.text {
text-transform: uppercase;
font-family: monospace;
}
.footer {
position: absolute;
bottom: 0;
text-align: center;
padding: calc(var(--pixel-size) * 0.5) 0;
.text {
color: var(--inactive-color);
font-size: calc(var(--pixel-size) * 0.8);
&.active {
animation: blink 1s steps(1, end);
animation-iteration-count: infinite;
}
}
}
@keyframes blink {
0% {
color: var(--inactive-color);
}
50% {
color: var(--active-color);
}
}

View File

@ -0,0 +1,6 @@
@font-face {
font-family: 'LCD';
src: url("./7-segments-display.ttf") format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,288 @@
import { render } from "preact";
import type { Display } from ".";
import { clamp, ensureImageLoaded, range } from "@common/utils";
import classNames from "classnames";
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;
interface BrickDisplayImage {
image: boolean[];
width: number;
height: number;
}
export class BrickDisplay implements Display {
#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;
init() {
this.update();
}
get score() {
return this.#score;
}
set score(value) {
this.#score = (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) {
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) {
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={classNames(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={classNames(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.footer}>
<div className={classNames(styles.text, this.pause && styles.active)}>Pause</div>
<div className={classNames(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,
}
ensureImageLoaded(image).then(() => {
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;
}
}

View File

@ -0,0 +1,6 @@
export interface Display {
init(): void;
update(): void;
}
export { BrickDisplay } from './brick';

View File

@ -2,9 +2,11 @@ export const delay = async (ms: number) => new Promise((resolve) => setTimeout(r
export const nextFrame = async (): Promise<number> => new Promise((resolve) => requestAnimationFrame(resolve));
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
export const randBool = () => Math.random() < 0.5;
export const choice = (array: any[]) => array[randInt(0, array.length)];
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
export const prevent = (e: Event) => (e.preventDefault(), false);
@ -20,3 +22,8 @@ export const intHash = (seed: number, ...parts: number[]) => {
return h1;
};
export const sinHash = (...data: number[]) => data.reduce((hash, n) => Math.sin((hash * 123.12 + n) * 756.12), 0) / 2 + 0.5;
export const ensureImageLoaded = async (image: HTMLImageElement): Promise<void> =>
image.naturalWidth === 0
? new Promise(r => image.addEventListener('load', () => r()))
: void 0;

View File

@ -5,6 +5,10 @@
--color-border-select: rgb(0, 200, 0);
}
body {
font-family: 'IBM VGA 8x16', monospace;
}
.button {
display: flex;
justify-content: center;

View File

@ -43,7 +43,7 @@ export default class Graphics {
resetStyle() {
this.context = this.canvas.getContext('2d')!;
this.context.imageSmoothingEnabled = false;
this.context.font = 'bold 0.3px sans-serif';
this.context.font = '0.4px "IBM VGA 8x16"';
this.context.textRendering = 'optimizeSpeed';
this.context.textAlign = 'center';
this.context.textBaseline = 'middle';

View File

@ -1,6 +1,12 @@
import Binario from "./game";
export default function runGame(canvas: HTMLCanvasElement) {
export default function runGame() {
const canvas = document.createElement('canvas');
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.margin = '0';
document.body.append(canvas);
const game = new Binario(canvas);
game.run();
}

View File

@ -53,7 +53,7 @@ export const renderResource = (ctx: CanvasRenderingContext2D, view: ViewConfig,
ctx.lineWidth = px * 2;
ctx.miterLimit = 2;
if (tile.type === TileType.SOURCE) {
ctx.font = `bold ${0.3 * fontScale}px sans-serif`;
ctx.font = `${0.4 * fontScale}px "IBM VGA 8x16"`;
}
ctx.strokeText(str, x, y);
ctx.fillText(str, x, y);

View File

@ -2,6 +2,7 @@ import React, { render } from 'preact';
import cn from 'classnames';
import { TileType } from './world';
import "@common/assets/vga.font.css"
import styles from './assets/ui.module.css';
import conveyorImage from './assets/img/conveyor.png';
import extractorImage from './assets/img/extractor.png';

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View File

@ -0,0 +1,42 @@
import { BrickDisplay } from "@common/display";
import { delay, randBool, randInt } from "@common/utils";
import iconImage from './assets/icon.png';
import background from './assets/background.png';
let display: BrickDisplay;
const iconSprite = BrickDisplay.convertImage(iconImage);
const backgroundSprite = BrickDisplay.convertImage(background);
let bgY = 0;
function moveBackground() {
bgY++;
if (bgY >= 0) {
bgY -= display.height;
}
}
let prevFrameTime: number = 0;
async function loop(time: number) {
const dt = time - prevFrameTime;
prevFrameTime = time;
moveBackground();
display.clear();
display.drawImage(backgroundSprite, 0, bgY);
display.drawImage(backgroundSprite, 0, bgY + display.height);
display.update();
await delay(100);
requestAnimationFrame(loop);
}
export default function main() {
display = new BrickDisplay();
display.init();
requestAnimationFrame(loop);
}

View File

@ -3,8 +3,7 @@ import styles from './game.module.css';
declare const GAMES: string[];
export default function run(canvas: HTMLCanvasElement) {
canvas.remove();
export default function run() {
const root = <div class={styles.games}>
{GAMES.map(g => <a key={g} href={`?game=${g}`}>{g}</a>)}
</div>;

View File

@ -1,8 +1,7 @@
import '@common/assets/vga.font.css';
import './assets/style.css';
export default async function run(canvas: HTMLCanvasElement) {
canvas.remove();
export default async function run() {
const root = document.createElement('div');
root.id = 'root';
document.body.append(root);

View File

@ -2,12 +2,7 @@ declare const GAME: string;
async function main() {
const { default: runGame }: { default: RunGame } = await import(`./games/${GAME}`);
const canvas = document.getElementById('canvas');
if (canvas instanceof HTMLCanvasElement) {
await runGame(canvas);
} else {
alert('Something wrong with your canvas!');
}
await runGame();
}
main().catch((e) => {

2
src/types.d.ts vendored
View File

@ -1,7 +1,7 @@
type Point = [number, number];
type Rect = [number, number, number, number];
type RunGame = (canvas: HTMLCanvasElement) => Promise<void>;
type RunGame = () => Promise<void>;
declare module '*.module.css' {
const classes: { [key: string]: string };