diff --git a/.gitignore b/.gitignore index 9b1ee42..88a47b8 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ dist # Finder (MacOS) folder config .DS_Store + +public/build \ No newline at end of file diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index 7408ab3..0cda77a 100644 --- a/package.json +++ b/package.json @@ -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" } \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3173609 --- /dev/null +++ b/public/index.html @@ -0,0 +1,21 @@ + + + + + + Binario + + + + + + + \ No newline at end of file diff --git a/src/game.ts b/src/game.ts new file mode 100644 index 0000000..1f0e618 --- /dev/null +++ b/src/game.ts @@ -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); + } + } +} + diff --git a/src/graphics.ts b/src/graphics.ts new file mode 100644 index 0000000..85f2363 --- /dev/null +++ b/src/graphics.ts @@ -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}`; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..168604e --- /dev/null +++ b/src/index.ts @@ -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); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2564776 --- /dev/null +++ b/src/server.ts @@ -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 }); + } + } +}) \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..ed7b5f1 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,2 @@ +type Point = [number, number]; +type Rect = [number, number, number, number]; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e4b8a4d --- /dev/null +++ b/src/utils.ts @@ -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 = { + '+': [(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); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 238655f..f922565 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "noEmit": true, // Best practices