const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const dropZone = document.getElementById("drop-zone"); dropZone.addEventListener("dragenter", (e) => e.preventDefault()); dropZone.addEventListener("dragover", (e) => e.preventDefault()); dropZone.addEventListener("drop", (e) => { const reader = new FileReader(); reader.addEventListener("loadend", async () => { const data = JSON.parse(reader.result); await run(data); }); reader.readAsText(e.dataTransfer.files[0]); e.preventDefault(); }); async function run({ models, scene }) { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const { model, transforms } of scene) { let m = Matrix.identity(); for (const transform of transforms) { switch (transform.type) { case "scale": m = m.scale(transform.factor[0], transform.factor[1]); break; case "rotate": m = m.rotate(transform.angle); break; case "translate": m = m.translate(transform.vector[0], transform.vector[1]); break; } } m = m.screen(0, 0, canvas.width, canvas.height); for (const line of models[model]) { const p1 = m.multiply(Matrix.vector(...line[0], 1)); ctx.moveTo(p1.vector[0], p1.vector[1]); const p2 = m.multiply(Matrix.vector(...line[1], 1)); ctx.lineTo(p2.vector[0], p2.vector[1]); } ctx.stroke(); } } class Matrix extends Array { /** * @param {number[][]} matrix */ constructor(matrix) { super(); this.push(...matrix); } static identity() { return new Matrix([ [1, 0, 0], [0, 1, 0], [0, 0, 1], ]); } /** * @param {number} vector */ static vector(...vector) { const matrix = Array(vector.length); for (let i = 0; i < vector.length; i++) { matrix[i] = [vector[i]]; } return new Matrix(matrix); } get width() { if (this.length === 0) { return 0; } return this[0].length; } get height() { return this.length; } get vector() { const v = []; for (const l of this) { v.push(l[0]); } return v; } toString() { let str = ""; for (const line of this) { str += line.join(", ") + "\n"; } return str; } /** * @param {Matrix} matrix */ multiply(matrix) { if (this.width !== matrix.height) { throw Error("Can not multiply"); } const m = Array.from({ length: this.height }, () => Array.from( { length: matrix.width }, () => 0, ), ); for (let i = 0; i < m.length; i++) { for (let j = 0; j < m[0].length; j++) { let sum = 0; for (let k = 0; k < this.width; k++) { sum += this[i][k] * matrix[k][j]; } m[i][j] = sum; } } return new Matrix(m); } /** * @param {number} x * @param {number} y */ scale(x, y) { return new Matrix([ [x, 0, 0], [0, y, 0], [0, 0, 1], ]).multiply(this); } /** * @param {number} angle */ rotate(angle) { return new Matrix([ [Math.cos(angle), -Math.sin(angle), 0], [Math.sin(angle), Math.cos(angle), 0], [0, 0, 1], ]).multiply(this); } translate(x, y) { return new Matrix([ [1, 0, x], [0, 1, y], [0, 0, 1], ]).multiply(this); } /** * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 */ screen(x0, y0, x1, y1) { return new Matrix([ [(x1 - x0) / 2, 0, (x0 + x1) / 2], [0, -(y1 - y0) / 2, (y0 + y1) / 2], [0, 0, 1], ]).multiply(this); } }