class Block { /* blockData: { "block_id": "1", "type": "image", "position": {"x": 0, "y": 0}, "size": {"x": 100, "y": 200}, "image_id": "hedgehog.jpg" } */ constructor(mountPoint, blockData, onSelected = () => { }) { this.blockData = blockData; this.onSelected = onSelected this.mountPoint = mountPoint this.selected = false; this.div = document.createElement('div') this.div.classList.add('boards_display__block') this.onSelectedDiv = document.createElement('div') this.div.appendChild(this.onSelectedDiv) if (blockData.type == "image") { this.internal = document.createElement("img") let thumb = thumbnails[blockData.image_id] let fullImage = `/api/v1/images/${blockData.image_id}` this.internal.src = thumb || fullImage if (thumb) { this.internal.addEventListener('load', () => this.internal.src = `/api/v1/images/${blockData.image_id}`) } this.internal.draggable = "false" this.internal.classList.add('boards_display__block_content') this.div.appendChild(this.internal) } else if (blockData.type == "text") { this.internal = document.createElement('div') this.internal.classList.add('boards_display__block_content') this.internal.classList.add('boards_display__block_text') this.div.appendChild(this.internal) this.setText(blockData.text || "") } this.div.style.cursor = 'pointer'; this.onClick = (e) => { if (e.button !== 0) return e.stopPropagation() if (!this.selected) { this.makeSelected() return } } this.onDblClick = (e) => { if (e.button !== 0) return const aabb = calculateAABB([this]) globalTransform.boundAABB(aabb) } this.onMouseMove = (e) => { if (this.blockIsMoving) { this.moveBlock(e.movementX, e.movementY) } } this.onMouseDown = (e) => { if (e.button !== 0) return this.blockIsMoving = true } this.onMouseUp = (e) => { if (e.button !== 0) return this.blockIsMoving = false } this.onTransformed = () => this.applyTransform() this.div.addEventListener('click', this.onClick) this.div.addEventListener('dblclick', this.onDblClick) document.addEventListener('transformed', this.onTransformed) this.applyTransform() mountPoint.appendChild(this.div) } destroy() { this.div.remove() this.makeUnselected() this.div.removeEventListener('click', this.onClick) this.div.removeEventListener('dblclick', this.onDblClick) document.removeEventListener('transformed', this.onTransformed) this.dispatchBoardChangedEvent() } dispatchBoardChangedEvent() { const event = new CustomEvent('boardchanged', { 'detail': { } }) document.dispatchEvent(event) } saveBounds() { this.backup = { position: { ...this.blockData.position }, size: { ...this.blockData.size }, } } restoreBounds() { this.blockData.position = { ...this.backup.position } this.blockData.size = { ...this.backup.size } } fixAspect() { if (this.blockData.type !== 'image') return const aspect = this.internal.naturalWidth / this.internal.naturalHeight const s = this.blockData.size.y * this.blockData.size.x const cX = this.blockData.position.x + this.blockData.size.x / 2 const cY = this.blockData.position.y + this.blockData.size.y / 2 this.blockData.size.x = Math.sqrt(aspect * s) this.blockData.size.y = this.blockData.size.x / aspect this.blockData.position.x = cX - this.blockData.size.x / 2 this.blockData.position.y = cY - this.blockData.size.y / 2 this.applyTransform() } setText(text) { if (this.blockData.type !== 'text') return this.blockData.text = text this.internal.innerText = this.blockData.text this.dispatchBoardChangedEvent() } getBlockData() { return this.blockData } applyTransform() { const transform = globalTransform.applyTransform(this.blockData.position, this.blockData.size) this.div.style.left = toPixels(transform.x) this.div.style.top = toPixels(transform.y) this.div.style.width = toPixels(transform.w) this.div.style.height = toPixels(transform.h) if (this.blockData.type === 'text') { this.internal.style.fontSize = `${globalTransform.scale * 14}px` } this.dispatchBoardChangedEvent() } moveBlock(dx, dy) { if (dx !== 0) { this.blockData.position.x += globalTransform.applyScale(dx) } if (dy !== 0) { this.blockData.position.y += globalTransform.applyScale(dy) } if (dx !== 0 || dy !== 0) { this.applyTransform() } } scaleBlock(scales, proportional) { if (proportional) scales = this.applyProportionalScale2(scales) else { if (scales.left) scales.left = -scales.left if (scales.up) scales.up = -scales.up } if (scales.left) this.scaleBlockLeft(scales.left) if (scales.right) this.scaleBlockRight(scales.right) if (scales.up) this.scaleBlockUp(scales.up) if (scales.down) this.scaleBlockDown(scales.down) this.applyTransform() } applyProportionalScale({ left, right, up, down }) { let x = right || -left let y = down || -up const aspect = this.blockData.size.x / this.blockData.size.y const sameSign = x * y > 0; const aspectGreater = x / y > aspect === x > 0 if (sameSign == aspectGreater) y = x / aspect else x = y * aspect return { left: left ? x : undefined, right: right ? x : undefined, up: up ? y : undefined, down: down ? y : undefined, } } applyProportionalScale2({ left, right, up, down }) { const w = this.blockData.size.x const h = this.blockData.size.y let x0 = right || -left || 0 let y0 = down || -up || 0 let x = w + x0 let y = h + y0 const distance = Math.sqrt((x0 === 0 ? 0 : (x * x)) + (y0 === 0 ? 0 : (y * y))) const defaultDistance = Math.sqrt((x0 === 0 ? 0 : (w * w)) + (y0 === 0 ? 0 : (h * h))) const scale = distance / defaultDistance - 1 const result = {} if (left) result.left = w * scale else if (right) result.right = w * scale else { result.left = w * scale / 2 result.right = w * scale / 2 } if (up) result.up = h * scale else if (down) result.down = h * scale else { result.up = h * scale / 2 result.down = h * scale / 2 } return result } scaleBlockLeft(dx) { this.blockData.size.x += globalTransform.applyScale(dx) this.moveBlock(-dx, 0) } scaleBlockUp(dy) { this.blockData.size.y += globalTransform.applyScale(dy) this.moveBlock(0, -dy) } scaleBlockRight(dx) { this.blockData.size.x += globalTransform.applyScale(dx) } scaleBlockDown(dy) { this.blockData.size.y += globalTransform.applyScale(dy) } setSelected(value) { if (value) this.makeSelected() else this.makeUnselected() } makeSelected() { if (this.selected) return this.selected = true document.addEventListener('mousemove', this.onMouseMove) this.div.addEventListener("mousedown", this.onMouseDown) document.addEventListener("mouseup", this.onMouseUp) this.mountPoint.lastChild.after(this.div) this.div.style.outline = "2px solid cyan" this.div.style.cursor = 'grab' this.grabbingPoints = [] const pos = ['start', 'middle', 'end'] for (let x of pos) { for (let y of pos) { if (x === 'middle' && y === 'middle') { continue } this.grabbingPoints.push(new OnSelectedDiv(this.onSelectedDiv, this, x, y)) } } const event = new CustomEvent('blockselected', { 'detail': { block: this } }) document.dispatchEvent(event) } makeUnselected() { if (!this.selected) return this.selected = false document.removeEventListener('mousemove', this.onMouseMove) this.div.removeEventListener("mousedown", this.onMouseDown) document.removeEventListener("mouseup", this.onMouseUp) const event = new CustomEvent('blockunselected', { 'detail': { block: this } }) document.dispatchEvent(event) this.div.style.outline = "none" this.div.style.cursor = 'pointer' for (const point of this.grabbingPoints) { point.destroy() } this.onSelectedDiv.innerHTML = "" } } const cursorMap = { "start": { "start": "nwse-resize", "middle": "ew-resize", "end": "nesw-resize", }, "middle": { "start": "ns-resize", "middle": "grab", "end": "ns-resize", }, "end": { "start": "nesw-resize", "middle": "ew-resize", "end": "nwse-resize", }, } class OnSelectedDiv { constructor(mountPoint, block, locationX, locationY) { this.div = document.createElement('div') this.div.classList.add('boards_display__block_handle') const cursor = cursorMap[locationX][locationY] this.div.style.cursor = cursor if (locationX === 'start') { this.div.style.left = '-7px' } else if (locationX === 'middle') { this.div.style.left = '50%' this.div.style.marginLeft = '-7px' } else if (locationX === 'end') { this.div.style.right = '-7px' } if (locationY === 'start') { this.div.style.top = '-7px' } else if (locationY === 'middle') { this.div.style.top = '50%' this.div.style.marginTop = '-7px' } else if (locationY === 'end') { this.div.style.bottom = '-7px' } let down = false let x let y this.onMouseDown = (e) => { if (e.button !== 0) return e.stopPropagation() x = e.x y = e.y document.body.style.cursor = cursor block.saveBounds() down = true } const scalerX = { "start": "left", "middle": "", "end": "right", }[locationX] const scalerY = { "start": "up", "middle": "", "end": "down", }[locationY] this.onMouseMove = (e) => { if (!down) return e.stopPropagation() block.restoreBounds() const scaler = {} scaler[scalerX] = e.x - x scaler[scalerY] = e.y - y block.scaleBlock(scaler, !e.shiftKey) } this.onMouseUp = (e) => { if (e.button !== 0) return e.stopPropagation() document.body.style.cursor = "auto" down = false } this.div.addEventListener('mousedown', this.onMouseDown) document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mouseup', this.onMouseUp) mountPoint.appendChild(this.div) } destroy() { this.div.removeEventListener('mousedown', this.onMouseDown) document.removeEventListener('mousemove', this.onMouseMove) document.removeEventListener('mouseup', this.onMouseUp) this.div.remove() } }