Brick game display
This commit is contained in:
parent
877742cafb
commit
655ecf6858
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@font-face {
|
||||
font-family: 'LCD';
|
||||
src: url("./7-segments-display.ttf") format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export interface Display {
|
||||
init(): void;
|
||||
update(): void;
|
||||
}
|
||||
|
||||
export { BrickDisplay } from './brick';
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
--color-border-select: rgb(0, 200, 0);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'IBM VGA 8x16', monospace;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue