{ "version": 3, "sources": ["../node_modules/kaboom/src/utils.ts", "../node_modules/kaboom/src/math.ts", "../node_modules/kaboom/src/fps.ts", "../node_modules/kaboom/src/timer.ts", "../node_modules/kaboom/src/kaboom.ts", "../code/easing.js", "../code/main.js"], "sourcesContent": ["export class IDList extends Map {\n\t_lastID: number;\n\tconstructor(...args) {\n\t\tsuper(...args);\n\t\tthis._lastID = 0;\n\t}\n\tpush(v: T): number {\n\t\tconst id = this._lastID;\n\t\tthis.set(id, v);\n\t\tthis._lastID++;\n\t\treturn id;\n\t}\n\tpushd(v: T): () => void {\n\t\tconst id = this.push(v);\n\t\treturn () => this.delete(id);\n\t}\n}\n\nexport function deepEq(o1: any, o2: any): boolean {\n\tconst t1 = typeof o1;\n\tconst t2 = typeof o2;\n\tif (t1 !== t2) {\n\t\treturn false;\n\t}\n\tif (t1 === \"object\" && t2 === \"object\") {\n\t\tconst k1 = Object.keys(o1);\n\t\tconst k2 = Object.keys(o2);\n\t\tif (k1.length !== k2.length) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const k of k1) {\n\t\t\tconst v1 = o1[k];\n\t\t\tconst v2 = o2[k];\n\t\t\tif (!(typeof v1 === \"function\" && typeof v2 === \"function\")) {\n\t\t\t\tif (!deepEq(v1, v2)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\treturn o1 === o2;\n}\n\nexport function downloadURL(url: string, filename: string) {\n\tconst a = document.createElement(\"a\");\n\tdocument.body.appendChild(a);\n\ta.setAttribute(\"style\", \"display: none\");\n\ta.href = url;\n\ta.download = filename;\n\ta.click();\n\tdocument.body.removeChild(a);\n}\n\nexport function downloadBlob(blob: Blob, filename: string) {\n\tconst url = URL.createObjectURL(blob);\n\tdownloadURL(url, filename);\n\tURL.revokeObjectURL(url);\n}\n\nexport function isDataURL(str: string) {\n\treturn str.match(/^data:\\w+\\/\\w+;base64,.+/);\n}\n\nexport const uid = (() => {\n\tlet id = 0;\n\treturn () => id++;\n})();\n\nconst warned = new Set();\n\nexport function deprecateMsg(oldName: string, newName: string) {\n\tif (!warned.has(oldName)) {\n\t\twarned.add(oldName);\n\t\tconsole.warn(`${oldName} is deprecated. Use ${newName} instead.`);\n\t}\n}\n\nexport const deprecate = (oldName: string, newName: string, newFunc: (...args) => any) => (...args) => {\n\tdeprecateMsg(oldName, newName);\n\treturn newFunc(...args);\n};\n", "import {\n\tVec4,\n\tPoint,\n\tPolygon,\n\tArea,\n} from \"./types\";\n\nimport {\n\tdeprecateMsg,\n} from \"./utils\";\n\nexport function deg2rad(deg: number): number {\n\treturn deg * Math.PI / 180;\n}\n\nexport function rad2deg(rad: number): number {\n\treturn rad * 180 / Math.PI;\n}\n\nexport function clamp(\n\tval: number,\n\tmin: number,\n\tmax: number,\n): number {\n\tif (min > max) {\n\t\treturn clamp(val, max, min);\n\t}\n\treturn Math.min(Math.max(val, min), max);\n}\n\nexport function lerp(\n\ta: number,\n\tb: number,\n\tt: number,\n): number {\n\treturn a + (b - a) * t;\n}\n\nexport function map(\n\tv: number,\n\tl1: number,\n\th1: number,\n\tl2: number,\n\th2: number,\n): number {\n\treturn l2 + (v - l1) / (h1 - l1) * (h2 - l2);\n}\n\nexport function mapc(\n\tv: number,\n\tl1: number,\n\th1: number,\n\tl2: number,\n\th2: number,\n): number {\n\treturn clamp(map(v, l1, h1, l2, h2), l2, h2);\n}\n\nexport class Vec2 {\n\tx: number = 0;\n\ty: number = 0;\n\tconstructor(x: number = 0, y: number = x) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t}\n\tstatic fromAngle(deg: number) {\n\t\tconst angle = deg2rad(deg);\n\t\treturn new Vec2(Math.cos(angle), Math.sin(angle));\n\t}\n\tstatic LEFT = new Vec2(-1, 0);\n\tstatic RIGHT = new Vec2(1, 0);\n\tstatic UP = new Vec2(0, -1);\n\tstatic DOWN = new Vec2(0, 1);\n\tclone(): Vec2 {\n\t\treturn new Vec2(this.x, this.y);\n\t}\n\tadd(...args): Vec2 {\n\t\tconst p2 = vec2(...args);\n\t\treturn new Vec2(this.x + p2.x, this.y + p2.y);\n\t}\n\tsub(...args): Vec2 {\n\t\tconst p2 = vec2(...args);\n\t\treturn new Vec2(this.x - p2.x, this.y - p2.y);\n\t}\n\tscale(...args): Vec2 {\n\t\tconst s = vec2(...args);\n\t\treturn new Vec2(this.x * s.x, this.y * s.y);\n\t}\n\tdist(...args): number {\n\t\tconst p2 = vec2(...args);\n\t\treturn Math.sqrt(\n\t\t\t(this.x - p2.x) * (this.x - p2.x)\n\t\t\t+ (this.y - p2.y) * (this.y - p2.y)\n\t\t);\n\t}\n\tlen(): number {\n\t\treturn this.dist(new Vec2(0, 0));\n\t}\n\tunit(): Vec2 {\n\t\treturn this.scale(1 / this.len());\n\t}\n\tnormal(): Vec2 {\n\t\treturn new Vec2(this.y, -this.x);\n\t}\n\tdot(p2: Vec2): number {\n\t\treturn this.x * p2.x + this.y * p2.y;\n\t}\n\tangle(...args): number {\n\t\tconst p2 = vec2(...args);\n\t\treturn rad2deg(Math.atan2(this.y - p2.y, this.x - p2.x));\n\t}\n\tlerp(p2: Vec2, t: number): Vec2 {\n\t\treturn new Vec2(lerp(this.x, p2.x, t), lerp(this.y, p2.y, t));\n\t}\n\ttoFixed(n: number): Vec2 {\n\t\treturn new Vec2(Number(this.x.toFixed(n)), Number(this.y.toFixed(n)));\n\t}\n\teq(other: Vec2): boolean {\n\t\treturn this.x === other.x && this.y === other.y;\n\t}\n\ttoString(): string {\n\t\treturn `(${this.x.toFixed(2)}, ${this.y.toFixed(2)})`;\n\t}\n\tstr(): string {\n\t\treturn this.toString();\n\t}\n}\n\nexport function vec2(...args): Vec2 {\n\tif (args.length === 1) {\n\t\tif (args[0] instanceof Vec2) {\n\t\t\treturn vec2(args[0].x, args[0].y);\n\t\t} else if (Array.isArray(args[0]) && args[0].length === 2) {\n\t\t\treturn vec2.apply(null, args[0]);\n\t\t}\n\t}\n\treturn new Vec2(...args);\n}\n\nexport class Vec3 {\n\tx: number = 0;\n\ty: number = 0;\n\tz: number = 0;\n\tconstructor(x: number, y: number, z: number) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t}\n\txy() {\n\t\treturn vec2(this.x, this.y);\n\t}\n}\n\nexport const vec3 = (x, y, z) => new Vec3(x, y, z);\n\nexport class Color {\n\n\tr: number = 255;\n\tg: number = 255;\n\tb: number = 255;\n\n\tconstructor(r: number, g: number, b: number) {\n\t\tthis.r = clamp(r, 0, 255);\n\t\tthis.g = clamp(g, 0, 255);\n\t\tthis.b = clamp(b, 0, 255);\n\t}\n\n\tstatic fromArray(arr: number[]) {\n\t\treturn new Color(arr[0], arr[1], arr[2])\n\t}\n\n\tstatic RED = rgb(255, 0, 0);\n\tstatic GREEN = rgb(0, 255, 0);\n\tstatic BLUE = rgb(0, 0, 255);\n\tstatic YELLOW = rgb(255, 255, 0);\n\tstatic MAGENTA = rgb(255, 0, 255);\n\tstatic CYAN = rgb(0, 255, 255);\n\tstatic WHITE = rgb(255, 255, 255);\n\tstatic BLACK = rgb(0, 0, 0);\n\n\tclone(): Color {\n\t\treturn new Color(this.r, this.g, this.b);\n\t}\n\n\tlighten(a: number): Color {\n\t\treturn new Color(this.r + a, this.g + a, this.b + a);\n\t}\n\n\tdarken(a: number): Color {\n\t\treturn this.lighten(-a);\n\t}\n\n\tinvert(): Color {\n\t\treturn new Color(255 - this.r, 255 - this.g, 255 - this.b);\n\t}\n\n\tmult(other: Color): Color {\n\t\treturn new Color(\n\t\t\tthis.r * other.r / 255,\n\t\t\tthis.g * other.g / 255,\n\t\t\tthis.b * other.b / 255,\n\t\t);\n\t}\n\n\teq(other: Color): boolean {\n\t\treturn this.r === other.r\n\t\t\t&& this.g === other.g\n\t\t\t&& this.b === other.b\n\t\t\t;\n\t}\n\n\tstr(): string {\n\t\tdeprecateMsg(\"str()\", \"toString()\");\n\t\treturn `(${this.r}, ${this.g}, ${this.b})`;\n\t}\n\n\ttoString(): string {\n\t\treturn `(${this.r}, ${this.g}, ${this.b})`;\n\t}\n\n\tstatic fromHSL(h: number, s: number, l: number) {\n\n\t\tif (s == 0){\n\t\t\treturn rgb(255 * l, 255 * l, 255 * l);\n\t\t}\n\n\t\tconst hue2rgb = (p, q, t) => {\n\t\t\tif (t < 0) t += 1;\n\t\t\tif (t > 1) t -= 1;\n\t\t\tif (t < 1 / 6) return p + (q - p) * 6 * t;\n\t\t\tif (t < 1 / 2) return q;\n\t\t\tif (t < 2 / 3) return p + (q - p) * (2/3 - t) * 6;\n\t\t\treturn p;\n\t\t}\n\n\t\tconst q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n\t\tconst p = 2 * l - q;\n\t\tconst r = hue2rgb(p, q, h + 1 / 3);\n\t\tconst g = hue2rgb(p, q, h);\n\t\tconst b = hue2rgb(p, q, h - 1 / 3);\n\n\t\treturn new Color(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));\n\n\t}\n\n}\n\nexport function rgb(...args): Color {\n\tif (args.length === 0) {\n\t\treturn new Color(255, 255, 255);\n\t} else if (args.length === 1) {\n\t\tif (args[0] instanceof Color) {\n\t\t\treturn args[0].clone();\n\t\t} else if (Array.isArray(args[0]) && args[0].length === 3) {\n\t\t\treturn Color.fromArray(args[0]);\n\t\t}\n\t}\n\t// @ts-ignore\n\treturn new Color(...args);\n}\n\nexport const hsl2rgb = (h, s, l) => Color.fromHSL(h, s, l);\n\nexport class Quad {\n\tx: number = 0;\n\ty: number = 0;\n\tw: number = 1;\n\th: number = 1;\n\tconstructor(x: number, y: number, w: number, h: number) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.w = w;\n\t\tthis.h = h;\n\t}\n\tscale(other: Quad): Quad {\n\t\treturn new Quad(\n\t\t\tthis.x + this.w * other.x,\n\t\t\tthis.y + this.h * other.y,\n\t\t\tthis.w * other.w,\n\t\t\tthis.h * other.h\n\t\t);\n\t}\n\tclone(): Quad {\n\t\treturn new Quad(this.x, this.y, this.w, this.h);\n\t}\n\teq(other: Quad): boolean {\n\t\treturn this.x === other.x\n\t\t\t&& this.y === other.y\n\t\t\t&& this.w === other.w\n\t\t\t&& this.h === other.h;\n\t}\n\ttoString(): string {\n\t\treturn `quad(${this.x}, ${this.y}, ${this.w}, ${this.h})`;\n\t}\n}\n\nexport function quad(x: number, y: number, w: number, h: number): Quad {\n\treturn new Quad(x, y, w, h);\n}\n\nexport class Mat4 {\n\n\tm: number[] = [\n\t\t1, 0, 0, 0,\n\t\t0, 1, 0, 0,\n\t\t0, 0, 1, 0,\n\t\t0, 0, 0, 1,\n\t];\n\n\tconstructor(m?: number[]) {\n\t\tif (m) {\n\t\t\tthis.m = m;\n\t\t}\n\t}\n\n\tclone(): Mat4 {\n\t\treturn new Mat4(this.m);\n\t};\n\n\tmult(other: Mat4): Mat4 {\n\n\t\tconst out = [];\n\n\t\tfor (let i = 0; i < 4; i++) {\n\t\t\tfor (let j = 0; j < 4; j++) {\n\t\t\t\tout[i * 4 + j] =\n\t\t\t\t\tthis.m[0 * 4 + j] * other.m[i * 4 + 0] +\n\t\t\t\t\tthis.m[1 * 4 + j] * other.m[i * 4 + 1] +\n\t\t\t\t\tthis.m[2 * 4 + j] * other.m[i * 4 + 2] +\n\t\t\t\t\tthis.m[3 * 4 + j] * other.m[i * 4 + 3];\n\t\t\t}\n\t\t}\n\n\t\treturn new Mat4(out);\n\n\t}\n\n\tmultVec4(p: Vec4): Vec4 {\n\t\treturn {\n\t\t\tx: p.x * this.m[0] + p.y * this.m[4] + p.z * this.m[8] + p.w * this.m[12],\n\t\t\ty: p.x * this.m[1] + p.y * this.m[5] + p.z * this.m[9] + p.w * this.m[13],\n\t\t\tz: p.x * this.m[2] + p.y * this.m[6] + p.z * this.m[10] + p.w * this.m[14],\n\t\t\tw: p.x * this.m[3] + p.y * this.m[7] + p.z * this.m[11] + p.w * this.m[15]\n\t\t};\n\t}\n\n\tmultVec3(p: Vec3): Vec3 {\n\t\tconst p4 = this.multVec4({\n\t\t\tx: p.x,\n\t\t\ty: p.y,\n\t\t\tz: p.z,\n\t\t\tw: 1.0,\n\t\t});\n\t\treturn vec3(p4.x, p4.y, p4.z);\n\t}\n\n\tmultVec2(p: Vec2): Vec2 {\n\t\treturn vec2(\n\t\t\tp.x * this.m[0] + p.y * this.m[4] + 0 * this.m[8] + 1 * this.m[12],\n\t\t\tp.x * this.m[1] + p.y * this.m[5] + 0 * this.m[9] + 1 * this.m[13],\n\t\t);\n\t}\n\n\tstatic translate(p: Vec2): Mat4 {\n\t\treturn new Mat4([\n\t\t\t1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\tp.x, p.y, 0, 1,\n\t\t]);\n\t}\n\n\tstatic scale(s: Vec2): Mat4 {\n\t\treturn new Mat4([\n\t\t\ts.x, 0, 0, 0,\n\t\t\t0, s.y, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1,\n\t\t]);\n\t}\n\n\tstatic rotateX(a: number): Mat4 {\n\t\ta = deg2rad(-a);\n\t\treturn new Mat4([\n\t\t\t1, 0, 0, 0,\n\t\t\t0, Math.cos(a), -Math.sin(a), 0,\n\t\t\t0, Math.sin(a), Math.cos(a), 0,\n\t\t\t0, 0, 0, 1,\n\t\t]);\n\t}\n\n\tstatic rotateY(a: number): Mat4 {\n\t\ta = deg2rad(-a);\n\t\treturn new Mat4([\n\t\t\tMath.cos(a), 0, Math.sin(a), 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t-Math.sin(a), 0, Math.cos(a), 0,\n\t\t\t0, 0, 0, 1,\n\t\t]);\n\t}\n\n\tstatic rotateZ(a: number): Mat4 {\n\t\ta = deg2rad(-a);\n\t\treturn new Mat4([\n\t\t\tMath.cos(a), -Math.sin(a), 0, 0,\n\t\t\tMath.sin(a), Math.cos(a), 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1,\n\t\t]);\n\t}\n\n\ttranslate(p: Vec2): Mat4 {\n\t\treturn this.mult(Mat4.translate(p));\n\t}\n\n\tscale(s: Vec2): Mat4 {\n\t\treturn this.mult(Mat4.scale(s));\n\t}\n\n\trotateX(a: number): Mat4 {\n\t\treturn this.mult(Mat4.rotateX(a));\n\t}\n\n\trotateY(a: number): Mat4 {\n\t\treturn this.mult(Mat4.rotateY(a));\n\t}\n\n\trotateZ(a: number): Mat4 {\n\t\treturn this.mult(Mat4.rotateZ(a));\n\t}\n\n\tinvert(): Mat4 {\n\n\t\tconst out = [];\n\n\t\tconst f00 = this.m[10] * this.m[15] - this.m[14] * this.m[11];\n\t\tconst f01 = this.m[9] * this.m[15] - this.m[13] * this.m[11];\n\t\tconst f02 = this.m[9] * this.m[14] - this.m[13] * this.m[10];\n\t\tconst f03 = this.m[8] * this.m[15] - this.m[12] * this.m[11];\n\t\tconst f04 = this.m[8] * this.m[14] - this.m[12] * this.m[10];\n\t\tconst f05 = this.m[8] * this.m[13] - this.m[12] * this.m[9];\n\t\tconst f06 = this.m[6] * this.m[15] - this.m[14] * this.m[7];\n\t\tconst f07 = this.m[5] * this.m[15] - this.m[13] * this.m[7];\n\t\tconst f08 = this.m[5] * this.m[14] - this.m[13] * this.m[6];\n\t\tconst f09 = this.m[4] * this.m[15] - this.m[12] * this.m[7];\n\t\tconst f10 = this.m[4] * this.m[14] - this.m[12] * this.m[6];\n\t\tconst f11 = this.m[5] * this.m[15] - this.m[13] * this.m[7];\n\t\tconst f12 = this.m[4] * this.m[13] - this.m[12] * this.m[5];\n\t\tconst f13 = this.m[6] * this.m[11] - this.m[10] * this.m[7];\n\t\tconst f14 = this.m[5] * this.m[11] - this.m[9] * this.m[7];\n\t\tconst f15 = this.m[5] * this.m[10] - this.m[9] * this.m[6];\n\t\tconst f16 = this.m[4] * this.m[11] - this.m[8] * this.m[7];\n\t\tconst f17 = this.m[4] * this.m[10] - this.m[8] * this.m[6];\n\t\tconst f18 = this.m[4] * this.m[9] - this.m[8] * this.m[5];\n\n\t\tout[0] = this.m[5] * f00 - this.m[6] * f01 + this.m[7] * f02;\n\t\tout[4] = -(this.m[4] * f00 - this.m[6] * f03 + this.m[7] * f04);\n\t\tout[8] = this.m[4] * f01 - this.m[5] * f03 + this.m[7] * f05;\n\t\tout[12] = -(this.m[4] * f02 - this.m[5] * f04 + this.m[6] * f05);\n\n\t\tout[1] = -(this.m[1] * f00 - this.m[2] * f01 + this.m[3] * f02);\n\t\tout[5] = this.m[0] * f00 - this.m[2] * f03 + this.m[3] * f04;\n\t\tout[9] = -(this.m[0] * f01 - this.m[1] * f03 + this.m[3] * f05);\n\t\tout[13] = this.m[0] * f02 - this.m[1] * f04 + this.m[2] * f05;\n\n\t\tout[2] = this.m[1] * f06 - this.m[2] * f07 + this.m[3] * f08;\n\t\tout[6] = -(this.m[0] * f06 - this.m[2] * f09 + this.m[3] * f10);\n\t\tout[10] = this.m[0] * f11 - this.m[1] * f09 + this.m[3] * f12;\n\t\tout[14] = -(this.m[0] * f08 - this.m[1] * f10 + this.m[2] * f12);\n\n\t\tout[3] = -(this.m[1] * f13 - this.m[2] * f14 + this.m[3] * f15);\n\t\tout[7] = this.m[0] * f13 - this.m[2] * f16 + this.m[3] * f17;\n\t\tout[11] = -(this.m[0] * f14 - this.m[1] * f16 + this.m[3] * f18);\n\t\tout[15] = this.m[0] * f15 - this.m[1] * f17 + this.m[2] * f18;\n\n\t\tconst det =\n\t\t\tthis.m[0] * out[0] +\n\t\t\tthis.m[1] * out[4] +\n\t\t\tthis.m[2] * out[8] +\n\t\t\tthis.m[3] * out[12];\n\n\t\tfor (let i = 0; i < 4; i++) {\n\t\t\tfor (let j = 0; j < 4; j++) {\n\t\t\t\tout[i * 4 + j] *= (1.0 / det);\n\t\t\t}\n\t\t}\n\n\t\treturn new Mat4(out);\n\n\t}\n\n\ttoString(): string {\n\t\treturn this.m.toString();\n\t}\n\n}\n\nexport function wave(lo: number, hi: number, t: number, f = Math.sin): number {\n\treturn lo + (f(t) + 1) / 2 * (hi - lo);\n}\n\n// basic ANSI C LCG\nconst A = 1103515245;\nconst C = 12345;\nconst M = 2147483648;\n\nexport class RNG {\n\tseed: number;\n\tconstructor(seed: number) {\n\t\tthis.seed = seed;\n\t}\n\tgen(...args) {\n\t\tif (args.length === 0) {\n\t\t\tthis.seed = (A * this.seed + C) % M;\n\t\t\treturn this.seed / M;\n\t\t} else if (args.length === 1) {\n\t\t\tif (typeof args[0] === \"number\") {\n\t\t\t\treturn this.gen(0, args[0]);\n\t\t\t} else if (args[0] instanceof Vec2) {\n\t\t\t\treturn this.gen(vec2(0, 0), args[0]);\n\t\t\t} else if (args[0] instanceof Color) {\n\t\t\t\treturn this.gen(rgb(0, 0, 0), args[0]);\n\t\t\t}\n\t\t} else if (args.length === 2) {\n\t\t\tif (typeof args[0] === \"number\" && typeof args[1] === \"number\") {\n\t\t\t\treturn (this.gen() * (args[1] - args[0])) + args[0];\n\t\t\t} else if (args[0] instanceof Vec2 && args[1] instanceof Vec2) {\n\t\t\t\treturn vec2(\n\t\t\t\t\tthis.gen(args[0].x, args[1].x),\n\t\t\t\t\tthis.gen(args[0].y, args[1].y),\n\t\t\t\t);\n\t\t\t} else if (args[0] instanceof Color && args[1] instanceof Color) {\n\t\t\t\treturn rgb(\n\t\t\t\t\tthis.gen(args[0].r, args[1].r),\n\t\t\t\t\tthis.gen(args[0].g, args[1].g),\n\t\t\t\t\tthis.gen(args[0].b, args[1].b),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// TODO: let user pass seed\nconst defRNG = new RNG(Date.now());\n\nexport function rng(seed: number): RNG {\n\tdeprecateMsg(\"rng()\", \"new RNG()\");\n\treturn new RNG(seed);\n}\n\nexport function randSeed(seed?: number): number {\n\tif (seed != null) {\n\t\tdefRNG.seed = seed;\n\t}\n\treturn defRNG.seed;\n}\n\nexport function rand(...args) {\n\t// @ts-ignore\n\treturn defRNG.gen(...args);\n}\n\n// TODO: randi() to return 0 / 1?\nexport function randi(...args) {\n\treturn Math.floor(rand(...args));\n}\n\nexport function chance(p: number): boolean {\n\treturn rand() <= p;\n}\n\nexport function choose(list: T[]): T {\n\treturn list[randi(list.length)];\n}\n\n// TODO: better name\nexport function testRectRect2(r1: Rect, r2: Rect): boolean {\n\treturn r1.p2.x >= r2.p1.x\n\t\t&& r1.p1.x <= r2.p2.x\n\t\t&& r1.p2.y >= r2.p1.y\n\t\t&& r1.p1.y <= r2.p2.y;\n}\n\nexport function testRectRect(r1: Rect, r2: Rect): boolean {\n\treturn r1.p2.x > r2.p1.x\n\t\t&& r1.p1.x < r2.p2.x\n\t\t&& r1.p2.y > r2.p1.y\n\t\t&& r1.p1.y < r2.p2.y;\n}\n\n// TODO: better name\nexport function testLineLineT(l1: Line, l2: Line): number | null {\n\n\tif ((l1.p1.x === l1.p2.x && l1.p1.y === l1.p2.y) || (l2.p1.x === l2.p2.x && l2.p1.y === l2.p2.y)) {\n\t\treturn null;\n\t}\n\n\tconst denom = ((l2.p2.y - l2.p1.y) * (l1.p2.x - l1.p1.x) - (l2.p2.x - l2.p1.x) * (l1.p2.y - l1.p1.y));\n\n\t// parallel\n\tif (denom === 0) {\n\t\treturn null;\n\t}\n\n\tconst ua = ((l2.p2.x - l2.p1.x) * (l1.p1.y - l2.p1.y) - (l2.p2.y - l2.p1.y) * (l1.p1.x - l2.p1.x)) / denom;\n\tconst ub = ((l1.p2.x - l1.p1.x) * (l1.p1.y - l2.p1.y) - (l1.p2.y - l1.p1.y) * (l1.p1.x - l2.p1.x)) / denom;\n\n\t// is the intersection on the segments\n\tif (ua < 0 || ua > 1 || ub < 0 || ub > 1) {\n\t\treturn null;\n\t}\n\n\treturn ua;\n\n}\n\nexport function testLineLine(l1: Line, l2: Line): Vec2 | null {\n\tconst t = testLineLineT(l1, l2);\n\tif (!t) return null;\n\treturn vec2(\n\t\tl1.p1.x + t * (l1.p2.x - l1.p1.x),\n\t\tl1.p1.y + t * (l1.p2.y - l1.p1.y),\n\t);\n}\n\nexport function testRectLine(r: Rect, l: Line): boolean {\n\tif (testRectPoint(r, l.p1) || testRectPoint(r, l.p2)) {\n\t\treturn true;\n\t}\n\treturn !!testLineLine(l, new Line(r.p1, vec2(r.p2.x, r.p1.y)))\n\t\t|| !!testLineLine(l, new Line(vec2(r.p2.x, r.p1.y), r.p2))\n\t\t|| !!testLineLine(l, new Line(r.p2, vec2(r.p1.x, r.p2.y)))\n\t\t|| !!testLineLine(l, new Line(vec2(r.p1.x, r.p2.y), r.p1));\n}\n\nexport function testRectPoint2(r: Rect, pt: Point): boolean {\n\treturn pt.x >= r.p1.x && pt.x <= r.p2.x && pt.y >= r.p1.y && pt.y <= r.p2.y;\n}\n\nexport function testRectPoint(r: Rect, pt: Point): boolean {\n\treturn pt.x > r.p1.x && pt.x < r.p2.x && pt.y > r.p1.y && pt.y < r.p2.y;\n}\n\nexport function testRectCircle(r: Rect, c: Circle): boolean {\n\tconst nx = Math.max(r.p1.x, Math.min(c.center.x, r.p2.x));\n\tconst ny = Math.max(r.p1.y, Math.min(c.center.y, r.p2.y));\n\tconst nearestPoint = vec2(nx, ny);\n\treturn nearestPoint.dist(c.center) <= c.radius;\n}\n\nexport function testRectPolygon(r: Rect, p: Polygon): boolean {\n\treturn testPolygonPolygon(p, [\n\t\tr.p1,\n\t\tvec2(r.p2.x, r.p1.y),\n\t\tr.p2,\n\t\tvec2(r.p1.x, r.p2.y),\n\t]);\n}\n\n// TODO\nexport function testLinePoint(l: Line, pt: Vec2): boolean {\n\treturn false;\n}\n\n// TODO\nexport function testLineCircle(l: Line, c: Circle): boolean {\n\treturn false;\n}\n\nexport function testLinePolygon(l: Line, p: Polygon): boolean {\n\n\t// test if line is inside\n\tif (testPolygonPoint(p, l.p1) || testPolygonPoint(p, l.p2)) {\n\t\treturn true;\n\t}\n\n\t// test each line\n\tfor (let i = 0; i < p.length; i++) {\n\t\tconst p1 = p[i];\n\t\tconst p2 = p[(i + 1) % p.length];\n\t\tif (testLineLine(l, { p1, p2 })) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n\n}\n\nexport function testCirclePoint(c: Circle, p: Point): boolean {\n\treturn c.center.dist(p) < c.radius;\n}\n\nexport function testCircleCircle(c1: Circle, c2: Circle): boolean {\n\treturn c1.center.dist(c2.center) < c1.radius + c2.radius;\n}\n\n// TODO\nexport function testCirclePolygon(c: Circle, p: Polygon): boolean {\n\treturn false;\n}\n\nexport function testPolygonPolygon(p1: Polygon, p2: Polygon): boolean {\n\tfor (let i = 0; i < p1.length; i++) {\n\t\tconst l = {\n\t\t\tp1: p1[i],\n\t\t\tp2: p1[(i + 1) % p1.length],\n\t\t};\n\t\tif (testLinePolygon(l, p2)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html\nexport function testPolygonPoint(p: Polygon, pt: Point): boolean {\n\n\tlet c = false;\n\n\tfor (let i = 0, j = p.length - 1; i < p.length; j = i++) {\n\t\tif (\n\t\t\t((p[i].y > pt.y) != (p[j].y > pt.y))\n\t\t\t&& (pt.x < (p[j].x - p[i].x) * (pt.y - p[i].y) / (p[j].y - p[i].y) + p[i].x)\n\t\t) {\n\t\t\tc = !c;\n\t\t}\n\t}\n\n\treturn c;\n\n}\n\nexport function testPointPoint(p1: Point, p2: Point): boolean {\n\treturn p1.eq(p2);\n}\n\nexport function testAreaRect(a: Area, r: Rect): boolean {\n\tswitch (a.shape) {\n\t\tcase \"rect\": return testRectRect(r, a);\n\t\tcase \"line\": return testRectLine(r, a);\n\t\tcase \"circle\": return testRectCircle(r, a);\n\t\tcase \"polygon\": return testRectPolygon(r, a.pts);\n\t\tcase \"point\": return testRectPoint(r, a.pt);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a as Area).shape}`);\n}\n\nexport function testAreaLine(a: Area, l: Line): boolean {\n\tswitch (a.shape) {\n\t\tcase \"rect\": return testRectLine(a, l);\n\t\tcase \"line\": return Boolean(testLineLine(a, l));\n\t\tcase \"circle\": return testLineCircle(l, a);\n\t\tcase \"polygon\": return testLinePolygon(l, a.pts);\n\t\tcase \"point\": return testLinePoint(l, a.pt);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a as Area).shape}`);\n}\n\nexport function testAreaCircle(a: Area, c: Circle): boolean {\n\tswitch (a.shape) {\n\t\tcase \"rect\": return testRectCircle(a, c);\n\t\tcase \"line\": return testLineCircle(a, c);\n\t\tcase \"circle\": return testCircleCircle(a, c);\n\t\tcase \"polygon\": return testCirclePolygon(c, a.pts);\n\t\tcase \"point\": return testCirclePoint(c, a.pt);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a as Area).shape}`);\n}\n\nexport function testAreaPolygon(a: Area, p: Polygon): boolean {\n\tswitch (a.shape) {\n\t\tcase \"rect\": return testRectPolygon(a, p);\n\t\tcase \"line\": return testLinePolygon(a, p);\n\t\tcase \"circle\": return testCirclePolygon(a, p);\n\t\tcase \"polygon\": return testPolygonPolygon(p, a.pts);\n\t\tcase \"point\": return testPolygonPoint(p, a.pt);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a as Area).shape}`);\n}\n\nexport function testAreaPoint(a: Area, p: Point): boolean {\n\tswitch (a.shape) {\n\t\tcase \"rect\": return testRectPoint(a, p);\n\t\tcase \"line\": return testLinePoint(a, p);\n\t\tcase \"circle\": return testCirclePoint(a, p);\n\t\tcase \"polygon\": return testPolygonPoint(a.pts, p);\n\t\tcase \"point\": return testPointPoint(a.pt, p);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a as Area).shape}`);\n}\n\nexport function testAreaArea(a1: Area, a2: Area): boolean {\n\tswitch (a2.shape) {\n\t\tcase \"rect\": return testAreaRect(a1, a2);\n\t\tcase \"line\": return testAreaLine(a1, a2);\n\t\tcase \"circle\": return testAreaCircle(a1, a2);\n\t\tcase \"polygon\": return testAreaPolygon(a1, a2.pts);\n\t\tcase \"point\": return testAreaPoint(a1, a2.pt);\n\t}\n\tthrow new Error(`Unknown area shape: ${(a2 as Area).shape}`);\n}\n\nexport function minkDiff(r1: Rect, r2: Rect): Rect {\n\treturn {\n\t\tp1: vec2(r1.p1.x - r2.p2.x, r1.p1.y - r2.p2.y),\n\t\tp2: vec2(r1.p2.x - r2.p1.x, r1.p2.y - r2.p1.y),\n\t};\n}\n\nexport class Line {\n\tp1: Vec2;\n\tp2: Vec2;\n\tconstructor(p1: Vec2, p2: Vec2) {\n\t\tthis.p1 = p1;\n\t\tthis.p2 = p2;\n\t}\n}\n\nexport class Rect {\n\tp1: Vec2;\n\tp2: Vec2;\n\tconstructor(p1: Vec2, p2: Vec2) {\n\t\tthis.p1 = p1;\n\t\tthis.p2 = p2;\n\t}\n}\n\nexport class Circle {\n\tcenter: Vec2;\n\tradius: number;\n\tconstructor(center: Vec2, radius: number) {\n\t\tthis.center = center;\n\t\tthis.radius = radius;\n\t}\n}\n", "export default class FPSCounter {\n\n\tprivate buf: number[] = [];\n\tprivate timer: number = 0;\n\tfps: number = 0;\n\n\ttick(dt: number) {\n\n\t\tthis.buf.push(1 / dt);\n\t\tthis.timer += dt;\n\n\t\tif (this.timer >= 1) {\n\t\t\tthis.timer = 0;\n\t\t\tthis.fps = Math.round(this.buf.reduce((a, b) => a + b) / this.buf.length);\n\t\t\tthis.buf = [];\n\t\t}\n\n\t}\n\n}\n", "export default class Timer {\n\n\ttime: number\n\taction: () => void\n\tfinished: boolean = false\n\tpaused: boolean = false\n\n\tconstructor(time: number, action: () => void) {\n\t\tthis.time = time;\n\t\tthis.action = action;\n\t}\n\n\ttick(dt: number): boolean {\n\t\tif (this.finished || this.paused) return false;\n\t\tthis.time -= dt;\n\t\tif (this.time <= 0) {\n\t\t\tthis.action();\n\t\t\tthis.finished = true;\n\t\t\tthis.time = 0;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\treset(time) {\n\t\tthis.time = time;\n\t\tthis.finished = false;\n\t}\n\n}\n", "import {\n\tvec2,\n\tvec3,\n\tVec3,\n\tRect,\n\tLine,\n\tCircle,\n\tColor,\n\tVec2,\n\tMat4,\n\tQuad,\n\tRNG,\n\tquad,\n\trgb,\n\thsl2rgb,\n\trng,\n\trand,\n\trandi,\n\trandSeed,\n\tchance,\n\tchoose,\n\tclamp,\n\tlerp,\n\tmap,\n\tmapc,\n\twave,\n\ttestAreaRect,\n\ttestAreaLine,\n\ttestAreaCircle,\n\ttestAreaPolygon,\n\ttestAreaPoint,\n\ttestAreaArea,\n\ttestLineLineT,\n\ttestRectRect2,\n\ttestLineLine,\n\ttestRectRect,\n\ttestRectLine,\n\ttestRectPoint,\n\ttestPolygonPoint,\n\ttestLinePolygon,\n\ttestPolygonPolygon,\n\ttestCircleCircle,\n\ttestCirclePoint,\n\ttestRectPolygon,\n\tminkDiff,\n\tdeg2rad,\n\trad2deg,\n} from \"./math\";\n\nimport {\n\tIDList,\n\tdownloadURL,\n\tdownloadBlob,\n\tuid,\n\tdeprecate,\n\tdeprecateMsg,\n\tisDataURL,\n\tdeepEq,\n} from \"./utils\";\n\nimport {\n\tGfxShader,\n\tGfxFont,\n\tTexFilter,\n\tRenderProps,\n\tCharTransform,\n\tCharTransformFunc,\n\tTexWrap,\n\tFormattedText,\n\tFormattedChar,\n\tDrawRectOpt,\n\tDrawLineOpt,\n\tDrawLinesOpt,\n\tDrawTriangleOpt,\n\tDrawPolygonOpt,\n\tDrawCircleOpt,\n\tDrawEllipseOpt,\n\tDrawUVQuadOpt,\n\tVertex,\n\tSpriteData,\n\tSoundData,\n\tFontData,\n\tShaderData,\n\tSpriteLoadSrc,\n\tSpriteLoadOpt,\n\tSpriteAtlasData,\n\tFontLoadOpt,\n\tGfxTexData,\n\tKaboomCtx,\n\tKaboomOpt,\n\tAudioPlay,\n\tAudioPlayOpt,\n\tDrawSpriteOpt,\n\tDrawTextOpt,\n\tGameObj,\n\tEventCanceller,\n\tSceneID,\n\tSceneDef,\n\tCompList,\n\tComp,\n\tTag,\n\tKey,\n\tMouseButton,\n\tTouchID,\n\tCollision,\n\tPosComp,\n\tScaleComp,\n\tRotateComp,\n\tColorComp,\n\tOpacityComp,\n\tOrigin,\n\tOriginComp,\n\tLayerComp,\n\tZComp,\n\tFollowComp,\n\tMoveComp,\n\tOutviewCompOpt,\n\tOutviewComp,\n\tCleanupCompOpt,\n\tCleanupComp,\n\tAreaCompOpt,\n\tAreaComp,\n\tArea,\n\tSpriteComp,\n\tSpriteCompOpt,\n\tGfxTexture,\n\tSpriteAnimPlayOpt,\n\tTextComp,\n\tTextCompOpt,\n\tRectComp,\n\tRectCompOpt,\n\tUVQuadComp,\n\tCircleComp,\n\tOutlineComp,\n\tTimerComp,\n\tBodyComp,\n\tBodyCompOpt,\n\tUniform,\n\tShaderComp,\n\tSolidComp,\n\tFixedComp,\n\tStayComp,\n\tHealthComp,\n\tLifespanComp,\n\tLifespanCompOpt,\n\tStateComp,\n\tDebug,\n\tKaboomPlugin,\n\tMergeObj,\n\tLevel,\n\tLevelOpt,\n\tCursor,\n\tRecording,\n\tKaboom,\n} from \"./types\";\n\nimport FPSCounter from \"./fps\";\nimport Timer from \"./timer\";\n\n// @ts-ignore\nimport apl386Src from \"./assets/apl386.png\";\n// @ts-ignore\nimport apl386oSrc from \"./assets/apl386o.png\";\n// @ts-ignore\nimport sinkSrc from \"./assets/sink.png\";\n// @ts-ignore\nimport sinkoSrc from \"./assets/sinko.png\";\n// @ts-ignore\nimport beanSrc from \"./assets/bean.png\";\n// @ts-ignore\nimport burpBytes from \"./assets/burp.mp3\";\n// @ts-ignore\nimport kaSrc from \"./assets/ka.png\";\n// @ts-ignore\nimport boomSrc from \"./assets/boom.png\";\n\ntype ButtonState =\n\t\"up\"\n\t| \"pressed\"\n\t| \"rpressed\"\n\t| \"down\"\n\t| \"released\"\n\t;\n\ntype DrawTextureOpt = RenderProps & {\n\ttex: GfxTexture,\n\twidth?: number,\n\theight?: number,\n\ttiled?: boolean,\n\tflipX?: boolean,\n\tflipY?: boolean,\n\tquad?: Quad,\n\torigin?: Origin | Vec2,\n}\n\ninterface GfxTexOpt {\n\tfilter?: TexFilter,\n\twrap?: TexWrap,\n}\n\n// translate these key names to a simpler version\nconst KEY_ALIAS = {\n\t\"ArrowLeft\": \"left\",\n\t\"ArrowRight\": \"right\",\n\t\"ArrowUp\": \"up\",\n\t\"ArrowDown\": \"down\",\n\t\" \": \"space\",\n};\n\n// according to https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button\nconst MOUSE_BUTTONS = [\n\t\"left\",\n\t\"middle\",\n\t\"right\",\n\t\"back\",\n\t\"forward\",\n];\n\n// don't trigger browser default event when these keys are pressed\nconst PREVENT_DEFAULT_KEYS = [\n\t\"space\",\n\t\"left\",\n\t\"right\",\n\t\"up\",\n\t\"down\",\n\t\"tab\",\n\t\"f1\",\n\t\"f2\",\n\t\"f3\",\n\t\"f4\",\n\t\"f5\",\n\t\"f6\",\n\t\"f7\",\n\t\"f8\",\n\t\"f9\",\n\t\"f10\",\n\t\"f11\",\n\t\"s\",\n];\n\n// some default charsets for loading bitmap fonts\nconst ASCII_CHARS = \" !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\";\nconst CP437_CHARS = \" \u263A\u263B\u2665\u2666\u2663\u2660\u2022\u25D8\u25CB\u25D9\u2642\u2640\u266A\u266B\u263C\u25BA\u25C4\u2195\u203C\u00B6\u00A7\u25AC\u21A8\u2191\u2193\u2192\u2190\u221F\u2194\u25B2\u25BC !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u2302\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\";\n\n// audio gain range\nconst MIN_GAIN = 0;\nconst MAX_GAIN = 3;\n\n// audio speed range\nconst MIN_SPEED = 0;\nconst MAX_SPEED = 3;\n\n// audio detune range\nconst MIN_DETUNE = -1200;\nconst MAX_DETUNE = 1200;\n\nconst DEF_ORIGIN = \"topleft\";\nconst DEF_GRAVITY = 1600;\nconst QUEUE_COUNT = 65536;\nconst BG_GRID_SIZE = 64;\n\nconst DEF_FONT = \"apl386o\";\nconst DBG_FONT = \"sink\";\n\n// vertex format stride (vec3 pos, vec2 uv, vec4 color)\nconst STRIDE = 9;\n\n// vertex shader template, replace {{user}} with user vertex shader code\nconst VERT_TEMPLATE = `\nattribute vec3 a_pos;\nattribute vec2 a_uv;\nattribute vec4 a_color;\n\nvarying vec3 v_pos;\nvarying vec2 v_uv;\nvarying vec4 v_color;\n\nvec4 def_vert() {\n\treturn vec4(a_pos, 1.0);\n}\n\n{{user}}\n\nvoid main() {\n\tvec4 pos = vert(a_pos, a_uv, a_color);\n\tv_pos = a_pos;\n\tv_uv = a_uv;\n\tv_color = a_color;\n\tgl_Position = pos;\n}\n`;\n\n// fragment shader template, replace {{user}} with user fragment shader code\nconst FRAG_TEMPLATE = `\nprecision mediump float;\n\nvarying vec3 v_pos;\nvarying vec2 v_uv;\nvarying vec4 v_color;\n\nuniform sampler2D u_tex;\n\nvec4 def_frag() {\n\treturn v_color * texture2D(u_tex, v_uv);\n}\n\n{{user}}\n\nvoid main() {\n\tgl_FragColor = frag(v_pos, v_uv, v_color, u_tex);\n\tif (gl_FragColor.a == 0.0) {\n\t\tdiscard;\n\t}\n}\n`;\n\n// default {{user}} vertex shader code\nconst DEF_VERT = `\nvec4 vert(vec3 pos, vec2 uv, vec4 color) {\n\treturn def_vert();\n}\n`;\n\n// default {{user}} fragment shader code\nconst DEF_FRAG = `\nvec4 frag(vec3 pos, vec2 uv, vec4 color, sampler2D tex) {\n\treturn def_frag();\n}\n`;\n\nconst COMP_DESC = new Set([\n\t\"id\",\n\t\"require\",\n]);\n\nconst COMP_EVENTS = new Set([\n\t\"add\",\n\t\"load\",\n\t\"update\",\n\t\"draw\",\n\t\"destroy\",\n\t\"inspect\",\n]);\n\n// transform the button state to the next state\n// e.g. a button might become \"pressed\" one frame, and it should become \"down\" next frame\nfunction processButtonState(s: ButtonState): ButtonState {\n\tif (s === \"pressed\" || s === \"rpressed\") {\n\t\treturn \"down\";\n\t}\n\tif (s === \"released\") {\n\t\treturn \"up\";\n\t}\n\treturn s;\n}\n\n// wrappers around full screen functions to work across browsers\nfunction enterFullscreen(el: HTMLElement) {\n\tif (el.requestFullscreen) el.requestFullscreen();\n\t// @ts-ignore\n\telse if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();\n};\n\nfunction exitFullscreen() {\n\tif (document.exitFullscreen) document.exitFullscreen();\n\t// @ts-ignore\n\telse if (document.webkitExitFullScreen) document.webkitExitFullScreen();\n};\n\nfunction getFullscreenElement(): Element | void {\n\treturn document.fullscreenElement\n\t\t// @ts-ignore\n\t\t|| document.webkitFullscreenElement\n\t\t;\n};\n\n// convert origin string to a vec2 offset\nfunction originPt(orig: Origin | Vec2): Vec2 {\n\tswitch (orig) {\n\t\tcase \"topleft\": return vec2(-1, -1);\n\t\tcase \"top\": return vec2(0, -1);\n\t\tcase \"topright\": return vec2(1, -1);\n\t\tcase \"left\": return vec2(-1, 0);\n\t\tcase \"center\": return vec2(0, 0);\n\t\tcase \"right\": return vec2(1, 0);\n\t\tcase \"botleft\": return vec2(-1, 1);\n\t\tcase \"bot\": return vec2(0, 1);\n\t\tcase \"botright\": return vec2(1, 1);\n\t\tdefault: return orig;\n\t}\n}\n\nfunction createEmptyAudioBuffer() {\n\treturn new AudioBuffer({\n\t\tlength: 1,\n\t\tnumberOfChannels: 1,\n\t\tsampleRate: 44100\n\t});\n}\n\n// only exports one kaboom() which contains all the state\nexport default (gopt: KaboomOpt = {}): KaboomCtx => {\n\nconst app = (() => {\n\n\tconst root = gopt.root ?? document.body;\n\n\t// if root is not defined (which falls back to ) we assume user is using kaboom on a clean page, and modify to better fit a full screen canvas\n\tif (root === document.body) {\n\t\tdocument.body.style[\"width\"] = \"100%\";\n\t\tdocument.body.style[\"height\"] = \"100%\";\n\t\tdocument.body.style[\"margin\"] = \"0px\";\n\t\tdocument.documentElement.style[\"width\"] = \"100%\";\n\t\tdocument.documentElement.style[\"height\"] = \"100%\";\n\t}\n\n\t// create a if user didn't provide one\n\tconst canvas = gopt.canvas ?? (() => {\n\t\tconst canvas = document.createElement(\"canvas\");\n\t\troot.appendChild(canvas);\n\t\treturn canvas;\n\t})();\n\n\t// global pixel scale\n\tconst gscale = gopt.scale ?? 1;\n\n\t// adjust canvas size according to user size / viewport settings\n\tif (gopt.width && gopt.height && !gopt.stretch && !gopt.letterbox) {\n\t\tcanvas.width = gopt.width * gscale;\n\t\tcanvas.height = gopt.height * gscale;\n\t} else {\n\t\tcanvas.width = canvas.parentElement.offsetWidth;\n\t\tcanvas.height = canvas.parentElement.offsetHeight;\n\t}\n\n\t// canvas css styles\n\tconst styles = [\n\t\t\"outline: none\",\n\t\t\"cursor: default\",\n\t];\n\n\tif (gopt.crisp) {\n\t\tstyles.push(\"image-rendering: pixelated\");\n\t\tstyles.push(\"image-rendering: crisp-edges\");\n\t}\n\n\t// TODO: .style is supposed to be readonly? alternative?\n\t// @ts-ignore\n\tcanvas.style = styles.join(\";\");\n\n\t// make canvas focusable\n\tcanvas.setAttribute(\"tabindex\", \"0\");\n\n\t// create webgl context\n\tconst gl = canvas\n\t\t.getContext(\"webgl\", {\n\t\t\tantialias: true,\n\t\t\tdepth: true,\n\t\t\tstencil: true,\n\t\t\talpha: true,\n\t\t\tpreserveDrawingBuffer: true,\n\t\t});\n\n\treturn {\n\n\t\tcanvas: canvas,\n\t\tscale: gscale,\n\t\tgl: gl,\n\n\t\t// keep track of all button states\n\t\tkeyStates: {} as Record,\n\t\tmouseStates: {} as Record,\n\n\t\t// input states from last frame, should reset every frame\n\t\tcharInputted: [],\n\t\tisMouseMoved: false,\n\t\tisKeyPressed: false,\n\t\tisKeyPressedRepeat: false,\n\t\tisKeyReleased: false,\n\t\tmousePos: vec2(0, 0),\n\t\tmouseDeltaPos: vec2(0, 0),\n\n\t\t// total time elapsed\n\t\ttime: 0,\n\t\t// real total time elapsed (including paused time)\n\t\trealTime: 0,\n\t\t// if we should skip next dt, to prevent the massive dt surge if user switch to another tab for a while and comeback\n\t\tskipTime: false,\n\t\t// how much time last frame took\n\t\tdt: 0.0,\n\t\t// total frames elapsed\n\t\tnumFrames: 0,\n\n\t\t// if we're on a touch device\n\t\tisTouch: (\"ontouchstart\" in window) || navigator.maxTouchPoints > 0,\n\n\t\t// requestAnimationFrame id\n\t\tloopID: null,\n\t\t// if our game loop is currently stopped / paused\n\t\tstopped: false,\n\t\tpaused: false,\n\n\t\t// TODO: take fps counter out pure\n\t\tfpsCounter: new FPSCounter(),\n\n\t\t// if we finished loading all assets\n\t\tloaded: false,\n\n\t};\n\n})();\n\nconst gfx = (() => {\n\n\tconst gl = app.gl;\n\tconst defShader = makeShader(DEF_VERT, DEF_FRAG);\n\n\t// a 1x1 white texture to draw raw shapes like rectangles and polygons\n\t// we use a texture for those so we can use only 1 pipeline for drawing sprites + shapes\n\tconst emptyTex = makeTex(\n\t\tnew ImageData(new Uint8ClampedArray([ 255, 255, 255, 255, ]), 1, 1)\n\t);\n\n\tconst c = gopt.background ?? rgb(0, 0, 0);\n\n\tif (gopt.background) {\n\t\tconst c = Color.fromArray(gopt.background);\n\t\tgl.clearColor(c.r / 255, c.g / 255, c.b / 255, 1);\n\t}\n\n\tgl.enable(gl.BLEND);\n\tgl.enable(gl.SCISSOR_TEST);\n\tgl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n\t// we only use one vertex and index buffer that batches all draw calls\n\tconst vbuf = gl.createBuffer();\n\n\tgl.bindBuffer(gl.ARRAY_BUFFER, vbuf);\n\t// vec3 pos\n\tgl.vertexAttribPointer(0, 3, gl.FLOAT, false, STRIDE * 4, 0);\n\tgl.enableVertexAttribArray(0);\n\t// vec2 uv\n\tgl.vertexAttribPointer(1, 2, gl.FLOAT, false, STRIDE * 4, 12);\n\tgl.enableVertexAttribArray(1);\n\t// vec4 color\n\tgl.vertexAttribPointer(2, 4, gl.FLOAT, false, STRIDE * 4, 20);\n\tgl.enableVertexAttribArray(2);\n\tgl.bufferData(gl.ARRAY_BUFFER, QUEUE_COUNT * 4, gl.DYNAMIC_DRAW);\n\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\n\tconst ibuf = gl.createBuffer();\n\n\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);\n\tgl.bufferData(gl.ELEMENT_ARRAY_BUFFER, QUEUE_COUNT * 2, gl.DYNAMIC_DRAW);\n\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);\n\n\t// a checkerboard texture used for the default background\n\tconst bgTex = makeTex(\n\t\tnew ImageData(new Uint8ClampedArray([\n\t\t\t128, 128, 128, 255,\n\t\t\t190, 190, 190, 255,\n\t\t\t190, 190, 190, 255,\n\t\t\t128, 128, 128, 255,\n\t\t]), 2, 2), {\n\t\t\twrap: \"repeat\",\n\t\t\tfilter: \"nearest\",\n\t\t},\n\t);\n\n\treturn {\n\n\t\t// keep track of how many draw calls we're doing this frame\n\t\tdrawCalls: 0,\n\t\t// how many draw calls we're doing last frame, this is the number we give to users\n\t\tlastDrawCalls: 0,\n\n\t\t// gfx states\n\t\tdefShader: defShader,\n\t\tcurShader: defShader,\n\t\tdefTex: emptyTex,\n\t\tcurTex: emptyTex,\n\t\tcurUniform: {},\n\t\tvbuf: vbuf,\n\t\tibuf: ibuf,\n\n\t\t// local vertex / index buffer queue\n\t\tvqueue: [],\n\t\tiqueue: [],\n\n\t\ttransform: new Mat4(),\n\t\ttransformStack: [],\n\n\t\tbgTex: bgTex,\n\n\t\twidth: gopt.width,\n\t\theight: gopt.height,\n\n\t\tviewport: {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: gl.drawingBufferWidth,\n\t\t\theight: gl.drawingBufferHeight,\n\t\t},\n\n\t};\n\n})();\n\nupdateViewport();\n\nconst audio = (() => {\n\n\t// TODO: handle when audio context is unavailable\n\tconst ctx = new (window.AudioContext || (window as any).webkitAudioContext)() as AudioContext\n\tconst masterNode = ctx.createGain();\n\tmasterNode.connect(ctx.destination);\n\n\t// by default browsers can only load audio async, we don't deal with that and just start with an empty audio buffer\n\tconst burpSnd = {\n\t\tbuf: createEmptyAudioBuffer(),\n\t};\n\n\t// load that burp sound\n\tctx.decodeAudioData(burpBytes.buffer.slice(0), (buf) => {\n\t\tburpSnd.buf = buf;\n\t}, () => {\n\t\tthrow new Error(\"Failed to load burp.\")\n\t});\n\n\treturn {\n\t\tctx,\n\t\tmasterNode,\n\t\tburpSnd,\n\t};\n\n})();\n\nconst assets = {\n\n\t// keep track of how many assets are loading / loaded, for calculaating progress\n\tnumLoading: 0,\n\tnumLoaded: 0,\n\n\t// prefix for when loading from a url\n\turlPrefix: \"\",\n\n\t// asset holders\n\tsprites: {},\n\tsounds: {},\n\tshaders: {},\n\tfonts: {},\n\n};\n\nconst game = {\n\n\t// event callbacks\n\tevents: {},\n\tobjEvents: {},\n\n\t// root game object\n\t// these transforms are used as camera\n\troot: make([]),\n\n\ttimers: new IDList(),\n\n\t// misc\n\tlayers: {},\n\tdefLayer: null,\n\tgravity: DEF_GRAVITY,\n\ton(ev: string, cb: F): EventCanceller {\n\t\tif (!this.events[ev]) {\n\t\t\tthis.events[ev] = new IDList();\n\t\t}\n\t\treturn this.events[ev].pushd(cb);\n\t},\n\ttrigger(ev: string, ...args) {\n\t\tif (this.events[ev]) {\n\t\t\tthis.events[ev].forEach((cb) => cb(...args));\n\t\t}\n\t},\n\tscenes: {},\n\n\t// on screen log\n\tlogs: [],\n\n\t// camera\n\tcam: {\n\t\tpos: center(),\n\t\tscale: vec2(1),\n\t\tangle: 0,\n\t\tshake: 0,\n\t\ttransform: new Mat4(),\n\t},\n\n}\n\n// wrap individual loaders with global loader counter, for stuff like progress bar\nfunction load(prom: Promise): Promise {\n\n\tassets.numLoading++;\n\n\t// wrapping another layer of promise because we are catching errors here internally and we also want users be able to catch errors, however only one catch is allowed per promise chain\n\treturn new Promise((resolve, reject) => {\n\t\tprom\n\t\t\t.then(resolve)\n\t\t\t.catch((err) => {\n\t\t\t\tdebug.error(err);\n\t\t\t\treject(err);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tassets.numLoading--;\n\t\t\t\tassets.numLoaded++;\n\t\t\t});\n\t}) as Promise;\n\n}\n\n// get current load progress\nfunction loadProgress(): number {\n\treturn assets.numLoaded / (assets.numLoading + assets.numLoaded);\n}\n\n// global load path prefix\nfunction loadRoot(path?: string): string {\n\tif (path !== undefined) {\n\t\tassets.urlPrefix = path;\n\t}\n\treturn assets.urlPrefix;\n}\n\n// wrapper around fetch() that applies urlPrefix and basic error handling\nfunction fetchURL(path: string) {\n\tconst url = assets.urlPrefix + path;\n\treturn fetch(url)\n\t\t.then((res) => {\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`Failed to fetch ${url}`);\n\t\t\t}\n\t\t\treturn res;\n\t\t});\n}\n\n// wrapper around image loader to get a Promise\nfunction loadImg(src: string): Promise {\n\tconst img = new Image();\n\timg.src = isDataURL(src) ? src : assets.urlPrefix + src;\n\timg.crossOrigin = \"anonymous\";\n\treturn new Promise((resolve, reject) => {\n\t\timg.onload = () => resolve(img);\n\t\t// TODO: truncate for long dataurl src\n\t\timg.onerror = () => reject(`Failed to load image from \"${src}\"`);\n\t});\n}\n\n// TODO: support SpriteLoadSrc\nfunction loadFont(\n\tname: string | null,\n\tsrc: string,\n\tgw: number,\n\tgh: number,\n\topt: FontLoadOpt = {},\n): Promise {\n\treturn load(loadImg(src)\n\t\t.then((img) => {\n\t\t\tconst font = makeFont(\n\t\t\t\tmakeTex(img, opt),\n\t\t\t\tgw,\n\t\t\t\tgh,\n\t\t\t\topt.chars ?? ASCII_CHARS\n\t\t\t);\n\t\t\tif (name) {\n\t\t\t\tassets.fonts[name] = font;\n\t\t\t}\n\t\t\treturn font;\n\t\t})\n\t);\n}\n\nfunction getSprite(name: string): SpriteData | null {\n\treturn assets.sprites[name] ?? null;\n}\n\nfunction getSound(name: string): SoundData | null {\n\treturn assets.sounds[name] ?? null;\n}\n\nfunction getFont(name: string): FontData | null {\n\treturn assets.fonts[name] ?? null;\n}\n\nfunction getShader(name: string): ShaderData | null {\n\treturn assets.shaders[name] ?? null;\n}\n\n// get an array of frames based on configuration on how to slice the image\nfunction slice(x = 1, y = 1, dx = 0, dy = 0, w = 1, h = 1): Quad[] {\n\tconst frames = [];\n\tconst qw = w / x;\n\tconst qh = h / y;\n\tfor (let j = 0; j < y; j++) {\n\t\tfor (let i = 0; i < x; i++) {\n\t\t\tframes.push(new Quad(\n\t\t\t\tdx + i * qw,\n\t\t\t\tdy + j * qh,\n\t\t\t\tqw,\n\t\t\t\tqh,\n\t\t\t));\n\t\t}\n\t}\n\treturn frames;\n}\n\nfunction loadSpriteAtlas(\n\tsrc: SpriteLoadSrc,\n\tdata: SpriteAtlasData | string\n): Promise> {\n\tif (typeof data === \"string\") {\n\t\t// TODO: this adds a new loader asyncly\n\t\treturn load(fetchURL(data)\n\t\t\t.then((res) => res.json())\n\t\t\t.then((data2) => loadSpriteAtlas(src, data2)));\n\t}\n\treturn load(loadSprite(null, src).then((atlas) => {\n\t\tconst map = {};\n\t\tconst w = atlas.tex.width;\n\t\tconst h = atlas.tex.height;\n\t\tfor (const name in data) {\n\t\t\tconst info = data[name];\n\t\t\tconst spr = {\n\t\t\t\ttex: atlas.tex,\n\t\t\t\tframes: slice(info.sliceX, info.sliceY, info.x / w, info.y / h, info.width / w, info.height / h),\n\t\t\t\tanims: info.anims,\n\t\t\t}\n\t\t\tassets.sprites[name] = spr;\n\t\t\tmap[name] = spr;\n\t\t}\n\t\treturn map;\n\t}));\n}\n\n// synchronously load sprite from local pixel data\nfunction loadRawSprite(\n\tname: string | null,\n\tsrc: GfxTexData,\n\topt: SpriteLoadOpt = {}\n) {\n\n\tconst tex = makeTex(src, opt);\n\tconst frames = slice(opt.sliceX || 1, opt.sliceY || 1);\n\n\tconst sprite = {\n\t\ttex: tex,\n\t\tframes: frames,\n\t\tanims: opt.anims || {},\n\t};\n\n\tif (name) {\n\t\tassets.sprites[name] = sprite;\n\t}\n\n\treturn sprite;\n\n}\n\n// load a sprite to asset manager\nfunction loadSprite(\n\tname: string | null,\n\tsrc: SpriteLoadSrc,\n\topt: SpriteLoadOpt = {\n\t\tsliceX: 1,\n\t\tsliceY: 1,\n\t\tanims: {},\n\t},\n): Promise {\n\n\treturn load(new Promise((resolve, reject) => {\n\n\t\tif (!src) {\n\t\t\treturn reject(`Expected sprite src for \"${name}\"`);\n\t\t}\n\n\t\t// from url\n\t\tif (typeof(src) === \"string\") {\n\t\t\tloadImg(src)\n\t\t\t\t.then((img) => resolve(loadRawSprite(name, img, opt)))\n\t\t\t\t.catch(reject);\n\t\t} else {\n\t\t\tresolve(loadRawSprite(name, src, opt));\n\t\t}\n\n\t}));\n\n}\n\n// TODO: accept raw json\nfunction loadPedit(name: string, src: string): Promise {\n\n\treturn load(new Promise((resolve, reject) => {\n\n\t\tfetchURL(src)\n\t\t\t.then((res) => res.json())\n\t\t\t.then(async (data) => {\n\n\t\t\t\tconst images = await Promise.all(data.frames.map(loadImg));\n\t\t\t\tconst canvas = document.createElement(\"canvas\");\n\t\t\t\tcanvas.width = data.width;\n\t\t\t\tcanvas.height = data.height * data.frames.length;\n\n\t\t\t\tconst ctx = canvas.getContext(\"2d\");\n\n\t\t\t\timages.forEach((img: HTMLImageElement, i) => {\n\t\t\t\t\tctx.drawImage(img, 0, i * data.height);\n\t\t\t\t});\n\n\t\t\t\treturn loadSprite(name, canvas, {\n\t\t\t\t\tsliceY: data.frames.length,\n\t\t\t\t\tanims: data.anims,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then(resolve)\n\t\t\t.catch(reject)\n\t\t\t;\n\n\t}));\n\n}\n\n// TODO: accept raw json\nfunction loadAseprite(\n\tname: string | null,\n\timgSrc: SpriteLoadSrc,\n\tjsonSrc: string\n): Promise {\n\n\treturn load(new Promise((resolve, reject) => {\n\n\t\tloadSprite(name, imgSrc)\n\t\t\t.then((sprite: SpriteData) => {\n\t\t\t\tfetchURL(jsonSrc)\n\t\t\t\t\t.then((res) => res.json())\n\t\t\t\t\t.then((data) => {\n\t\t\t\t\t\tconst size = data.meta.size;\n\t\t\t\t\t\tsprite.frames = data.frames.map((f: any) => {\n\t\t\t\t\t\t\treturn new Quad(\n\t\t\t\t\t\t\t\tf.frame.x / size.w,\n\t\t\t\t\t\t\t\tf.frame.y / size.h,\n\t\t\t\t\t\t\t\tf.frame.w / size.w,\n\t\t\t\t\t\t\t\tf.frame.h / size.h,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const anim of data.meta.frameTags) {\n\t\t\t\t\t\t\tif (anim.from === anim.to) {\n\t\t\t\t\t\t\t\tsprite.anims[anim.name] = anim.from\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsprite.anims[anim.name] = {\n\t\t\t\t\t\t\t\t\tfrom: anim.from,\n\t\t\t\t\t\t\t\t\tto: anim.to,\n\t\t\t\t\t\t\t\t\t// TODO: let users define these\n\t\t\t\t\t\t\t\t\tspeed: 10,\n\t\t\t\t\t\t\t\t\tloop: true,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve(sprite);\n\t\t\t\t\t})\n\t\t\t\t\t.catch(reject)\n\t\t\t\t\t;\n\t\t\t})\n\t\t\t.catch(reject);\n\n\t}));\n\n}\n\nfunction loadShader(\n\tname: string | null,\n\tvert?: string,\n\tfrag?: string,\n\tisUrl: boolean = false,\n): Promise {\n\n\tfunction loadRawShader(\n\t\tname: string | null,\n\t\tvert: string | null,\n\t\tfrag: string | null,\n\t): ShaderData {\n\t\tconst shader = makeShader(vert, frag);\n\t\tif (name) {\n\t\t\tassets.shaders[name] = shader;\n\t\t}\n\t\treturn shader;\n\t}\n\n\treturn load(new Promise((resolve, reject) => {\n\n\t\tif (!vert && !frag) {\n\t\t\treturn reject(\"no shader\");\n\t\t}\n\n\t\tfunction resolveUrl(url?: string) {\n\t\t\treturn url ?\n\t\t\t\tfetchURL(url)\n\t\t\t\t\t.then((res) => res.text())\n\t\t\t\t\t.catch(reject)\n\t\t\t\t: new Promise((r) => r(null));\n\t\t}\n\n\t\tif (isUrl) {\n\t\t\tPromise.all([resolveUrl(vert), resolveUrl(frag)])\n\t\t\t\t.then(([vcode, fcode]: [string | null, string | null]) => {\n\t\t\t\t\tresolve(loadRawShader(name, vcode, fcode));\n\t\t\t\t})\n\t\t\t\t.catch(reject);\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tresolve(loadRawShader(name, vert, frag));\n\t\t\t} catch (err) {\n\t\t\t\treject(err);\n\t\t\t}\n\t\t}\n\n\t}));\n\n}\n\n// TODO: accept dataurl\n// load a sound to asset manager\nfunction loadSound(\n\tname: string | null,\n\tsrc: string,\n): Promise {\n\n\treturn load(new Promise((resolve, reject) => {\n\n\t\tif (!src) {\n\t\t\treturn reject(`expected sound src for \"${name}\"`);\n\t\t}\n\n\t\t// from url\n\t\tif (typeof(src) === \"string\") {\n\t\t\tfetchURL(src)\n\t\t\t\t.then((res) => res.arrayBuffer())\n\t\t\t\t.then((data) => {\n\t\t\t\t\treturn new Promise((resolve2, reject2) =>\n\t\t\t\t\t\taudio.ctx.decodeAudioData(data, resolve2, reject2)\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.then((buf: AudioBuffer) => {\n\t\t\t\t\tconst snd = {\n\t\t\t\t\t\tbuf: buf,\n\t\t\t\t\t}\n\t\t\t\t\tif (name) {\n\t\t\t\t\t\tassets.sounds[name] = snd;\n\t\t\t\t\t}\n\t\t\t\t\tresolve(snd);\n\t\t\t\t})\n\t\t\t\t.catch(reject);\n\t\t}\n\n\t}));\n\n}\n\nfunction loadBean(name: string = \"bean\"): Promise {\n\treturn loadSprite(name, beanSrc);\n}\n\n// get / set master volume\nfunction volume(v?: number): number {\n\tif (v !== undefined) {\n\t\taudio.masterNode.gain.value = clamp(v, MIN_GAIN, MAX_GAIN);\n\t}\n\treturn audio.masterNode.gain.value;\n}\n\n// plays a sound, returns a control handle\nfunction play(\n\tsrc: SoundData | string,\n\topt: AudioPlayOpt = {\n\t\tloop: false,\n\t\tvolume: 1,\n\t\tspeed: 1,\n\t\tdetune: 0,\n\t\tseek: 0,\n\t},\n): AudioPlay {\n\n\t// TODO: clean?\n\tif (typeof src === \"string\") {\n\n\t\tconst pb = play({\n\t\t\tbuf: createEmptyAudioBuffer(),\n\t\t});\n\n\t\tonLoad(() => {\n\t\t\tconst snd = assets.sounds[src];\n\t\t\tif (!snd) {\n\t\t\t\tthrow new Error(`Sound not found: \"${src}\"`);\n\t\t\t}\n\t\t\tconst pb2 = play(snd, opt);\n\t\t\tfor (const k in pb2) {\n\t\t\t\tpb[k] = pb2[k];\n\t\t\t}\n\t\t});\n\n\t\treturn pb;\n\n\t}\n\n\tconst ctx = audio.ctx;\n\tlet stopped = false;\n\tlet srcNode = ctx.createBufferSource();\n\n\tsrcNode.buffer = src.buf;\n\tsrcNode.loop = opt.loop ? true : false;\n\n\tconst gainNode = ctx.createGain();\n\n\tsrcNode.connect(gainNode);\n\tgainNode.connect(audio.masterNode);\n\n\tconst pos = opt.seek ?? 0;\n\n\tsrcNode.start(0, pos);\n\n\tlet startTime = ctx.currentTime - pos;\n\tlet stopTime: number | null = null;\n\n\tconst handle = {\n\n\t\tstop() {\n\t\t\tif (stopped) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.pause();\n\t\t\tstartTime = ctx.currentTime;\n\t\t},\n\n\t\tplay(seek?: number) {\n\n\t\t\tif (!stopped) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst oldNode = srcNode;\n\n\t\t\tsrcNode = ctx.createBufferSource();\n\t\t\tsrcNode.buffer = oldNode.buffer;\n\t\t\tsrcNode.loop = oldNode.loop;\n\t\t\tsrcNode.playbackRate.value = oldNode.playbackRate.value;\n\n\t\t\tif (srcNode.detune) {\n\t\t\t\tsrcNode.detune.value = oldNode.detune.value;\n\t\t\t}\n\n\t\t\tsrcNode.connect(gainNode);\n\n\t\t\tconst pos = seek ?? this.time();\n\n\t\t\tsrcNode.start(0, pos);\n\t\t\tstartTime = ctx.currentTime - pos;\n\t\t\tstopped = false;\n\t\t\tstopTime = null;\n\n\t\t},\n\n\t\tpause() {\n\t\t\tif (stopped) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsrcNode.stop();\n\t\t\tstopped = true;\n\t\t\tstopTime = ctx.currentTime;\n\t\t},\n\n\t\tisPaused(): boolean {\n\t\t\treturn stopped;\n\t\t},\n\n\t\tpaused(): boolean {\n\t\t\tdeprecateMsg(\"paused()\", \"isPaused()\");\n\t\t\treturn this.isPaused();\n\t\t},\n\n\t\tisStopped(): boolean {\n\t\t\treturn stopped;\n\t\t},\n\n\t\tstopped(): boolean {\n\t\t\tdeprecateMsg(\"stopped()\", \"isStopped()\");\n\t\t\treturn this.isStopped();\n\t\t},\n\n\t\t// TODO: affect time()\n\t\tspeed(val?: number): number {\n\t\t\tif (val !== undefined) {\n\t\t\t\tsrcNode.playbackRate.value = clamp(val, MIN_SPEED, MAX_SPEED);\n\t\t\t}\n\t\t\treturn srcNode.playbackRate.value;\n\t\t},\n\n\t\tdetune(val?: number): number {\n\t\t\tif (!srcNode.detune) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tif (val !== undefined) {\n\t\t\t\tsrcNode.detune.value = clamp(val, MIN_DETUNE, MAX_DETUNE);\n\t\t\t}\n\t\t\treturn srcNode.detune.value;\n\t\t},\n\n\t\tvolume(val?: number): number {\n\t\t\tif (val !== undefined) {\n\t\t\t\tgainNode.gain.value = clamp(val, MIN_GAIN, MAX_GAIN);\n\t\t\t}\n\t\t\treturn gainNode.gain.value;\n\t\t},\n\n\t\tloop() {\n\t\t\tsrcNode.loop = true;\n\t\t},\n\n\t\tunloop() {\n\t\t\tsrcNode.loop = false;\n\t\t},\n\n\t\tduration(): number {\n\t\t\treturn src.buf.duration;\n\t\t},\n\n\t\ttime(): number {\n\t\t\tif (stopped) {\n\t\t\t\treturn stopTime - startTime;\n\t\t\t} else {\n\t\t\t\treturn ctx.currentTime - startTime;\n\t\t\t}\n\t\t},\n\n\t};\n\n\thandle.speed(opt.speed);\n\thandle.detune(opt.detune);\n\thandle.volume(opt.volume);\n\n\treturn handle;\n\n}\n\n// core kaboom logic\nfunction burp(opt?: AudioPlayOpt): AudioPlay {\n\treturn play(audio.burpSnd, opt);\n}\n\n// TODO: take these webgl structures out pure\nfunction makeTex(\n\tdata: GfxTexData,\n\topt: GfxTexOpt = {}\n): GfxTexture {\n\n\tconst gl = app.gl;\n\tconst id = gl.createTexture();\n\n\tgl.bindTexture(gl.TEXTURE_2D, id);\n\tgl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n\n\tconst filter = (() => {\n\t\tswitch (opt.filter ?? gopt.texFilter) {\n\t\t\tcase \"linear\": return gl.LINEAR;\n\t\t\tcase \"nearest\": return gl.NEAREST;\n\t\t\tdefault: return gl.NEAREST;\n\t\t}\n\t})();\n\n\tconst wrap = (() => {\n\t\tswitch (opt.wrap) {\n\t\t\tcase \"repeat\": return gl.REPEAT;\n\t\t\tcase \"clampToEdge\": return gl.CLAMP_TO_EDGE;\n\t\t\tdefault: return gl.CLAMP_TO_EDGE;\n\t\t}\n\t})();\n\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);\n\tgl.bindTexture(gl.TEXTURE_2D, null);\n\n\treturn {\n\t\twidth: data.width,\n\t\theight: data.height,\n\t\tbind() {\n\t\t\tgl.bindTexture(gl.TEXTURE_2D, id);\n\t\t},\n\t\tunbind() {\n\t\t\tgl.bindTexture(gl.TEXTURE_2D, null);\n\t\t},\n\t};\n\n}\n\nfunction makeShader(\n\tvertSrc: string | null = DEF_VERT,\n\tfragSrc: string | null = DEF_FRAG,\n): GfxShader {\n\n\tconst gl = app.gl;\n\tlet msg;\n\tconst vcode = VERT_TEMPLATE.replace(\"{{user}}\", vertSrc ?? DEF_VERT);\n\tconst fcode = FRAG_TEMPLATE.replace(\"{{user}}\", fragSrc ?? DEF_FRAG);\n\tconst vertShader = gl.createShader(gl.VERTEX_SHADER);\n\tconst fragShader = gl.createShader(gl.FRAGMENT_SHADER);\n\n\tgl.shaderSource(vertShader, vcode);\n\tgl.shaderSource(fragShader, fcode);\n\tgl.compileShader(vertShader);\n\tgl.compileShader(fragShader);\n\n\tif ((msg = gl.getShaderInfoLog(vertShader))) {\n\t\tthrow new Error(msg);\n\t}\n\n\tif ((msg = gl.getShaderInfoLog(fragShader))) {\n\t\tthrow new Error(msg);\n\t}\n\n\tconst id = gl.createProgram();\n\n\tgl.attachShader(id, vertShader);\n\tgl.attachShader(id, fragShader);\n\n\tgl.bindAttribLocation(id, 0, \"a_pos\");\n\tgl.bindAttribLocation(id, 1, \"a_uv\");\n\tgl.bindAttribLocation(id, 2, \"a_color\");\n\n\tgl.linkProgram(id);\n\n\tif ((msg = gl.getProgramInfoLog(id))) {\n\t\t// for some reason on safari it always has a \"\\n\" msg\n\t\tif (msg !== \"\\n\") {\n\t\t\tthrow new Error(msg);\n\t\t}\n\t}\n\n\treturn {\n\n\t\tbind() {\n\t\t\tgl.useProgram(id);\n\t\t},\n\n\t\tunbind() {\n\t\t\tgl.useProgram(null);\n\t\t},\n\n\t\tsend(uniform: Uniform) {\n\t\t\tthis.bind();\n\t\t\tfor (const name in uniform) {\n\t\t\t\tconst val = uniform[name];\n\t\t\t\tconst loc = gl.getUniformLocation(id, name);\n\t\t\t\tif (typeof val === \"number\") {\n\t\t\t\t\tgl.uniform1f(loc, val);\n\t\t\t\t} else if (val instanceof Mat4) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\tgl.uniformMatrix4fv(loc, false, new Float32Array(val.m));\n\t\t\t\t} else if (val instanceof Color) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\tgl.uniform4f(loc, val.r, val.g, val.b, val.a);\n\t\t\t\t} else if (val instanceof Vec3) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\tgl.uniform3f(loc, val.x, val.y, val.z);\n\t\t\t\t} else if (val instanceof Vec2) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\tgl.uniform2f(loc, val.x, val.y);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.unbind();\n\t\t},\n\n\t};\n\n}\n\nfunction makeFont(\n\ttex: GfxTexture,\n\tgw: number,\n\tgh: number,\n\tchars: string,\n): GfxFont {\n\n\tconst cols = tex.width / gw;\n\tconst rows = tex.height / gh;\n\tconst qw = 1.0 / cols;\n\tconst qh = 1.0 / rows;\n\tconst map: Record = {};\n\tconst charMap = chars.split(\"\").entries();\n\n\tfor (const [i, ch] of charMap) {\n\t\tmap[ch] = vec2(\n\t\t\t(i % cols) * qw,\n\t\t\tMath.floor(i / cols) * qh,\n\t\t);\n\t}\n\n\treturn {\n\t\ttex: tex,\n\t\tmap: map,\n\t\tqw: qw,\n\t\tqh: qh,\n\t};\n\n}\n\n// TODO: expose\nfunction drawRaw(\n\tverts: Vertex[],\n\tindices: number[],\n\tfixed: boolean,\n\ttex: GfxTexture = gfx.defTex,\n\tshader: GfxShader = gfx.defShader,\n\tuniform: Uniform = {},\n) {\n\n\ttex = tex ?? gfx.defTex;\n\tshader = shader ?? gfx.defShader;\n\n\t// flush on texture / shader change and overflow\n\tif (\n\t\ttex !== gfx.curTex\n\t\t|| shader !== gfx.curShader\n\t\t|| !deepEq(gfx.curUniform, uniform)\n\t\t|| gfx.vqueue.length + verts.length * STRIDE > QUEUE_COUNT\n\t\t|| gfx.iqueue.length + indices.length > QUEUE_COUNT\n\t) {\n\t\tflush();\n\t}\n\n\tfor (const v of verts) {\n\n\t\t// TODO: cache camTransform * gfxTransform?\n\t\tconst transform = fixed ? gfx.transform : game.cam.transform.mult(gfx.transform);\n\n\t\t// normalized world space coordinate [-1.0 ~ 1.0]\n\t\tconst pt = screen2ndc(transform.multVec2(v.pos.xy()));\n\n\t\tgfx.vqueue.push(\n\t\t\tpt.x, pt.y, v.pos.z,\n\t\t\tv.uv.x, v.uv.y,\n\t\t\tv.color.r / 255, v.color.g / 255, v.color.b / 255, v.opacity,\n\t\t);\n\n\t}\n\n\tfor (const i of indices) {\n\t\tgfx.iqueue.push(i + gfx.vqueue.length / STRIDE - verts.length);\n\t}\n\n\tgfx.curTex = tex;\n\tgfx.curShader = shader;\n\tgfx.curUniform = uniform;\n\n}\n\n// draw all batched shapes\nfunction flush() {\n\n\tif (\n\t\t!gfx.curTex\n\t\t|| !gfx.curShader\n\t\t|| gfx.vqueue.length === 0\n\t\t|| gfx.iqueue.length === 0\n\t) {\n\t\treturn;\n\t}\n\n\tconst gl = app.gl;\n\n\tgfx.curShader.send(gfx.curUniform);\n\tgl.bindBuffer(gl.ARRAY_BUFFER, gfx.vbuf);\n\tgl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(gfx.vqueue));\n\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gfx.ibuf);\n\tgl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint16Array(gfx.iqueue));\n\tgfx.curShader.bind();\n\tgfx.curTex.bind();\n\tgl.drawElements(gl.TRIANGLES, gfx.iqueue.length, gl.UNSIGNED_SHORT, 0);\n\tgfx.curTex.unbind();\n\tgfx.curShader.unbind();\n\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);\n\n\tgfx.iqueue = [];\n\tgfx.vqueue = [];\n\n\tgfx.drawCalls++;\n\n}\n\n// start a rendering frame, reset some states\nfunction frameStart() {\n\n\tapp.gl.clear(app.gl.COLOR_BUFFER_BIT);\n\n\tif (!gopt.background) {\n\t\tdrawUVQuad({\n\t\t\twidth: width(),\n\t\t\theight: height(),\n\t\t\tquad: new Quad(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\twidth() * app.scale / BG_GRID_SIZE,\n\t\t\t\theight() * app.scale / BG_GRID_SIZE,\n\t\t\t),\n\t\t\ttex: gfx.bgTex,\n\t\t\tfixed: true,\n\t\t})\n\t}\n\n\tgfx.drawCalls = 0;\n\tgfx.transformStack = [];\n\tgfx.transform = new Mat4();\n\n}\n\nfunction frameEnd() {\n\tflush();\n\tgfx.lastDrawCalls = gfx.drawCalls;\n}\n\nfunction drawCalls() {\n\treturn gfx.lastDrawCalls;\n}\n\n// convert a screen space coordinate to webgl normalized device coordinate\nfunction screen2ndc(pt: Vec2): Vec2 {\n\treturn vec2(\n\t\tpt.x / width() * 2 - 1,\n\t\t-pt.y / height() * 2 + 1,\n\t);\n}\n\n// convert a webgl normalied device coordinate to screen space coordinate\nfunction ndc2screen(pt: Vec2): Vec2 {\n\treturn vec2(\n\t\t(pt.x + 1) / 2 * width(),\n\t\t-(pt.y - 1) / 2 * height(),\n\t);\n}\n\nfunction applyMatrix(m: Mat4) {\n\tgfx.transform = m.clone();\n}\n\nfunction pushTranslate(...args) {\n\tif (args[0] === undefined) return;\n\tconst p = vec2(...args);\n\tif (p.x === 0 && p.y === 0) return;\n\tgfx.transform = gfx.transform.translate(p);\n}\n\nfunction pushScale(...args) {\n\tif (args[0] === undefined) return;\n\tconst p = vec2(...args);\n\tif (p.x === 1 && p.y === 1) return;\n\tgfx.transform = gfx.transform.scale(p);\n}\n\nfunction pushRotateX(a: number) {\n\tif (!a) {\n\t\treturn;\n\t}\n\tgfx.transform = gfx.transform.rotateX(a);\n}\n\nfunction pushRotateY(a: number) {\n\tif (!a) {\n\t\treturn;\n\t}\n\tgfx.transform = gfx.transform.rotateY(a);\n}\n\nfunction pushRotateZ(a: number) {\n\tif (!a) {\n\t\treturn;\n\t}\n\tgfx.transform = gfx.transform.rotateZ(a);\n}\n\nfunction pushTransform() {\n\tgfx.transformStack.push(gfx.transform.clone());\n}\n\nfunction popTransform() {\n\tif (gfx.transformStack.length > 0) {\n\t\tgfx.transform = gfx.transformStack.pop();\n\t}\n}\n\n// draw a uv textured quad\nfunction drawUVQuad(opt: DrawUVQuadOpt) {\n\n\tif (opt.width === undefined || opt.height === undefined) {\n\t\tthrow new Error(\"drawUVQuad() requires property \\\"width\\\" and \\\"height\\\".\");\n\t}\n\n\tif (opt.width <= 0 || opt.height <= 0) {\n\t\treturn;\n\t}\n\n\tconst w = opt.width;\n\tconst h = opt.height;\n\tconst origin = originPt(opt.origin || DEF_ORIGIN);\n\tconst offset = origin.scale(vec2(w, h).scale(-0.5));\n\tconst q = opt.quad || new Quad(0, 0, 1, 1);\n\tconst color = opt.color || rgb(255, 255, 255);\n\tconst opacity = opt.opacity ?? 1;\n\n\tpushTransform();\n\tpushTranslate(opt.pos);\n\tpushRotateZ(opt.angle);\n\tpushScale(opt.scale);\n\tpushTranslate(offset);\n\n\tdrawRaw([\n\t\t{\n\t\t\tpos: vec3(-w / 2, h / 2, 0),\n\t\t\tuv: vec2(opt.flipX ? q.x + q.w : q.x, opt.flipY ? q.y : q.y + q.h),\n\t\t\tcolor: color,\n\t\t\topacity: opacity,\n\t\t},\n\t\t{\n\t\t\tpos: vec3(-w / 2, -h / 2, 0),\n\t\t\tuv: vec2(opt.flipX ? q.x + q.w : q.x, opt.flipY ? q.y + q.h : q.y),\n\t\t\tcolor: color,\n\t\t\topacity: opacity,\n\t\t},\n\t\t{\n\t\t\tpos: vec3(w / 2, -h / 2, 0),\n\t\t\tuv: vec2(opt.flipX ? q.x : q.x + q.w, opt.flipY ? q.y + q.h : q.y),\n\t\t\tcolor: color,\n\t\t\topacity: opacity,\n\t\t},\n\t\t{\n\t\t\tpos: vec3(w / 2, h / 2, 0),\n\t\t\tuv: vec2(opt.flipX ? q.x : q.x + q.w, opt.flipY ? q.y : q.y + q.h),\n\t\t\tcolor: color,\n\t\t\topacity: opacity,\n\t\t},\n\t], [0, 1, 3, 1, 2, 3], opt.fixed, opt.tex, opt.shader, opt.uniform);\n\n\tpopTransform();\n\n}\n\n// TODO: clean\nfunction drawTexture(opt: DrawTextureOpt) {\n\n\tif (!opt.tex) {\n\t\tthrow new Error(\"drawTexture() requires property \\\"tex\\\".\");\n\t}\n\n\tconst q = opt.quad ?? new Quad(0, 0, 1, 1);\n\tconst w = opt.tex.width * q.w;\n\tconst h = opt.tex.height * q.h;\n\tconst scale = vec2(1);\n\n\tif (opt.tiled) {\n\n\t\t// TODO: draw fract\n\t\tconst repX = Math.ceil((opt.width || w) / w);\n\t\tconst repY = Math.ceil((opt.height || h) / h);\n\t\tconst origin = originPt(opt.origin || DEF_ORIGIN).add(vec2(1, 1)).scale(0.5);\n\t\tconst offset = origin.scale(repX * w, repY * h);\n\n\t\t// TODO: rotation\n\t\tfor (let i = 0; i < repX; i++) {\n\t\t\tfor (let j = 0; j < repY; j++) {\n\t\t\t\tdrawUVQuad({\n\t\t\t\t\t...opt,\n\t\t\t\t\tpos: (opt.pos || vec2(0)).add(vec2(w * i, h * j)).sub(offset),\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\tscale: scale.scale(opt.scale || vec2(1)),\n\t\t\t\t\ttex: opt.tex,\n\t\t\t\t\tquad: q,\n\t\t\t\t\twidth: w,\n\t\t\t\t\theight: h,\n\t\t\t\t\torigin: \"topleft\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else {\n\n\t\t// TODO: should this ignore scale?\n\t\tif (opt.width && opt.height) {\n\t\t\tscale.x = opt.width / w;\n\t\t\tscale.y = opt.height / h;\n\t\t} else if (opt.width) {\n\t\t\tscale.x = opt.width / w;\n\t\t\tscale.y = scale.x;\n\t\t} else if (opt.height) {\n\t\t\tscale.y = opt.height / h;\n\t\t\tscale.x = scale.y;\n\t\t}\n\n\t\tdrawUVQuad({\n\t\t\t...opt,\n\t\t\t// @ts-ignore\n\t\t\tscale: scale.scale(opt.scale || vec2(1)),\n\t\t\ttex: opt.tex,\n\t\t\tquad: q,\n\t\t\twidth: w,\n\t\t\theight: h,\n\t\t});\n\n\t}\n\n}\n\n// TODO: use native asset loader tracking\nconst loading = new Set();\n\nfunction drawSprite(opt: DrawSpriteOpt) {\n\n\tif (!opt.sprite) {\n\t\tthrow new Error(`drawSprite() requires property \"sprite\"`);\n\t}\n\n\tconst spr = findAsset(opt.sprite, assets.sprites);\n\n\tif (!spr) {\n\n\t\t// if passes a source url, we load it implicitly\n\t\tif (typeof opt.sprite === \"string\") {\n\t\t\tif (!loading.has(opt.sprite)) {\n\t\t\t\tloading.add(opt.sprite);\n\t\t\t\tloadSprite(opt.sprite, opt.sprite)\n\t\t\t\t\t.then((a) => loading.delete(opt.sprite));\n\t\t\t}\n\t\t\treturn;\n\t\t} else {\n\t\t\tthrow new Error(`sprite not found: \"${opt.sprite}\"`);\n\t\t}\n\n\t}\n\n\tconst q = spr.frames[opt.frame ?? 0];\n\n\tif (!q) {\n\t\tthrow new Error(`frame not found: ${opt.frame ?? 0}`);\n\t}\n\n\tdrawTexture({\n\t\t...opt,\n\t\ttex: spr.tex,\n\t\tquad: q.scale(opt.quad || new Quad(0, 0, 1, 1)),\n\t\tuniform: opt.uniform,\n\t});\n\n}\n\n// generate vertices to form an arc\nfunction getArcPts(\n\tpos: Vec2,\n\tradiusX: number,\n\tradiusY: number,\n\tstart: number,\n\tend: number,\n\tres: number = 1,\n): Vec2[] {\n\n\t// normalize and turn start and end angles to radians\n\tstart = deg2rad(start % 360);\n\tend = deg2rad(end % 360);\n\tif (end <= start) end += Math.PI * 2;\n\n\t// TODO: better way to get this?\n\t// the number of vertices is sqrt(r1 + r2) * 3 * res with a minimum of 16\n\tconst nverts = Math.ceil(Math.max(Math.sqrt(radiusX + radiusY) * 3 * (res || 1), 16));\n\tconst step = (end - start) / nverts;\n\tconst pts = [];\n\n\t// calculate vertices\n\tfor (let a = start; a < end; a += step) {\n\t\tpts.push(pos.add(radiusX * Math.cos(a), radiusY * Math.sin(a)));\n\t}\n\n\t// doing this on the side due to possible floating point inaccuracy\n\tpts.push(pos.add(radiusX * Math.cos(end), radiusY * Math.sin(end)));\n\n\treturn pts;\n\n}\n\nfunction drawRect(opt: DrawRectOpt) {\n\n\tif (opt.width === undefined || opt.height === undefined) {\n\t\tthrow new Error(\"drawRect() requires property \\\"width\\\" and \\\"height\\\".\");\n\t}\n\n\tif (opt.width <= 0 || opt.height <= 0) {\n\t\treturn;\n\t}\n\n\tconst w = opt.width;\n\tconst h = opt.height;\n\tconst origin = originPt(opt.origin || DEF_ORIGIN).add(1, 1);\n\tconst offset = origin.scale(vec2(w, h).scale(-0.5));\n\n\tlet pts = [\n\t\tvec2(0, 0),\n\t\tvec2(w, 0),\n\t\tvec2(w, h),\n\t\tvec2(0, h),\n\t];\n\n\t// TODO: drawPolygon should handle generic rounded corners\n\tif (opt.radius) {\n\n\t\t// maxium radius is half the shortest side\n\t\tconst r = Math.min(Math.min(w, h) / 2, opt.radius);\n\n\t\tpts = [\n\t\t\tvec2(r, 0),\n\t\t\tvec2(w - r, 0),\n\t\t\t...getArcPts(vec2(w - r, r), r, r, 270, 360),\n\t\t\tvec2(w, r),\n\t\t\tvec2(w, h - r),\n\t\t\t...getArcPts(vec2(w - r, h - r), r, r, 0, 90),\n\t\t\tvec2(w - r, h),\n\t\t\tvec2(r, h),\n\t\t\t...getArcPts(vec2(r, h - r), r, r, 90, 180),\n\t\t\tvec2(0, h - r),\n\t\t\tvec2(0, r),\n\t\t\t...getArcPts(vec2(r, r), r, r, 180, 270),\n\t\t];\n\n\t}\n\n\tdrawPolygon({ ...opt, offset, pts });\n\n}\n\nfunction drawLine(opt: DrawLineOpt) {\n\n\tconst { p1, p2 } = opt;\n\n\tif (!p1 || !p2) {\n\t\tthrow new Error(\"drawLine() requires properties \\\"p1\\\" and \\\"p2\\\".\");\n\t}\n\n\tconst w = opt.width || 1;\n\n\t// the displacement from the line end point to the corner point\n\tconst dis = p2.sub(p1).unit().normal().scale(w * 0.5);\n\n\t// calculate the 4 corner points of the line polygon\n\tconst verts = [\n\t\tp1.sub(dis),\n\t\tp1.add(dis),\n\t\tp2.add(dis),\n\t\tp2.sub(dis),\n\t].map((p) => ({\n\t\tpos: vec3(p.x, p.y, 0),\n\t\tuv: vec2(0),\n\t\tcolor: opt.color ?? Color.WHITE,\n\t\topacity: opt.opacity ?? 1,\n\t}));\n\n\tdrawRaw(verts, [0, 1, 3, 1, 2, 3], opt.fixed, gfx.defTex, opt.shader, opt.uniform);\n\n}\n\nfunction drawLines(opt: DrawLinesOpt) {\n\n\tconst pts = opt.pts;\n\n\tif (!pts) {\n\t\tthrow new Error(\"drawLines() requires property \\\"pts\\\".\");\n\t}\n\n\tif (pts.length < 2) {\n\t\treturn;\n\t}\n\n\tif (opt.radius && pts.length >= 3) {\n\n\t\t// TODO: rounded vertices for arbitury polygonic shape\n\t\tlet minLen = pts[0].dist(pts[1]);\n\n\t\tfor (let i = 1; i < pts.length - 1; i++) {\n\t\t\tminLen = Math.min(pts[i].dist(pts[i + 1]), minLen);\n\t\t}\n\n\t\tconst radius = Math.min(opt.radius, minLen / 2);\n\n\t\tdrawLine({ ...opt, p1: pts[0], p2: pts[1], });\n\n\t\tfor (let i = 1; i < pts.length - 2; i++) {\n\t\t\tconst p1 = pts[i];\n\t\t\tconst p2 = pts[i + 1];\n\t\t\tdrawLine({\n\t\t\t\t...opt,\n\t\t\t\tp1: p1,\n\t\t\t\tp2: p2,\n\t\t\t});\n\t\t}\n\n\t\tdrawLine({ ...opt, p1: pts[pts.length - 2], p2: pts[pts.length - 1], });\n\n\t} else {\n\n\t\tfor (let i = 0; i < pts.length - 1; i++) {\n\t\t\tdrawLine({\n\t\t\t\t...opt,\n\t\t\t\tp1: pts[i],\n\t\t\t\tp2: pts[i + 1],\n\t\t\t});\n\t\t}\n\n\t}\n\n}\n\nfunction drawTriangle(opt: DrawTriangleOpt) {\n\tif (!opt.p1 || !opt.p2 || !opt.p3) {\n\t\tthrow new Error(\"drawPolygon() requires properties \\\"p1\\\", \\\"p2\\\" and \\\"p3\\\".\");\n\t}\n\treturn drawPolygon({\n\t\t...opt,\n\t\tpts: [opt.p1, opt.p2, opt.p3],\n\t});\n}\n\n// TODO: origin\nfunction drawCircle(opt: DrawCircleOpt) {\n\n\tif (!opt.radius) {\n\t\tthrow new Error(\"drawCircle() requires property \\\"radius\\\".\");\n\t}\n\n\tif (opt.radius === 0) {\n\t\treturn;\n\t}\n\n\tdrawEllipse({\n\t\t...opt,\n\t\tradiusX: opt.radius,\n\t\tradiusY: opt.radius,\n\t\tangle: 0,\n\t});\n\n}\n\n// TODO: use fan-like triangulation\nfunction drawEllipse(opt: DrawEllipseOpt) {\n\n\tif (opt.radiusX === undefined || opt.radiusY === undefined) {\n\t\tthrow new Error(\"drawEllipse() requires properties \\\"radiusX\\\" and \\\"radiusY\\\".\");\n\t}\n\n\tif (opt.radiusX === 0 || opt.radiusY === 0) {\n\t\treturn;\n\t}\n\n\tdrawPolygon({\n\t\t...opt,\n\t\tpts: getArcPts(\n\t\t\tvec2(0),\n\t\t\topt.radiusX,\n\t\t\topt.radiusY,\n\t\t\topt.start ?? 0,\n\t\t\topt.end ?? 360,\n\t\t\topt.resolution\n\t\t),\n\t\tradius: 0,\n\t});\n\n}\n\nfunction drawPolygon(opt: DrawPolygonOpt) {\n\n\tif (!opt.pts) {\n\t\tthrow new Error(\"drawPolygon() requires property \\\"pts\\\".\");\n\t}\n\n\tconst npts = opt.pts.length;\n\n\tif (npts < 3) {\n\t\treturn;\n\t}\n\n\tpushTransform();\n\tpushTranslate(opt.pos);\n\tpushScale(opt.scale);\n\tpushRotateZ(opt.angle);\n\tpushTranslate(opt.offset);\n\n\tif (opt.fill !== false) {\n\n\t\tconst color = opt.color ?? Color.WHITE;\n\n\t\tconst verts = opt.pts.map((pt) => ({\n\t\t\tpos: vec3(pt.x, pt.y, 0),\n\t\t\tuv: vec2(0, 0),\n\t\t\tcolor: color,\n\t\t\topacity: opt.opacity ?? 1,\n\t\t}));\n\n\t\t// TODO: better triangulation\n\t\tconst indices = [...Array(npts - 2).keys()]\n\t\t\t.map((n) => [0, n + 1, n + 2])\n\t\t\t.flat();\n\n\t\tdrawRaw(verts, opt.indices ?? indices, opt.fixed, gfx.defTex, opt.shader, opt.uniform);\n\n\t}\n\n\tif (opt.outline) {\n\t\tdrawLines({\n\t\t\tpts: [ ...opt.pts, opt.pts[0] ],\n\t\t\tradius: opt.radius,\n\t\t\twidth: opt.outline.width,\n\t\t\tcolor: opt.outline.color,\n\t\t\tuniform: opt.uniform,\n\t\t\tfixed: opt.fixed,\n\t\t});\n\t}\n\n\tpopTransform();\n\n}\n\nfunction applyCharTransform(fchar: FormattedChar, tr: CharTransform) {\n\tif (tr.pos) fchar.pos = fchar.pos.add(tr.pos);\n\tif (tr.scale) fchar.scale = fchar.scale.scale(vec2(tr.scale));\n\tif (tr.angle) fchar.angle += tr.angle;\n\tif (tr.color) fchar.color = fchar.color.mult(tr.color);\n\tif (tr.opacity) fchar.opacity *= tr.opacity;\n}\n\n// TODO: escape\nconst TEXT_STYLE_RE = /\\[(?[^\\]]*)\\]\\.(?