diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc4c38f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.log
+node_modules
+dist
\ No newline at end of file
diff --git a/esbuild.d.ts b/esbuild.d.ts
new file mode 100644
index 0000000..56b7c17
--- /dev/null
+++ b/esbuild.d.ts
@@ -0,0 +1,4 @@
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
diff --git a/example/example.ts b/example/example.ts
new file mode 100644
index 0000000..e1ae16d
--- /dev/null
+++ b/example/example.ts
@@ -0,0 +1,38 @@
+import { Terminal } from "..";
+
+let t = new Terminal(30, 30);
+let m = { x: 0, y: 0 };
+
+t.canvas.style.width = t.canvas.width * 3 + "px";
+t.canvas.style.height = t.canvas.height * 3 + "px";
+document.body.append(t.canvas);
+
+t.ready().then(() => {
+ update();
+ onmousemove = event => {
+ m = t.screenToGrid(event.clientX, event.clientY);
+ update();
+ };
+});
+
+const BOX_DRAWING_EXAMPLE = `
+┌─┬┐ ╔═╦╗ ╓─╥╖ ╒═╤╕
+│ ││ ║ ║║ ║ ║║ │ ││
+├─┼┤ ╠═╬╣ ╟─╫╢ ╞═╪╡
+└─┴┘ ╚═╩╝ ╙─╨╜ ╘═╧╛
+┌───────────────────┐
+│ ╔═══╗ Some Text │▒
+│ ╚═╦═╝ in the box │▒
+╞═╤══╩══╤═══════════╡▒
+│ ├──┬──┤ │▒
+│ └──┴──┘ │▒
+└───────────────────┘▒
+ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
+`;
+
+function update() {
+ t.clear();
+ t.write(1, 1, BOX_DRAWING_EXAMPLE, "blue");
+ t.put(m.x, m.y, 0x40, "black", "white");
+}
+
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..6d75d7f
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/font.png b/font.png
new file mode 100644
index 0000000..53890dc
Binary files /dev/null and b/font.png differ
diff --git a/index.ts b/index.ts
new file mode 100644
index 0000000..6937fc0
--- /dev/null
+++ b/index.ts
@@ -0,0 +1,129 @@
+import defaultFontSrc from "./font.png";
+
+export interface Font {
+ image: HTMLImageElement;
+ charWidth: number;
+ charHeight: number;
+}
+
+const defaultFontImage = new Image();
+defaultFontImage.src = defaultFontSrc;
+
+const defaultFont: Font = {
+ image: defaultFontImage,
+ charWidth: 6,
+ charHeight: 6,
+};
+
+type CodePage = Record;
+export const CP437: CodePage = { 9786: 1, 9787: 2, 9829: 3, 9830: 4, 9827: 5, 9824: 6, 8226: 7, 9688: 8, 9675: 9, 9689: 10, 9794: 11, 9792: 12, 9834: 13, 9835: 14, 9788: 15, 9658: 16, 9668: 17, 8597: 18, 8252: 19, 182: 20, 167: 21, 9644: 22, 8616: 23, 8593: 24, 8595: 25, 8594: 26, 8592: 27, 8735: 28, 8596: 29, 9650: 30, 9660: 31, 8962: 127, 199: 128, 252: 129, 233: 130, 226: 131, 228: 132, 224: 133, 229: 134, 231: 135, 234: 136, 235: 137, 232: 138, 239: 139, 238: 140, 236: 141, 196: 142, 197: 143, 201: 144, 230: 145, 198: 146, 244: 147, 246: 148, 242: 149, 251: 150, 249: 151, 255: 152, 214: 153, 220: 154, 162: 155, 163: 156, 165: 157, 8359: 158, 402: 159, 225: 160, 237: 161, 243: 162, 250: 163, 241: 164, 209: 165, 170: 166, 186: 167, 191: 168, 8976: 169, 172: 170, 189: 171, 188: 172, 161: 173, 171: 174, 187: 175, 9617: 176, 9618: 177, 9619: 178, 9474: 179, 9508: 180, 9569: 181, 9570: 182, 9558: 183, 9557: 184, 9571: 185, 9553: 186, 9559: 187, 9565: 188, 9564: 189, 9563: 190, 9488: 191, 9492: 192, 9524: 193, 9516: 194, 9500: 195, 9472: 196, 9532: 197, 9566: 198, 9567: 199, 9562: 200, 9556: 201, 9577: 202, 9574: 203, 9568: 204, 9552: 205, 9580: 206, 9575: 207, 9576: 208, 9572: 209, 9573: 210, 9561: 211, 9560: 212, 9554: 213, 9555: 214, 9579: 215, 9578: 216, 9496: 217, 9484: 218, 9608: 219, 9604: 220, 9612: 221, 9616: 222, 9600: 223, 945: 224, 223: 225, 915: 226, 960: 227, 931: 228, 963: 229, 181: 230, 964: 231, 934: 232, 920: 233, 937: 234, 948: 235, 8734: 236, 966: 237, 949: 238, 8745: 239, 8801: 240, 177: 241, 8805: 242, 8804: 243, 8992: 244, 8993: 245, 247: 246, 8776: 247, 176: 248, 8729: 249, 183: 250, 8730: 251, 8319: 252, 178: 253, 9632: 254 };
+
+export class Terminal {
+ width: number;
+ height: number;
+ canvas: HTMLCanvasElement;
+ ctx: CanvasRenderingContext2D;
+ codepage: CodePage = CP437;
+
+ private font: Font;
+ private tintCache = new Map();
+
+ constructor(width: number, height: number, font: Font = defaultFont) {
+ let canvas = document.createElement("canvas");
+ let ctx = canvas.getContext("2d")!;
+ canvas.width = width * font.charWidth;
+ canvas.height = height * font.charHeight;
+ canvas.style.imageRendering = "pixelated";
+ ctx.imageSmoothingEnabled = false;
+
+ this.width = width;
+ this.height = height;
+ this.font = font;
+ this.canvas = canvas;
+ this.ctx = ctx;
+ }
+
+ private tint(color: string) {
+ let cached = this.tintCache.get(color);
+ if (cached) return cached;
+
+ let canvas = document.createElement("canvas");
+ let ctx = canvas.getContext("2d")!;
+ canvas.width = this.font.image.width;
+ canvas.height = this.font.image.height;
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(this.font.image, 0, 0);
+ ctx.fillStyle = color;
+ ctx.globalCompositeOperation = "source-atop";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ this.tintCache.set(color, canvas);
+ return canvas;
+ }
+
+ ready() {
+ return new Promise((resolve, reject) => {
+ this.font.image.addEventListener("load", () => resolve());
+ this.font.image.addEventListener("error", reject);
+ });
+ }
+
+ put(x: number, y: number, code: number, fg?: string, bg?: string) {
+ // If the image hasn't loaded yet, we need to bail out of rendering
+ if (this.font.image.width === 0 || this.font.image.height === 0) return;
+
+ code = this.codepage[code] ?? code;
+ let img: HTMLImageElement | HTMLCanvasElement = this.font.image;
+ let cw = this.font.charWidth;
+ let ch = this.font.charHeight;
+ let cols = img.width / cw;
+ let sx = (code % cols) * cw;
+ let sy = (code / cols | 0) * ch;
+ let dx = x * cw;
+ let dy = y * ch;
+
+ if (fg) {
+ img = this.tint(fg);
+ }
+
+ if (bg) {
+ this.ctx.fillStyle = bg;
+ this.ctx.fillRect(dx, dy, cw, ch);
+ }
+
+ this.ctx.drawImage(img, sx, sy, cw, ch, dx, dy, cw, ch);
+ }
+
+ write(x: number, y: number, text: string, fg?: string, bg?: string) {
+ let tx = x;
+ let ty = y;
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === "\n") {
+ tx = x;
+ ty++;
+ } else {
+ this.put(tx++, ty, text.charCodeAt(i), fg, bg);
+ }
+ }
+ }
+
+ clear(
+ x: number = 0,
+ y: number = 0,
+ width: number = this.width,
+ height: number = this.height
+ ) {
+ let cw = this.font.charWidth;
+ let ch = this.font.charHeight;
+ this.ctx.clearRect(x * cw, y * ch, width * cw, height * ch);
+ }
+
+ screenToGrid(x: number, y: number) {
+ let { charWidth, charHeight } = this.font;
+ let rect = this.canvas.getBoundingClientRect();
+ let scaleX = rect.width / charWidth / this.width;
+ let scaleY = rect.height / charHeight / this.height;
+ let gridX = (x - rect.x) / charWidth / scaleX;
+ let gridY = (y - rect.y) / charHeight / scaleY;
+ return { x: Math.floor(gridX), y: Math.floor(gridY) };
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..89356be
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "devDependencies": {
+ "esbuild": "^0.14.27",
+ "typescript": "^4.6.2"
+ },
+ "scripts": {
+ "dev": "./scripts/dev",
+ "build": "./scripts/build"
+ }
+}
diff --git a/scripts/build b/scripts/build
new file mode 100755
index 0000000..31d22cb
--- /dev/null
+++ b/scripts/build
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+let esbuild = require("esbuild");
+
+/**
+ * @type {esbuild.BuildOptions}
+ */
+let options = {
+ entryPoints: ["index.ts"],
+ bundle: true,
+ minify: true,
+ sourcemap: true,
+ loader: { ".png": "dataurl" },
+ format: "esm",
+ outfile: "dist/telerin.js",
+};
+
+if (require.main) {
+ esbuild.buildSync(options);
+}
+
+module.exports = options;
\ No newline at end of file
diff --git a/scripts/dev b/scripts/dev
new file mode 100755
index 0000000..8dd5391
--- /dev/null
+++ b/scripts/dev
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+
+let esbuild = require("esbuild");
+let buildOptions = require("./build");
+
+esbuild.serve({
+ servedir: "example"
+}, {
+ ...buildOptions,
+ entryPoints: ["example/example.ts"],
+ outfile: "example/example.js",
+}).then(result => {
+ console.log(`http://localhost:${result.port}`);
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ecd4829
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noImplicitReturns": true
+ }
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..7408c95
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,134 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+esbuild-android-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz#b868bbd9955a92309c69df628d8dd1945478b45c"
+ integrity sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==
+
+esbuild-android-arm64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz#e7d6430555e8e9c505fd87266bbc709f25f1825c"
+ integrity sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==
+
+esbuild-darwin-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz#4dc7484127564e89b4445c0a560a3cb50b3d68e1"
+ integrity sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==
+
+esbuild-darwin-arm64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz#469e59c665f84a8ed323166624c5e7b9b2d22ac1"
+ integrity sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==
+
+esbuild-freebsd-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz#895df03bf5f87094a56c9a5815bf92e591903d70"
+ integrity sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==
+
+esbuild-freebsd-arm64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz#0b72a41a6b8655e9a8c5608f2ec1afdcf6958441"
+ integrity sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==
+
+esbuild-linux-32@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz#43b8ba3803b0bbe7f051869c6a8bf6de1e95de28"
+ integrity sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==
+
+esbuild-linux-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz#dc8072097327ecfadba1735562824ce8c05dd0bd"
+ integrity sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==
+
+esbuild-linux-arm64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz#c52b58cbe948426b1559910f521b0a3f396f10b8"
+ integrity sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==
+
+esbuild-linux-arm@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz#df869dbd67d4ee3a04b3c7273b6bd2b233e78a18"
+ integrity sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==
+
+esbuild-linux-mips64le@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz#a2b646d9df368b01aa970a7b8968be6dd6b01d19"
+ integrity sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==
+
+esbuild-linux-ppc64le@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz#9a21af766a0292578a3009c7408b8509cac7cefd"
+ integrity sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==
+
+esbuild-linux-riscv64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz#344a27f91568056a5903ad5841b447e00e78d740"
+ integrity sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==
+
+esbuild-linux-s390x@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz#73a7309bd648a07ef58f069658f989a5096130db"
+ integrity sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==
+
+esbuild-netbsd-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz#482a587cdbd18a6c264a05136596927deb46c30a"
+ integrity sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==
+
+esbuild-openbsd-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz#e99f8cdc63f1628747b63edd124d53cf7796468d"
+ integrity sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==
+
+esbuild-sunos-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz#8611d825bcb8239c78d57452e83253a71942f45c"
+ integrity sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==
+
+esbuild-windows-32@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz#c06374206d4d92dd31d4fda299b09f51a35e82f6"
+ integrity sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==
+
+esbuild-windows-64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz#756631c1d301dfc0d1a887deed2459ce4079582f"
+ integrity sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==
+
+esbuild-windows-arm64@0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz#ad7e187193dcd18768b16065a950f4441d7173f4"
+ integrity sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==
+
+esbuild@^0.14.27:
+ version "0.14.27"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.27.tgz#41fe0f1b6b68b9f77cac025009bc54bb96e616f1"
+ integrity sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==
+ optionalDependencies:
+ esbuild-android-64 "0.14.27"
+ esbuild-android-arm64 "0.14.27"
+ esbuild-darwin-64 "0.14.27"
+ esbuild-darwin-arm64 "0.14.27"
+ esbuild-freebsd-64 "0.14.27"
+ esbuild-freebsd-arm64 "0.14.27"
+ esbuild-linux-32 "0.14.27"
+ esbuild-linux-64 "0.14.27"
+ esbuild-linux-arm "0.14.27"
+ esbuild-linux-arm64 "0.14.27"
+ esbuild-linux-mips64le "0.14.27"
+ esbuild-linux-ppc64le "0.14.27"
+ esbuild-linux-riscv64 "0.14.27"
+ esbuild-linux-s390x "0.14.27"
+ esbuild-netbsd-64 "0.14.27"
+ esbuild-openbsd-64 "0.14.27"
+ esbuild-sunos-64 "0.14.27"
+ esbuild-windows-32 "0.14.27"
+ esbuild-windows-64 "0.14.27"
+ esbuild-windows-arm64 "0.14.27"
+
+typescript@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
+ integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==