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