Compare commits
10 commits
2f77cf7b74
...
0052328511
Author | SHA1 | Date | |
---|---|---|---|
0052328511 | |||
6484d42dab | |||
1605e743f4 | |||
6c8f2d780a | |||
7dce448d9b | |||
448d8c52a4 | |||
bfdecdbd3c | |||
9eed5c04ff | |||
d35a68caf3 | |||
764cd8344d |
36 changed files with 3133 additions and 3000 deletions
15
.eslintrc.js
15
.eslintrc.js
|
@ -1,6 +1,10 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'react-hooks',
|
||||||
|
],
|
||||||
extends: [
|
extends: [
|
||||||
|
'plugin:react/recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'prettier/@typescript-eslint',
|
'prettier/@typescript-eslint',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
|
@ -8,8 +12,19 @@ module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2018,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'react/prop-types': [0],
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
'prettier/prettier': 'warn',
|
'prettier/prettier': 'warn',
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/dist
|
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.webpack
|
||||||
|
|
15
build/webpack.main.js
Normal file
15
build/webpack.main.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/main/index.ts',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
};
|
30
build/webpack.renderer.js
Normal file
30
build/webpack.renderer.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||||
|
use: 'file-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(vs|fs)$/,
|
||||||
|
use: 'raw-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [new CopyPlugin([{ from: 'static' }])],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"renderer": {
|
|
||||||
"webpackConfig": "webpack.renderer.js"
|
|
||||||
}
|
|
||||||
}
|
|
68
package.json
68
package.json
|
@ -1,25 +1,67 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": ".webpack/main",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-webpack dev",
|
"start": "electron-forge start",
|
||||||
"lint": "eslint 'src/**/*.ts'"
|
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"forge": {
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@electron-forge/plugin-webpack",
|
||||||
|
{
|
||||||
|
"mainConfig": "./build/webpack.main.js",
|
||||||
|
"renderer": {
|
||||||
|
"config": "./build/webpack.renderer.js",
|
||||||
|
"entryPoints": [
|
||||||
|
{
|
||||||
|
"html": "./src/renderer/index.html",
|
||||||
|
"js": "./src/renderer/index.tsx",
|
||||||
|
"name": "main_window"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gl-matrix": "^2.4.5",
|
"@electron-forge/cli": "^6.0.0-beta.50",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.13.0",
|
"@electron-forge/plugin-webpack": "^6.0.0-beta.50",
|
||||||
"@typescript-eslint/parser": "^2.13.0",
|
"@types/color": "^3.0.1",
|
||||||
"electron": "^7.1.7",
|
"@types/electron-devtools-installer": "^2.2.0",
|
||||||
"electron-webpack": "^2.7.4",
|
"@types/react": "^16.9.25",
|
||||||
"electron-webpack-ts": "^3.2.0",
|
"@types/react-dom": "^16.9.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.24.0",
|
||||||
|
"@typescript-eslint/parser": "^2.24.0",
|
||||||
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
|
"css-loader": "^3.4.2",
|
||||||
|
"electron": "^8.1.1",
|
||||||
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-prettier": "^6.7.0",
|
"eslint-config-prettier": "^6.10.0",
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
"eslint-plugin-prettier": "^3.1.2",
|
||||||
"prettier": "^1.19.1",
|
"eslint-plugin-react": "^7.19.0",
|
||||||
|
"eslint-plugin-react-hooks": "^2.5.1",
|
||||||
|
"file-loader": "^6.0.0",
|
||||||
|
"prettier": "^2.0.1",
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
"typescript": "^3.7.4",
|
"style-loader": "^1.1.3",
|
||||||
"webpack": "^4.41.4"
|
"ts-loader": "^6.2.1",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
|
"webpack": "^4.42.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gl-matrix": "^3.1.0"
|
"@material-ui/core": "^4.9.7",
|
||||||
|
"color": "^3.1.2",
|
||||||
|
"fast-glob": "^3.2.2",
|
||||||
|
"gl-matrix": "^3.2.1",
|
||||||
|
"immutable": "^4.0.0-rc.12",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"typeface-roboto": "^0.0.75"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
project/tiling/dirt/0_0.png
Normal file
BIN
project/tiling/dirt/0_0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
5
project/tiling/dirt/meta.json
Normal file
5
project/tiling/dirt/meta.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Dirt",
|
||||||
|
"width": 1,
|
||||||
|
"height": 1
|
||||||
|
}
|
BIN
project/tiling/grass/0_0.png
Normal file
BIN
project/tiling/grass/0_0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
5
project/tiling/grass/meta.json
Normal file
5
project/tiling/grass/meta.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Grass",
|
||||||
|
"width": 1,
|
||||||
|
"height": 1
|
||||||
|
}
|
|
@ -1,56 +1,55 @@
|
||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import * as path from 'path';
|
|
||||||
import { format as formatUrl } from 'url';
|
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
// global reference to mainWindow (necessary to prevent window from being garbage collected)
|
app.allowRendererProcessReuse = true;
|
||||||
let mainWindow: BrowserWindow|null = null;
|
|
||||||
|
|
||||||
function getIndexURL(): string {
|
async function installReactDevTools(): Promise<void> {
|
||||||
if (isDevelopment)
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
|
||||||
return `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`;
|
const name = await installExtension(REACT_DEVELOPER_TOOLS);
|
||||||
|
console.log(`Added Extension: ${name}`);
|
||||||
return formatUrl({
|
|
||||||
pathname: path.join(__dirname, 'index.html'),
|
|
||||||
protocol: 'file',
|
|
||||||
slashes: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createWindow(): void {
|
||||||
const window = new BrowserWindow({webPreferences: {nodeIntegration: true}});
|
const window = new BrowserWindow({ webPreferences: { nodeIntegration: true } });
|
||||||
|
|
||||||
if (isDevelopment)
|
window.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
window.webContents.openDevTools();
|
window.webContents.openDevTools();
|
||||||
|
}
|
||||||
window.loadURL(getIndexURL());
|
|
||||||
|
|
||||||
window.on('closed', () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.webContents.on('devtools-opened', () => {
|
window.webContents.on('devtools-opened', () => {
|
||||||
window.webContents.focus();
|
window.webContents.focus();
|
||||||
})
|
});
|
||||||
|
|
||||||
return window;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// quit application when all windows are closed
|
// quit application when all windows are closed
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// on macOS it is common for applications to stay open until the user explicitly quits
|
// On macOS it is common for applications and their menu bar
|
||||||
if (process.platform !== 'darwin')
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
// on macOS it is common to re-create a window even after all windows have been closed
|
// On macOS it's common to re-create a window in the app when the
|
||||||
if (!mainWindow)
|
// dock icon is clicked and there are no other windows open.
|
||||||
mainWindow = createMainWindow();
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function initialize(): Promise<void> {
|
||||||
|
if (isDevelopment) {
|
||||||
|
await installReactDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
|
||||||
// create main BrowserWindow when electron is ready
|
// create main BrowserWindow when electron is ready
|
||||||
app.on('ready', () => {
|
app.on('ready', initialize);
|
||||||
mainWindow = createMainWindow();
|
|
||||||
});
|
|
||||||
|
|
11
src/renderer/custom.d.ts
vendored
11
src/renderer/custom.d.ts
vendored
|
@ -1,9 +1,16 @@
|
||||||
declare module "*.vs" {
|
declare namespace NodeJS {
|
||||||
|
interface Module {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
hot: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.vs' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.fs" {
|
declare module '*.fs' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
64
src/renderer/editor/editor.tsx
Normal file
64
src/renderer/editor/editor.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import 'typeface-roboto';
|
||||||
|
|
||||||
|
import { createMuiTheme, makeStyles, Theme, ThemeProvider } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
|
import Drawer from '@material-ui/core/Drawer';
|
||||||
|
|
||||||
|
import { Library } from './library';
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
width: drawerWidth,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function EditorLayout(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Drawer
|
||||||
|
className={classes.drawer}
|
||||||
|
variant='permanent'
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
anchor='left'
|
||||||
|
></Drawer>
|
||||||
|
<main className={classes.content}>
|
||||||
|
<Library />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
type: 'dark',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function Editor(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<EditorLayout />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
118
src/renderer/editor/library.tsx
Normal file
118
src/renderer/editor/library.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
const { useCallback, useMemo } = React;
|
||||||
|
|
||||||
|
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
import Box from '@material-ui/core/Box';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Container from '@material-ui/core/Container';
|
||||||
|
|
||||||
|
import * as Color from 'color';
|
||||||
|
import * as glob from 'fast-glob';
|
||||||
|
|
||||||
|
import { Tiling, TilingMeta } from './types';
|
||||||
|
import { usePromise, useReadFile, readJSON } from './util';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
grid: {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
margin: theme.spacing(-0.5),
|
||||||
|
},
|
||||||
|
tile: {
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
position: 'relative',
|
||||||
|
width: 130,
|
||||||
|
height: 130,
|
||||||
|
margin: theme.spacing(0.5),
|
||||||
|
// cursor: 'pointer',
|
||||||
|
// '&:hover': {
|
||||||
|
// borderColor: theme.palette.text.secondary,
|
||||||
|
// boxShadow: `0 0 2px 1px ${theme.palette.text.secondary}`,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 1,
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 2,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: '37.5%',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
background: Color(theme.palette.background.default).fade(0.3).string(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function tilingSprite(tiling: Tiling): string {
|
||||||
|
const x = (tiling.meta.width - 1) / 2;
|
||||||
|
const y = (tiling.meta.height - 1) / 2;
|
||||||
|
return `project/tiling/${tiling.id}/${x}_${y}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TilingDisplayProps {
|
||||||
|
tiling: Tiling;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TilingDisplay({ tiling }: TilingDisplayProps): JSX.Element | null {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const path = tilingSprite(tiling);
|
||||||
|
const image = useReadFile(path);
|
||||||
|
const src = useMemo(() => (image ? `data:image/png;base64,${image.toString('base64')}` : undefined), [image]);
|
||||||
|
return (
|
||||||
|
<div className={classes.tile}>
|
||||||
|
<img className={classes.img} src={src} />
|
||||||
|
<div className={classes.label}>{tiling.meta.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listTilings(): Promise<string[]> {
|
||||||
|
const matches = await glob('project/tiling/*/meta.json');
|
||||||
|
return matches.map((m) => m.split('/')[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTilingMeta(id: string): Promise<TilingMeta> {
|
||||||
|
const path = `project/tiling/${id}/meta.json`;
|
||||||
|
const meta = await readJSON(path);
|
||||||
|
return meta as TilingMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTilings(): Promise<Tiling[]> {
|
||||||
|
const tilings = await listTilings();
|
||||||
|
return Promise.all(
|
||||||
|
tilings.map((id) => [id, loadTilingMeta(id)] as const).map(async ([id, p]) => ({ id, meta: await p })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Library(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const mkLoadTilings = useCallback(() => loadTilings(), []);
|
||||||
|
const tilings = usePromise(mkLoadTilings) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box mb={2}>
|
||||||
|
<div className={classes.grid}>
|
||||||
|
{tilings.map((tiling, i) => (
|
||||||
|
<TilingDisplay key={i} tiling={tiling} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<Button variant='contained' color='primary'>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
11
src/renderer/editor/types.ts
Normal file
11
src/renderer/editor/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export interface TilingMeta {
|
||||||
|
name: string;
|
||||||
|
desc?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tiling {
|
||||||
|
id: string;
|
||||||
|
meta: TilingMeta;
|
||||||
|
}
|
36
src/renderer/editor/util.ts
Normal file
36
src/renderer/editor/util.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export function usePromise<T>(f: () => Promise<T>): T | null {
|
||||||
|
const [value, setValue] = useState<T | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(null);
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
(async (): Promise<void> => {
|
||||||
|
const v = await f();
|
||||||
|
if (!cancelled) {
|
||||||
|
setValue(v);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [f]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReadFile(path: string): Buffer | null {
|
||||||
|
const readFile = useCallback(() => fs.promises.readFile(path), [path]);
|
||||||
|
return usePromise(readFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readJSON(path: string): Promise<unknown> {
|
||||||
|
const content = await fs.promises.readFile(path, 'utf8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
18
src/renderer/index.html
Normal file
18
src/renderer/index.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||||
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||||
|
<title>RPGedit</title>
|
||||||
|
<style>
|
||||||
|
html, body, #app {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,26 +0,0 @@
|
||||||
import './runtime/index.css';
|
|
||||||
|
|
||||||
import { GameContext } from './runtime/controller/gamecontext';
|
|
||||||
|
|
||||||
import { Renderer } from './runtime/view/renderer/renderer';
|
|
||||||
|
|
||||||
window.onload = async () => {
|
|
||||||
const app = document.getElementById('app');
|
|
||||||
if (!app)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const renderer = new Renderer(canvas);
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
canvas.width = window.innerWidth;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
renderer.resize();
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
app.append(canvas);
|
|
||||||
|
|
||||||
GameContext.load(renderer);
|
|
||||||
};
|
|
8
src/renderer/index.tsx
Normal file
8
src/renderer/index.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import { Editor } from './editor/editor';
|
||||||
|
|
||||||
|
ReactDOM.render(<Editor />, document.querySelector('#app'));
|
||||||
|
|
||||||
|
module.hot?.accept();
|
|
@ -13,8 +13,7 @@ export function mkCollision(collision: Collision[]): Collidable[] {
|
||||||
for (const c of collision) {
|
for (const c of collision) {
|
||||||
switch (c.type) {
|
switch (c.type) {
|
||||||
case 'polygon':
|
case 'polygon':
|
||||||
if (!c.vertices.length)
|
if (!c.vertices.length) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
let prev = c.vertices[c.vertices.length - 1];
|
let prev = c.vertices[c.vertices.length - 1];
|
||||||
|
|
||||||
|
@ -39,24 +38,20 @@ export function mkCollision(collision: Collision[]): Collidable[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollidableGroup {
|
export interface CollidableGroup {
|
||||||
getTranslation(): vec2|null;
|
getTranslation(): vec2 | null;
|
||||||
getCollidables(): Collidable[];
|
getCollidables(): Collidable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
|
export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
|
||||||
const t = collision.getTranslation();
|
const t = collision.getTranslation();
|
||||||
if (t)
|
if (t) move = move.translate(vec2.negate(vec2.create(), t));
|
||||||
move = move.translate(vec2.negate(vec2.create(), t));
|
|
||||||
|
|
||||||
for (const c of collision.getCollidables()) {
|
for (const c of collision.getCollidables()) {
|
||||||
if (!c.collide(out, move, radius))
|
if (!c.collide(out, move, radius)) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
|
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (t)
|
if (t) vec2.add(out, out, t);
|
||||||
vec2.add(out, out, t);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,7 @@ import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
export class EntityContext implements CollidableGroup {
|
export class EntityContext implements CollidableGroup {
|
||||||
public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
|
public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
|
||||||
return new EntityContext(
|
return new EntityContext(renderer, name, await EntityView.load(renderer, name));
|
||||||
renderer,
|
|
||||||
name,
|
|
||||||
await EntityView.load(renderer, name),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly pos: vec2 = vec2.create();
|
public readonly pos: vec2 = vec2.create();
|
||||||
|
@ -28,7 +24,7 @@ export class EntityContext implements CollidableGroup {
|
||||||
this.collision = mkCollision(view.data.collision);
|
this.collision = mkCollision(view.data.collision);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(time: number) {
|
public render(time: number): void {
|
||||||
this.renderer.setTranslation(this.pos);
|
this.renderer.setTranslation(this.pos);
|
||||||
this.view.renderByTime(time);
|
this.view.renderByTime(time);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +37,7 @@ export class EntityContext implements CollidableGroup {
|
||||||
return this.collision;
|
return this.collision;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interact() {
|
public interact(): void {
|
||||||
alert(`You've interacted with ${this.name}!`);
|
alert(`You've interacted with ${this.name}!`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CollidableGroup, collide, mkCollision } from './collision';
|
import { CollidableGroup, collide, mkCollision } from './collision';
|
||||||
import { EntityContext } from './entitycontext';
|
import { EntityContext } from './entitycontext';
|
||||||
|
|
||||||
import { MapData } from '../model/data/map';
|
import { MapData, MapDataInput } from '../model/data/map';
|
||||||
|
|
||||||
import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
|
import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
|
||||||
import { MapView } from '../view/map';
|
import { MapView } from '../view/map';
|
||||||
|
@ -26,17 +26,11 @@ export class GameContext implements CollidableGroup {
|
||||||
vec2.set(player.pos, 7, 6);
|
vec2.set(player.pos, 7, 6);
|
||||||
vec2.set(entity.pos, 4, 3);
|
vec2.set(entity.pos, 4, 3);
|
||||||
|
|
||||||
return new GameContext(
|
return new GameContext(renderer, mapView, player, [entity], mapCollision);
|
||||||
renderer,
|
|
||||||
mapView,
|
|
||||||
player,
|
|
||||||
[entity],
|
|
||||||
mapCollision,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> {
|
private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> {
|
||||||
const map = new MapData(await getJSON(`resources/map/${name}.json`));
|
const map = new MapData((await getJSON(`resources/map/${name}.json`)) as MapDataInput);
|
||||||
return [await MapView.load(renderer, map), mkCollision(map.collision)];
|
return [await MapView.load(renderer, map), mkCollision(map.collision)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +44,7 @@ export class GameContext implements CollidableGroup {
|
||||||
private readonly input: GameInputHandler;
|
private readonly input: GameInputHandler;
|
||||||
|
|
||||||
private readonly playerDir: vec2 = vec2.fromValues(0, 1);
|
private readonly playerDir: vec2 = vec2.fromValues(0, 1);
|
||||||
private speed: number = 0;
|
private speed = 0;
|
||||||
|
|
||||||
private readonly collisionRadius = 15 / 32;
|
private readonly collisionRadius = 15 / 32;
|
||||||
private readonly interactLength = 1 / 32;
|
private readonly interactLength = 1 / 32;
|
||||||
|
@ -66,8 +60,7 @@ export class GameContext implements CollidableGroup {
|
||||||
this.input.addListener((input) => {
|
this.input.addListener((input) => {
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case 'button':
|
case 'button':
|
||||||
if (input.button === ButtonCode.Action)
|
if (input.button === ButtonCode.Action) this.interact();
|
||||||
this.interact();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'direction':
|
case 'direction':
|
||||||
|
@ -108,8 +101,7 @@ export class GameContext implements CollidableGroup {
|
||||||
|
|
||||||
private interact(): void {
|
private interact(): void {
|
||||||
for (const e of this.entities) {
|
for (const e of this.entities) {
|
||||||
if (!this.canInteract(e))
|
if (!this.canInteract(e)) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
e.interact();
|
e.interact();
|
||||||
break;
|
break;
|
||||||
|
@ -136,8 +128,7 @@ export class GameContext implements CollidableGroup {
|
||||||
const newDest = vec2.create();
|
const newDest = vec2.create();
|
||||||
|
|
||||||
while (this.updateStepCollide(newDest, dest)) {
|
while (this.updateStepCollide(newDest, dest)) {
|
||||||
if (vec2.equals(newDest, this.player.pos))
|
if (vec2.equals(newDest, this.player.pos)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
vec2.copy(dest, newDest);
|
vec2.copy(dest, newDest);
|
||||||
}
|
}
|
||||||
|
@ -153,21 +144,19 @@ export class GameContext implements CollidableGroup {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < diff; i++)
|
for (let i = 0; i < diff; i++) this.updateStep();
|
||||||
this.updateStep();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private render(): void {
|
private render(): void {
|
||||||
this.renderer.setCenter(this.player.pos);
|
this.renderer.setCenter(this.player.pos);
|
||||||
this.renderer.clear();
|
this.renderer.clear();
|
||||||
|
|
||||||
for (const r of [this.mapView, ...this.entities, this.player])
|
for (const r of [this.mapView, ...this.entities, this.player]) r.render(this.time);
|
||||||
r.render(this.time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderLoop(): Promise<void> {
|
private async renderLoop(): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
this.update(await nextAnimationFrame() - this.initTime);
|
this.update((await nextAnimationFrame()) - this.initTime);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { mat2, vec2 } from 'gl-matrix';
|
import { mat2, vec2 } from 'gl-matrix';
|
||||||
import { Collidable } from './collision';
|
import { Collidable } from './collision';
|
||||||
|
|
||||||
const rot90 = mat2.fromValues(
|
const rot90 = mat2.fromValues(0, 1, -1, 0);
|
||||||
0, 1,
|
|
||||||
-1, 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
export function normal(out: vec2, a: vec2): vec2 {
|
export function normal(out: vec2, a: vec2): vec2 {
|
||||||
return vec2.transformMat2(out, a, rot90);
|
return vec2.transformMat2(out, a, rot90);
|
||||||
|
@ -15,10 +12,7 @@ export function crossz(a: vec2, b: vec2): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Line {
|
export class Line {
|
||||||
constructor(
|
constructor(public readonly p: vec2, public readonly v: vec2) {}
|
||||||
public readonly p: vec2,
|
|
||||||
public readonly v: vec2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public getNormal(out: vec2): vec2 {
|
public getNormal(out: vec2): vec2 {
|
||||||
return normal(out, this.v);
|
return normal(out, this.v);
|
||||||
|
@ -50,10 +44,7 @@ export class Line {
|
||||||
export class Movement {
|
export class Movement {
|
||||||
public readonly v: vec2;
|
public readonly v: vec2;
|
||||||
|
|
||||||
constructor(
|
constructor(public readonly src: vec2, public readonly dest: vec2) {
|
||||||
public readonly src: vec2,
|
|
||||||
public readonly dest: vec2,
|
|
||||||
) {
|
|
||||||
this.v = vec2.sub(vec2.create(), dest, src);
|
this.v = vec2.sub(vec2.create(), dest, src);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +62,7 @@ export class Movement {
|
||||||
}
|
}
|
||||||
|
|
||||||
public toLineSegment(): LineSegment {
|
public toLineSegment(): LineSegment {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
return LineSegment.fromPoints(this.src, this.dest);
|
return LineSegment.fromPoints(this.src, this.dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,11 +82,7 @@ export class LineSegment extends Line implements Collidable {
|
||||||
return new LineSegment(p1, v, d);
|
return new LineSegment(p1, v, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(p: vec2, v: vec2, public readonly l: number) {
|
||||||
p: vec2,
|
|
||||||
v: vec2,
|
|
||||||
public readonly l: number,
|
|
||||||
) {
|
|
||||||
super(p, v);
|
super(p, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,35 +92,30 @@ export class LineSegment extends Line implements Collidable {
|
||||||
|
|
||||||
public containsPoint(p2: vec2): boolean {
|
public containsPoint(p2: vec2): boolean {
|
||||||
const d = this.projectPointDistance(p2);
|
const d = this.projectPointDistance(p2);
|
||||||
return (d >= 0 && d <= this.l);
|
return d >= 0 && d <= this.l;
|
||||||
}
|
}
|
||||||
|
|
||||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
public collide(out: vec2, move: Movement, r: number): boolean {
|
||||||
if (this.distancePoint(move.src) < 0)
|
if (this.distancePoint(move.src) < 0) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
if (crossz(move.v, this.v) < 0)
|
if (crossz(move.v, this.v) < 0) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
const t = this.getNormal(vec2.create());
|
const t = this.getNormal(vec2.create());
|
||||||
vec2.scale(t, t, -r);
|
vec2.scale(t, t, -r);
|
||||||
|
|
||||||
const refMove = move.translate(t);
|
const refMove = move.translate(t);
|
||||||
|
|
||||||
if (!this.collideRef(out, refMove))
|
if (!this.collideRef(out, refMove)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
vec2.sub(out, out, t);
|
vec2.sub(out, out, t);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private collideRef(out: vec2, move: Movement): boolean {
|
private collideRef(out: vec2, move: Movement): boolean {
|
||||||
if (this.distancePoint(move.dest) >= 0)
|
if (this.distancePoint(move.dest) >= 0) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
const x = move.intersectLine(vec2.create(), this);
|
const x = move.intersectLine(vec2.create(), this);
|
||||||
if (!this.containsPoint(x))
|
if (!this.containsPoint(x)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
this.projectPoint(out, move.dest);
|
this.projectPoint(out, move.dest);
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,10 @@ export class Point implements Collidable {
|
||||||
public collide(out: vec2, move: Movement, r: number): boolean {
|
public collide(out: vec2, move: Movement, r: number): boolean {
|
||||||
const moveLine = move.toLineSegment();
|
const moveLine = move.toLineSegment();
|
||||||
|
|
||||||
if (moveLine.projectPointDistance(this.p) < 0)
|
if (moveLine.projectPointDistance(this.p) < 0) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
const d = moveLine.distancePoint(this.p) / r;
|
const d = moveLine.distancePoint(this.p) / r;
|
||||||
if (Math.abs(d) >= 1)
|
if (Math.abs(d) >= 1) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
const e = Math.sqrt(1 - d * d);
|
const e = Math.sqrt(1 - d * d);
|
||||||
|
|
||||||
|
@ -26,8 +24,7 @@ export class Point implements Collidable {
|
||||||
|
|
||||||
const refMove = move.translate(tr);
|
const refMove = move.translate(tr);
|
||||||
|
|
||||||
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p))
|
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
normal(t, t);
|
normal(t, t);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
|
export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
|
||||||
const ret = new Map();
|
const ret = new Map();
|
||||||
|
|
||||||
for (const k of Object.keys(r))
|
for (const k of Object.keys(r)) ret.set(k, r[k]);
|
||||||
ret.set(k, r[k]);
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -10,8 +9,7 @@ export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
|
||||||
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
|
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
|
||||||
const ret: Map<K, V2> = new Map();
|
const ret: Map<K, V2> = new Map();
|
||||||
|
|
||||||
for (const [k, v] of map)
|
for (const [k, v] of map) ret.set(k, f(v));
|
||||||
ret.set(k, f(v));
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -19,8 +17,7 @@ export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K,
|
||||||
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
|
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
|
||||||
const ret: Map<K, V2> = new Map();
|
const ret: Map<K, V2> = new Map();
|
||||||
|
|
||||||
for (const [k, v] of mapValues(f, map))
|
for (const [k, v] of mapValues(f, map)) ret.set(k, await v);
|
||||||
ret.set(k, await v);
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -28,12 +25,12 @@ export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map:
|
||||||
export function nextPowerOf2(n: number): number {
|
export function nextPowerOf2(n: number): number {
|
||||||
let i = 1;
|
let i = 1;
|
||||||
|
|
||||||
while (i < n)
|
while (i < n) i *= 2;
|
||||||
i *= 2;
|
|
||||||
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export class Listenable<T extends any[]> {
|
export class Listenable<T extends any[]> {
|
||||||
private readonly listeners: Array<(...args: T) => void> = [];
|
private readonly listeners: Array<(...args: T) => void> = [];
|
||||||
|
|
||||||
|
@ -46,10 +43,9 @@ export class Listenable<T extends any[]> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getJSON(url: string): Promise<any> {
|
export async function getJSON(url: string): Promise<unknown> {
|
||||||
const res = await window.fetch(url);
|
const res = await window.fetch(url);
|
||||||
if (res.status < 200 || res.status >= 300)
|
if (res.status < 200 || res.status >= 300) throw new Error(res.statusText);
|
||||||
throw new Error(res.statusText);
|
|
||||||
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EntityData } from '../model/data/entity';
|
import { EntityData, EntityDataInput } from '../model/data/entity';
|
||||||
import { Renderer } from './renderer/renderer';
|
import { Renderer } from './renderer/renderer';
|
||||||
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
||||||
import { loadImage, mkTexture } from './util/image';
|
import { loadImage, mkTexture } from './util/image';
|
||||||
|
@ -9,11 +9,11 @@ import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
export class EntityView {
|
export class EntityView {
|
||||||
public static async load(r: Renderer, name: string): Promise<EntityView> {
|
public static async load(r: Renderer, name: string): Promise<EntityView> {
|
||||||
const data = new EntityData(await getJSON(`resources/entity/${name}.json`));
|
const data = new EntityData((await getJSON(`resources/entity/${name}.json`)) as EntityDataInput);
|
||||||
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
|
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
|
||||||
|
|
||||||
const [texture, size] = mkTexture(r, tile);
|
const [texture, size] = mkTexture(r, tile);
|
||||||
const frameSize = [size[0], size[1] / data.frames];
|
const frameSize: [number, number] = [size[0], size[1] / data.frames];
|
||||||
|
|
||||||
const offset = vec2.mul(vec2.create(), frameSize, data.anchor);
|
const offset = vec2.mul(vec2.create(), frameSize, data.anchor);
|
||||||
r.snapToGrid(offset, offset);
|
r.snapToGrid(offset, offset);
|
||||||
|
@ -33,22 +33,14 @@ export class EntityView {
|
||||||
sprites.push(builder.build());
|
sprites.push(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EntityView(
|
return new EntityView(data, sprites);
|
||||||
data,
|
|
||||||
sprites,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly totalTime: number;
|
private readonly totalTime: number;
|
||||||
|
|
||||||
private constructor(
|
private constructor(public readonly data: EntityData, public readonly sprites: SpriteView[]) {
|
||||||
public readonly data: EntityData,
|
if (data.animation) this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
|
||||||
public readonly sprites: SpriteView[],
|
else this.totalTime = 0;
|
||||||
) {
|
|
||||||
if (data.animation)
|
|
||||||
this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
|
|
||||||
else
|
|
||||||
this.totalTime = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSpriteByTime(time: number): SpriteView {
|
public getSpriteByTime(time: number): SpriteView {
|
||||||
|
@ -57,16 +49,14 @@ export class EntityView {
|
||||||
if (this.data.animation) {
|
if (this.data.animation) {
|
||||||
for (const [len, sprite] of this.data.animation.sequence) {
|
for (const [len, sprite] of this.data.animation.sequence) {
|
||||||
time -= len;
|
time -= len;
|
||||||
if (time < 0)
|
if (time < 0) return this.sprites[sprite];
|
||||||
return this.sprites[sprite];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sprites[0];
|
return this.sprites[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderByTime(time: number) {
|
public renderByTime(time: number): void {
|
||||||
this.getSpriteByTime(time).render();
|
this.getSpriteByTime(time).render();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,13 +35,8 @@ export class GameInputHandler extends Listenable<[GameInput]> {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.input = new InputHandler(
|
this.input = new InputHandler(
|
||||||
new Set([
|
new Set(['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', ...Object.keys(buttonMapping)]),
|
||||||
'ArrowLeft',
|
);
|
||||||
'ArrowUp',
|
|
||||||
'ArrowRight',
|
|
||||||
'ArrowDown',
|
|
||||||
...Object.keys(buttonMapping),
|
|
||||||
]));
|
|
||||||
|
|
||||||
this.input.addListener((key: string, pressed: boolean) => {
|
this.input.addListener((key: string, pressed: boolean) => {
|
||||||
const button = buttonMapping[key];
|
const button = buttonMapping[key];
|
||||||
|
@ -57,17 +52,12 @@ export class GameInputHandler extends Listenable<[GameInput]> {
|
||||||
|
|
||||||
const dir = vec2.create();
|
const dir = vec2.create();
|
||||||
|
|
||||||
if (this.input.has('ArrowLeft'))
|
if (this.input.has('ArrowLeft')) vec2.add(dir, dir, [-1, 0]);
|
||||||
vec2.add(dir, dir, [-1, 0]);
|
if (this.input.has('ArrowUp')) vec2.add(dir, dir, [0, -1]);
|
||||||
if (this.input.has('ArrowUp'))
|
if (this.input.has('ArrowRight')) vec2.add(dir, dir, [1, 0]);
|
||||||
vec2.add(dir, dir, [0, -1]);
|
if (this.input.has('ArrowDown')) vec2.add(dir, dir, [0, 1]);
|
||||||
if (this.input.has('ArrowRight'))
|
|
||||||
vec2.add(dir, dir, [1, 0]);
|
|
||||||
if (this.input.has('ArrowDown'))
|
|
||||||
vec2.add(dir, dir, [0, 1]);
|
|
||||||
|
|
||||||
if (vec2.sqrLen(dir) > 0)
|
if (vec2.sqrLen(dir) > 0) vec2.normalize(dir, dir);
|
||||||
vec2.normalize(dir, dir);
|
|
||||||
|
|
||||||
this.runListeners({
|
this.runListeners({
|
||||||
type: 'direction',
|
type: 'direction',
|
||||||
|
|
|
@ -7,26 +7,22 @@ export class InputHandler extends Listenable<[string, boolean]> {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
window.addEventListener('keydown', (ev) => {
|
window.addEventListener('keydown', (ev) => {
|
||||||
if (!relevantKeys.has(ev.code))
|
if (!relevantKeys.has(ev.code)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (ev.repeat)
|
if (ev.repeat) return;
|
||||||
return;
|
|
||||||
|
|
||||||
this.keys.add(ev.code);
|
this.keys.add(ev.code);
|
||||||
this.runListeners(ev.code, true);
|
this.runListeners(ev.code, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (ev) => {
|
window.addEventListener('keyup', (ev) => {
|
||||||
if (!relevantKeys.has(ev.code))
|
if (!relevantKeys.has(ev.code)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (!this.keys.has(ev.code))
|
if (!this.keys.has(ev.code)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
this.keys.delete(ev.code);
|
this.keys.delete(ev.code);
|
||||||
this.runListeners(ev.code, false);
|
this.runListeners(ev.code, false);
|
||||||
|
|
|
@ -55,10 +55,7 @@ function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
|
||||||
return Promise.all(tiles.map((tile) => loadTile(r, tile)));
|
return Promise.all(tiles.map((tile) => loadTile(r, tile)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mkTileset(
|
function mkTileset(r: Renderer, mapTiles: MapTile[]): Tileset {
|
||||||
r: Renderer,
|
|
||||||
mapTiles: MapTile[],
|
|
||||||
): Tileset {
|
|
||||||
const tileSize = 32;
|
const tileSize = 32;
|
||||||
|
|
||||||
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
|
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
|
||||||
|
@ -67,7 +64,8 @@ function mkTileset(
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = canvas.height = canvasSize;
|
canvas.width = canvas.height = canvasSize;
|
||||||
|
|
||||||
let x = 0, y = 0;
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
const tiles: TilesetTile[] = [];
|
const tiles: TilesetTile[] = [];
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
@ -77,7 +75,12 @@ function mkTileset(
|
||||||
ctx.drawImage(tile.image, x * tileSize, y * tileSize);
|
ctx.drawImage(tile.image, x * tileSize, y * tileSize);
|
||||||
tiles.push({
|
tiles.push({
|
||||||
type: 'static',
|
type: 'static',
|
||||||
coords: [x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim],
|
coords: [
|
||||||
|
x / canvasDim,
|
||||||
|
y / canvasDim,
|
||||||
|
(x + 1) / canvasDim,
|
||||||
|
(y + 1) / canvasDim,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
x++;
|
x++;
|
||||||
|
@ -107,9 +110,9 @@ function addSprite(
|
||||||
tileset: Tileset,
|
tileset: Tileset,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
tile: number) {
|
tile: number,
|
||||||
if (!tile)
|
): void {
|
||||||
return;
|
if (!tile) return;
|
||||||
|
|
||||||
const tilesetTile = tileset.tiles[tile - 1];
|
const tilesetTile = tileset.tiles[tile - 1];
|
||||||
|
|
||||||
|
@ -124,24 +127,12 @@ function addSprite(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
|
|
||||||
const builder = new SpriteViewBuilder(r, tileset.texture);
|
|
||||||
const entityTiles: Array<[[number, number], EntityView]> = [];
|
|
||||||
|
|
||||||
for (let x = 0; x < layer[0].length; x++)
|
|
||||||
for (let y = 0; y < layer.length; y++)
|
|
||||||
addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
|
|
||||||
|
|
||||||
return new MapLayerView(r, builder.build(), entityTiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MapLayerView {
|
class MapLayerView {
|
||||||
public constructor(
|
public constructor(
|
||||||
private r: Renderer,
|
private r: Renderer,
|
||||||
private staticTiles: SpriteView,
|
private staticTiles: SpriteView,
|
||||||
private entityTiles: Array<[[number, number], EntityView]>,
|
private entityTiles: Array<[[number, number], EntityView]>,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
public render(time: number): void {
|
public render(time: number): void {
|
||||||
this.r.setTranslation([0, 0]);
|
this.r.setTranslation([0, 0]);
|
||||||
|
@ -154,6 +145,16 @@ class MapLayerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
|
||||||
|
const builder = new SpriteViewBuilder(r, tileset.texture);
|
||||||
|
const entityTiles: Array<[[number, number], EntityView]> = [];
|
||||||
|
|
||||||
|
for (let x = 0; x < layer[0].length; x++)
|
||||||
|
for (let y = 0; y < layer.length; y++) addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
|
||||||
|
|
||||||
|
return new MapLayerView(r, builder.build(), entityTiles);
|
||||||
|
}
|
||||||
|
|
||||||
export class MapView {
|
export class MapView {
|
||||||
public static async load(r: Renderer, map: MapData): Promise<MapView> {
|
public static async load(r: Renderer, map: MapData): Promise<MapView> {
|
||||||
const tiles = await loadTiles(r, map.tiles);
|
const tiles = await loadTiles(r, map.tiles);
|
||||||
|
@ -163,11 +164,9 @@ export class MapView {
|
||||||
return new MapView(layers);
|
return new MapView(layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(private layers: MapLayerView[]) {
|
private constructor(private layers: MapLayerView[]) {}
|
||||||
}
|
|
||||||
|
|
||||||
public render(time: number): void {
|
public render(time: number): void {
|
||||||
for (const layer of this.layers)
|
for (const layer of this.layers) layer.render(time);
|
||||||
layer.render(time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,7 @@ export class Renderer {
|
||||||
|
|
||||||
public createBuffer(): WebGLBuffer {
|
public createBuffer(): WebGLBuffer {
|
||||||
const ret = this.gl.createBuffer();
|
const ret = this.gl.createBuffer();
|
||||||
if (!ret)
|
if (!ret) throw new Error('unable to create buffer');
|
||||||
throw new Error('unable to create buffer');
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -50,11 +49,11 @@ export class Renderer {
|
||||||
return this.shaders.samplerLoc;
|
return this.shaders.samplerLoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCenter(v: vec2|number[]) {
|
public setCenter(v: vec2): void {
|
||||||
this.snapToGrid(this.center, v);
|
this.snapToGrid(this.center, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTranslation(v: vec2|number[]) {
|
public setTranslation(v: vec2): void {
|
||||||
vec2.sub(this.translation, v, this.center);
|
vec2.sub(this.translation, v, this.center);
|
||||||
this.snapToGrid(this.translation, this.translation);
|
this.snapToGrid(this.translation, this.translation);
|
||||||
this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
|
this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
|
||||||
|
@ -66,7 +65,7 @@ export class Renderer {
|
||||||
this.setTranslation([0, 0]);
|
this.setTranslation([0, 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public snapToGrid(out: vec2, a: vec2|number[]): void {
|
public snapToGrid(out: vec2, a: vec2): void {
|
||||||
vec2.scale(out, a, this.coordScale);
|
vec2.scale(out, a, this.coordScale);
|
||||||
vec2.round(out, out);
|
vec2.round(out, out);
|
||||||
vec2.scale(out, out, 1 / this.coordScale);
|
vec2.scale(out, out, 1 / this.coordScale);
|
||||||
|
@ -84,14 +83,13 @@ export class Renderer {
|
||||||
const scale = this.viewScale * this.coordScale;
|
const scale = this.viewScale * this.coordScale;
|
||||||
|
|
||||||
mat4.identity(this.viewport);
|
mat4.identity(this.viewport);
|
||||||
mat4.scale(this.viewport, this.viewport, [2 * scale / ws, -2 * scale / hs, 1.0]);
|
mat4.scale(this.viewport, this.viewport, [(2 * scale) / ws, (-2 * scale) / hs, 1.0]);
|
||||||
this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
|
this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mkContext(): WebGLRenderingContext {
|
private mkContext(): WebGLRenderingContext {
|
||||||
const gl = this.canvas.getContext('webgl');
|
const gl = this.canvas.getContext('webgl');
|
||||||
if (!gl)
|
if (!gl) throw new Error('unable to initialize WebGL context');
|
||||||
throw new Error('unable to initialize WebGL context');
|
|
||||||
|
|
||||||
return gl;
|
return gl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,7 @@ export class Shaders {
|
||||||
|
|
||||||
constructor(private readonly gl: WebGLRenderingContext) {
|
constructor(private readonly gl: WebGLRenderingContext) {
|
||||||
const shaderProgram = this.gl.createProgram();
|
const shaderProgram = this.gl.createProgram();
|
||||||
if (!shaderProgram)
|
if (!shaderProgram) throw new Error('Unable to create shader program');
|
||||||
throw new Error('Unable to create shader program');
|
|
||||||
|
|
||||||
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
|
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
|
||||||
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
|
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
|
||||||
|
@ -46,8 +45,7 @@ export class Shaders {
|
||||||
|
|
||||||
private compileShader(type: number, src: string): WebGLShader {
|
private compileShader(type: number, src: string): WebGLShader {
|
||||||
const shader = this.gl.createShader(type);
|
const shader = this.gl.createShader(type);
|
||||||
if (!shader)
|
if (!shader) throw new Error('Unable to create shader');
|
||||||
throw new Error('Unable to create shader');
|
|
||||||
|
|
||||||
this.gl.shaderSource(shader, src);
|
this.gl.shaderSource(shader, src);
|
||||||
this.gl.compileShader(shader);
|
this.gl.compileShader(shader);
|
||||||
|
@ -63,16 +61,14 @@ export class Shaders {
|
||||||
|
|
||||||
private getAttribLocation(program: WebGLProgram, name: string): number {
|
private getAttribLocation(program: WebGLProgram, name: string): number {
|
||||||
const ret = this.gl.getAttribLocation(program, name);
|
const ret = this.gl.getAttribLocation(program, name);
|
||||||
if (ret < 0)
|
if (ret < 0) throw new Error(`unable to get location of attribute '${name}'`);
|
||||||
throw new Error("unable to get location of attribute '" + name + "'");
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
|
private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
|
||||||
const ret = this.gl.getUniformLocation(program, name);
|
const ret = this.gl.getUniformLocation(program, name);
|
||||||
if (!ret)
|
if (!ret) throw new Error(`unable to get location of uniform '${name}'`);
|
||||||
throw new Error("unable to get location of uniform '" + name + "'");
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,34 +2,6 @@ import { Renderer } from './renderer/renderer';
|
||||||
|
|
||||||
export type SpriteCoords = [number, number, number, number];
|
export type SpriteCoords = [number, number, number, number];
|
||||||
|
|
||||||
export class SpriteViewBuilder {
|
|
||||||
private static pushSprite(buf: number[], coords: SpriteCoords): void {
|
|
||||||
const [x1, y1, x2, y2] = coords;
|
|
||||||
|
|
||||||
buf.push(x1); buf.push(y1);
|
|
||||||
buf.push(x2); buf.push(y1);
|
|
||||||
buf.push(x1); buf.push(y2);
|
|
||||||
|
|
||||||
buf.push(x1); buf.push(y2);
|
|
||||||
buf.push(x2); buf.push(y1);
|
|
||||||
buf.push(x2); buf.push(y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly vertexData: number[] = [];
|
|
||||||
private readonly textureData: number[] = [];
|
|
||||||
|
|
||||||
constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
|
|
||||||
|
|
||||||
public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
|
|
||||||
SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
|
|
||||||
SpriteViewBuilder.pushSprite(this.textureData, texCoords);
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): SpriteView {
|
|
||||||
return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SpriteView {
|
export class SpriteView {
|
||||||
private readonly vertexCount: number;
|
private readonly vertexCount: number;
|
||||||
private readonly vertexBuffer: WebGLBuffer;
|
private readonly vertexBuffer: WebGLBuffer;
|
||||||
|
@ -70,3 +42,37 @@ export class SpriteView {
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
|
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SpriteViewBuilder {
|
||||||
|
private static pushSprite(buf: number[], coords: SpriteCoords): void {
|
||||||
|
const [x1, y1, x2, y2] = coords;
|
||||||
|
|
||||||
|
buf.push(x1);
|
||||||
|
buf.push(y1);
|
||||||
|
buf.push(x2);
|
||||||
|
buf.push(y1);
|
||||||
|
buf.push(x1);
|
||||||
|
buf.push(y2);
|
||||||
|
|
||||||
|
buf.push(x1);
|
||||||
|
buf.push(y2);
|
||||||
|
buf.push(x2);
|
||||||
|
buf.push(y1);
|
||||||
|
buf.push(x2);
|
||||||
|
buf.push(y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly vertexData: number[] = [];
|
||||||
|
private readonly textureData: number[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
|
||||||
|
|
||||||
|
public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
|
||||||
|
SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
|
||||||
|
SpriteViewBuilder.pushSprite(this.textureData, texCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): SpriteView {
|
||||||
|
return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,20 +3,20 @@ import { Renderer } from '../renderer/renderer';
|
||||||
export function loadImage(url: string): Promise<HTMLImageElement> {
|
export function loadImage(url: string): Promise<HTMLImageElement> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.addEventListener('load', () => { resolve(img); });
|
img.addEventListener('load', () => {
|
||||||
img.addEventListener('error', () => { reject(new Error('failed to load ' + url)); });
|
resolve(img);
|
||||||
|
});
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
reject(new Error('failed to load ' + url));
|
||||||
|
});
|
||||||
img.src = url;
|
img.src = url;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkTexture(
|
export function mkTexture(r: Renderer, src: HTMLCanvasElement | HTMLImageElement): [WebGLTexture, [number, number]] {
|
||||||
r: Renderer,
|
|
||||||
src: HTMLCanvasElement|HTMLImageElement,
|
|
||||||
): [WebGLTexture, [number, number]] {
|
|
||||||
const gl = r.getContext();
|
const gl = r.getContext();
|
||||||
const texture = gl.createTexture();
|
const texture = gl.createTexture();
|
||||||
if (!texture)
|
if (!texture) throw new Error('unable to create texture');
|
||||||
throw new Error('unable to create texture');
|
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
||||||
|
@ -25,9 +25,7 @@ export function mkTexture(
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
const size: [number, number] = [
|
const size: [number, number] = [src.width / r.coordScale, src.height / r.coordScale];
|
||||||
src.width / r.coordScale, src.height / r.coordScale,
|
|
||||||
];
|
|
||||||
|
|
||||||
return [texture, size];
|
return [texture, size];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"sourceMap": true,
|
"sourceMap": true
|
||||||
"declaration": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{ test: /\.(vs|fs)$/, use: ['raw-loader'] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
Reference in a new issue