1
0
Fork 0

Boilerplating, pan, zoom, draw grid

This commit is contained in:
Pabloader 2024-06-25 22:04:24 +00:00
parent d8a9a10811
commit e5a1f361ed
11 changed files with 322 additions and 4 deletions

2
.gitignore vendored
View File

@ -173,3 +173,5 @@ dist
# Finder (MacOS) folder config
.DS_Store
public/build

View File

@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@ -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"
}

21
public/index.html Normal file
View File

@ -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>

76
src/game.ts Normal file
View File

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

80
src/graphics.ts Normal file
View File

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

14
src/index.ts Normal file
View File

@ -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);

31
src/server.ts Normal file
View File

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

2
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
type Point = [number, number];
type Rect = [number, number, number, number];

90
src/utils.ts Normal file
View File

@ -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);

View File

@ -11,7 +11,7 @@
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"verbatimModuleSyntax": false,
"noEmit": true,
// Best practices