Boilerplating, pan, zoom, draw grid
This commit is contained in:
parent
d8a9a10811
commit
e5a1f361ed
|
|
@ -173,3 +173,5 @@ dist
|
|||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
public/build
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
{
|
||||
"name": "binario",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun --hot src/server.ts"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Binario</title>
|
||||
<style>
|
||||
html, body, #c {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script src="build/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import Graphics from "./graphics";
|
||||
import { prevent } from "./utils";
|
||||
|
||||
export default class Game {
|
||||
private running = false;
|
||||
private mouseDown = false;
|
||||
private graphics;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
canvas.focus();
|
||||
|
||||
canvas.addEventListener('wheel', this.onScroll);
|
||||
canvas.addEventListener('mousedown', this.onMouseDown);
|
||||
canvas.addEventListener('mousemove', this.onMouseMove);
|
||||
canvas.addEventListener('mouseup', this.onMouseUp);
|
||||
document.addEventListener('contextmenu', prevent);
|
||||
|
||||
this.graphics = new Graphics(canvas);
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
||||
}
|
||||
|
||||
private onResize = () => {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
private onScroll = (event: WheelEvent) => {
|
||||
const direction = event.deltaY / Math.abs(event.deltaY);
|
||||
|
||||
if (direction < 0) {
|
||||
this.graphics.scale /= 1.1;
|
||||
} else if (direction > 0) {
|
||||
this.graphics.scale *= 1.1;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private onMouseDown = (event: MouseEvent) => {
|
||||
this.mouseDown = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
private onMouseUp = (event: MouseEvent) => {
|
||||
this.mouseDown = false;
|
||||
event.preventDefault();
|
||||
}
|
||||
private onMouseMove = (event: MouseEvent) => {
|
||||
if (this.mouseDown) {
|
||||
this.graphics.pan([event.movementX, event.movementY]);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
run() {
|
||||
this.running = true;
|
||||
this.loop();
|
||||
}
|
||||
|
||||
private loop = () => {
|
||||
this.graphics.clear();
|
||||
this.graphics.drawGrid();
|
||||
|
||||
this.graphics.debug();
|
||||
|
||||
if (this.running) {
|
||||
requestAnimationFrame(this.loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { exp } from "./utils";
|
||||
|
||||
export default class Graphics {
|
||||
private context: CanvasRenderingContext2D;
|
||||
private tileSize = 32;
|
||||
private offset: Point = [0, 0];
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
this.context = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.canvas.width;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.canvas.height;
|
||||
}
|
||||
|
||||
get size(): Point {
|
||||
return [this.canvas.width, this.canvas.height];
|
||||
}
|
||||
|
||||
get scale() {
|
||||
return 1.0 / this.tileSize;
|
||||
}
|
||||
|
||||
set scale(value) {
|
||||
this.tileSize = Math.min(Math.max(1.0 / value, 2), this.width / 2, this.height / 2);
|
||||
}
|
||||
|
||||
pan(amount: Point) {
|
||||
this.offset = exp`${this.offset} + ${amount} / ${this.tileSize}`;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.context.clearRect(0, 0, this.width, this.height);
|
||||
}
|
||||
|
||||
debug() {
|
||||
const p00 = this.worldToScreen([0, 0]);
|
||||
const p11 = exp`${this.worldToScreen([1, 1])} - ${p00}`;
|
||||
|
||||
this.context.fillStyle = 'red';
|
||||
this.context.fillRect(...p00, ...p11);
|
||||
}
|
||||
|
||||
drawGrid() {
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = 'gray';
|
||||
let [x0, y0, x1, y1] = this.visibleWorld;
|
||||
[x0, y0] = this.worldToScreen([Math.floor(x0), Math.floor(y0)]);
|
||||
[x1, y1] = this.worldToScreen([Math.ceil(x1), Math.ceil(y1)]);
|
||||
|
||||
for (let x = x0; x < x1; x += this.tileSize) {
|
||||
this.context.moveTo(x, 0);
|
||||
this.context.lineTo(x, this.height);
|
||||
}
|
||||
for (let y = y0; y < y1; y += this.tileSize) {
|
||||
this.context.moveTo(0, y);
|
||||
this.context.lineTo(this.width, y);
|
||||
}
|
||||
this.context.stroke();
|
||||
}
|
||||
|
||||
get visibleWorld(): Rect {
|
||||
const topLeft = this.screenToWorld([0, 0]);
|
||||
const bottomRight = this.screenToWorld([this.width, this.height]);
|
||||
|
||||
return [...topLeft, ...bottomRight];
|
||||
}
|
||||
|
||||
worldToScreen(point: Point): Point {
|
||||
return exp`(${point} + ${this.offset}) * ${this.tileSize} + ${this.size} / ${2}`;
|
||||
}
|
||||
|
||||
screenToWorld(point: Point): Point {
|
||||
return exp`(${point} - ${this.size} / ${2}) / ${this.tileSize} - ${this.offset}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import Game from './game';
|
||||
|
||||
async function main() {
|
||||
const canvas = document.getElementById('c');
|
||||
if (canvas && 'getContext' in canvas && typeof canvas.getContext === 'function') {
|
||||
const game = new Game(canvas as HTMLCanvasElement);
|
||||
await game.load();
|
||||
game.run();
|
||||
} else {
|
||||
alert('Something wrong with your canvas!');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Bun from 'bun';
|
||||
import path from 'path';
|
||||
|
||||
Bun.serve({
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const pathname = path.basename(url.pathname);
|
||||
switch (pathname) {
|
||||
case '':
|
||||
case '/':
|
||||
case 'index.html':
|
||||
return new Response(Bun.file(path.resolve(import.meta.dir, '..', 'public', 'index.html')));
|
||||
case 'index.js':
|
||||
const bundle = await Bun.build({
|
||||
entrypoints: [path.resolve(import.meta.dir, 'index.ts')],
|
||||
sourcemap: 'inline',
|
||||
publicPath: '/build/',
|
||||
});
|
||||
|
||||
if (bundle.success && bundle.outputs.length === 1) {
|
||||
return new Response(bundle.outputs[0]);
|
||||
} else {
|
||||
console.error('Multiple assets: ', bundle.outputs, 'or fail: ', !bundle.success);
|
||||
}
|
||||
return new Response(null, { status: 500 });
|
||||
default:
|
||||
console.log(`Pathname: ${pathname}`);
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
type Point = [number, number];
|
||||
type Rect = [number, number, number, number];
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
type Operand = Point | number;
|
||||
type Operation = (a: number, b: number) => number;
|
||||
function op(a: Operand, b: Operand, fn: Operation): Operand {
|
||||
if (!Array.isArray(a) && !Array.isArray(b)) return fn(a, b);
|
||||
if (Array.isArray(a) && !Array.isArray(b)) return a.map((x) => fn(x, b)) as Point;
|
||||
if (!Array.isArray(a) && Array.isArray(b)) return b.map((x) => fn(a, x)) as Point;
|
||||
if (Array.isArray(a) && Array.isArray(b)) return a.map((x, i) => fn(x, b[i])) as Point;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const operations: Record<string, [Operation, number]> = {
|
||||
'+': [(x, y) => x + y, 1],
|
||||
'-': [(x, y) => x - y, 1],
|
||||
'*': [(x, y) => x * y, 2],
|
||||
'/': [(x, y) => x / y, 2],
|
||||
'%': [(x, y) => x / y, 2],
|
||||
'(': [() => 0, -1],
|
||||
}
|
||||
|
||||
export function exp(strings: TemplateStringsArray, ...args: number[]): number;
|
||||
export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point;
|
||||
export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point | number {
|
||||
const input: (string | Operand)[] = [];
|
||||
const output: (Operation | Operand)[] = [];
|
||||
const stack: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
input.push(strings[i].trim(), args[i]);
|
||||
}
|
||||
input.push(strings[args.length].trim());
|
||||
|
||||
const pushOp = (op: string) => output.push(operations[op.trim()][0]);
|
||||
const opPriority = (op: string) => operations[op.trim()][1];
|
||||
|
||||
for (const item of input) {
|
||||
if (typeof item === 'string') {
|
||||
for (const char of item) {
|
||||
if (char.match(/[ \n\r\t]/)) {
|
||||
continue;
|
||||
} else if (char === '(') {
|
||||
stack.push(char);
|
||||
} else if (char === ')') {
|
||||
while (true) {
|
||||
const item = stack.pop();
|
||||
if (!item || item === '(') break;
|
||||
pushOp(item);
|
||||
}
|
||||
} else {
|
||||
const p = opPriority(char);
|
||||
while (true) {
|
||||
const item = stack[stack.length - 1];
|
||||
if (!item || opPriority(item) < p) break;
|
||||
stack.pop();
|
||||
pushOp(item);
|
||||
}
|
||||
stack.push(char);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const item = stack.pop();
|
||||
if (!item) break;
|
||||
pushOp(item);
|
||||
}
|
||||
|
||||
const calcStack: Operand[] = [];
|
||||
|
||||
for (const item of output) {
|
||||
if (typeof item === 'function') {
|
||||
const y = calcStack.pop();
|
||||
const x = calcStack.pop();
|
||||
|
||||
if (x == null || y == null) return NaN;
|
||||
|
||||
const result = op(x, y, item);
|
||||
calcStack.push(result);
|
||||
} else {
|
||||
calcStack.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return calcStack[0];
|
||||
}
|
||||
|
||||
export const prevent = (e: Event) => (e.preventDefault(), false);
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
|
|
|
|||
Loading…
Reference in New Issue