class Transform { constructor() { this.display = document.body this.x = 0; this.y = 0; this.scale = 1; } movePixels(dx, dy) { this.x += dx / this.scale; this.y += dy / this.scale; if (dx !== 0 || dy !== 0) { const event = new Event("transformed"); document.dispatchEvent(event); } } addScale(ds, mouseX, mouseY) { if (this.scale < 0.2 && ds < 0) return const ratioX = (mouseX / this.display.offsetWidth) * 2 - 1 const ratioY = (mouseY / this.display.offsetHeight) * 2 - 1 const dx = -(this.display.clientWidth / this.scale - this.display.clientWidth / (this.scale + ds)) / 2; const dy = -(this.display.clientHeight / this.scale - this.display.clientHeight / (this.scale + ds)) / 2; this.x += dx * (1 + ratioX) this.y += dy * (1 + ratioY) this.scale += ds if (ds !== 0) { const event = new Event("transformed"); document.dispatchEvent(event); } } applyScale(number) { return number / this.scale } applyTransform(pos, size) { return { x: (pos.x + this.x) * this.scale, y: (pos.y + this.y) * this.scale, w: size.x * this.scale, h: size.y * this.scale, } } centerAt(x, y) { this.x = this.display.offsetWidth / 2 / this.scale - x this.y = this.display.offsetHeight / 2 / this.scale - y } boundAABB(aabb) { if (!aabb) return const { x0, y0, x1, y1 } = aabb const myRatio = this.display.offsetWidth / this.display.offsetHeight const newRatio = (x1 - x0) / (y1 - y0) this.scale = myRatio > newRatio ? this.display.offsetHeight / (y1 - y0) : this.display.offsetWidth / (x1 - x0); const centerX = (x1 + x0) / 2 const centerY = (y1 + y0) / 2 this.centerAt(centerX, centerY) const event = new Event("transformed"); document.dispatchEvent(event); } } function toPixels(size) { return `${size}px` } function getRandomInt(max) { return Math.floor(Math.random() * max); } function generateRandomId() { const symbols = "0123456789" let id = "" for (let i = 0; i < 10; i++) id += symbols[getRandomInt(symbols.length)] return id } const URLObj = window.URL || window.webkitURL; async function imageSize(imageBlob) { return new Promise((resolve, reject) => { let img = new Image() img.onload = () => resolve({ y: img.height, x: img.width, }) img.onerror = reject img.src = URLObj.createObjectURL(imageBlob); }) } function calculateAABB(blocks) { let x0 = Infinity let x1 = -Infinity let y0 = Infinity let y1 = -Infinity if (blocks.length === 0) { return null } for (let block of blocks) { let newX0 = block.blockData.position.x let newX1 = block.blockData.position.x + block.blockData.size.x let newY0 = block.blockData.position.y let newY1 = block.blockData.position.y + block.blockData.size.y if (newX0 < x0) x0 = newX0; if (newX1 > x1) x1 = newX1; if (newY0 < y0) y0 = newY0; if (newY1 > y1) y1 = newY1; } return { x0: x0 - 10, x1: x1 + 10, y0: y0 - 10, y1: y1 + 10, } } async function bytesToBase64DataUrl(bytes, type) { return await new Promise((resolve, reject) => { const reader = Object.assign(new FileReader(), { onload: () => resolve(reader.result), onerror: () => reject(reader.error), }); reader.readAsDataURL(new File([bytes], "", { type })); }); }