Spaces:
Sleeping
Sleeping
| const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max) | |
| function setAlpha(rgbColor: string, alpha: number) { | |
| if (rgbColor.startsWith('rgba')) { | |
| return rgbColor.replace(/[\d.]+$/, alpha.toString()); | |
| } | |
| const matches = rgbColor.match(/\d+/g); | |
| if (!matches || matches.length !== 3) { | |
| return `rgba(50, 50, 50, ${alpha})`; | |
| } | |
| const [r, g, b] = matches; | |
| return `rgba(${r}, ${g}, ${b}, ${alpha})`; | |
| } | |
| export default class Box { | |
| label: string; | |
| xmin: number; | |
| ymin: number; | |
| xmax: number; | |
| ymax: number; | |
| color: string; | |
| alpha: number; | |
| isDragging: boolean; | |
| isResizing: boolean; | |
| isSelected: boolean; | |
| isCreating: boolean; | |
| offsetMouseX: number; | |
| offsetMouseY: number; | |
| resizeHandleSize: number; | |
| resizingHandleIndex: number; | |
| minSize: number; | |
| renderCallBack: () => void; | |
| onFinishCreation: () => void; | |
| canvasXmin: number; | |
| canvasYmin: number; | |
| canvasXmax: number; | |
| canvasYmax: number; | |
| scaleFactor: number; | |
| thickness: number; | |
| selectedThickness: number; | |
| creatingAnchorX: string; | |
| creatingAnchorY: string; | |
| resizeHandles: { | |
| xmin: number; | |
| ymin: number; | |
| xmax: number; | |
| ymax: number; | |
| cursor: string; | |
| }[]; | |
| constructor( | |
| renderCallBack: () => void, | |
| onFinishCreation: () => void, | |
| canvasXmin: number, | |
| canvasYmin: number, | |
| canvasXmax: number, | |
| canvasYmax: number, | |
| label: string, | |
| xmin: number, | |
| ymin: number, | |
| xmax: number, | |
| ymax: number, | |
| color: string = "rgb(255, 255, 255)", | |
| alpha: number = 0.5, | |
| minSize: number = 25, | |
| handleSize: number = 8, | |
| thickness: number = 2, | |
| selectedThickness: number = 4, | |
| scaleFactor: number = 1, | |
| ) { | |
| this.renderCallBack = renderCallBack; | |
| this.onFinishCreation = onFinishCreation; | |
| this.canvasXmin = canvasXmin; | |
| this.canvasYmin = canvasYmin; | |
| this.canvasXmax = canvasXmax; | |
| this.canvasYmax = canvasYmax; | |
| this.scaleFactor = scaleFactor; | |
| this.label = label; | |
| this.isDragging = false; | |
| this.isCreating = false; | |
| this.xmin = xmin; | |
| this.ymin = ymin; | |
| this.xmax = xmax; | |
| this.ymax = ymax; | |
| this.isResizing = false; | |
| this.isSelected = false; | |
| this.offsetMouseX = 0; | |
| this.offsetMouseY = 0; | |
| this.resizeHandleSize = handleSize; | |
| this.thickness = thickness; | |
| this.selectedThickness = selectedThickness; | |
| this.updateHandles(); | |
| this.resizingHandleIndex = -1; | |
| this.minSize = minSize; | |
| this.color = color; | |
| this.alpha = alpha; | |
| this.creatingAnchorX = "xmin"; | |
| this.creatingAnchorY = "ymin"; | |
| } | |
| toJSON() { | |
| return { | |
| label: this.label, | |
| xmin: this.xmin, | |
| ymin: this.ymin, | |
| xmax: this.xmax, | |
| ymax: this.ymax, | |
| color: this.color, | |
| scaleFactor: this.scaleFactor, | |
| }; | |
| } | |
| setSelected(selected: boolean): void{ | |
| this.isSelected = selected; | |
| } | |
| setScaleFactor(scaleFactor: number) { | |
| let scale = scaleFactor / this.scaleFactor; | |
| this.xmin = Math.round(this.xmin * scale); | |
| this.ymin = Math.round(this.ymin * scale); | |
| this.xmax = Math.round(this.xmax * scale); | |
| this.ymax = Math.round(this.ymax * scale); | |
| this.updateHandles(); | |
| this.scaleFactor = scaleFactor; | |
| } | |
| updateHandles(): void { | |
| const halfSize = this.resizeHandleSize / 2; | |
| const width = this.getWidth(); | |
| const height = this.getHeight(); | |
| this.resizeHandles = [ | |
| { | |
| // Top left | |
| xmin: this.xmin - halfSize, | |
| ymin: this.ymin - halfSize, | |
| xmax: this.xmin + halfSize, | |
| ymax: this.ymin + halfSize, | |
| cursor: "nwse-resize", | |
| }, | |
| { | |
| // Top right | |
| xmin: this.xmax - halfSize, | |
| ymin: this.ymin - halfSize, | |
| xmax: this.xmax + halfSize, | |
| ymax: this.ymin + halfSize, | |
| cursor: "nesw-resize", | |
| }, | |
| { | |
| // Bottom right | |
| xmin: this.xmax - halfSize, | |
| ymin: this.ymax - halfSize, | |
| xmax: this.xmax + halfSize, | |
| ymax: this.ymax + halfSize, | |
| cursor: "nwse-resize", | |
| }, | |
| { | |
| // Bottom left | |
| xmin: this.xmin - halfSize, | |
| ymin: this.ymax - halfSize, | |
| xmax: this.xmin + halfSize, | |
| ymax: this.ymax + halfSize, | |
| cursor: "nesw-resize", | |
| }, | |
| { | |
| // Top center | |
| xmin: this.xmin + (width / 2) - halfSize, | |
| ymin: this.ymin - halfSize, | |
| xmax: this.xmin + (width / 2) + halfSize, | |
| ymax: this.ymin + halfSize, | |
| cursor: "ns-resize", | |
| }, | |
| { | |
| // Right center | |
| xmin: this.xmax - halfSize, | |
| ymin: this.ymin + (height / 2) - halfSize, | |
| xmax: this.xmax + halfSize, | |
| ymax: this.ymin + (height / 2) + halfSize, | |
| cursor: "ew-resize", | |
| }, | |
| { | |
| // Bottom center | |
| xmin: this.xmin + (width / 2) - halfSize, | |
| ymin: this.ymax - halfSize, | |
| xmax: this.xmin + (width / 2) + halfSize, | |
| ymax: this.ymax + halfSize, | |
| cursor: "ns-resize", | |
| }, | |
| { | |
| // Left center | |
| xmin: this.xmin - halfSize, | |
| ymin: this.ymin + (height / 2) - halfSize, | |
| xmax: this.xmin + halfSize, | |
| ymax: this.ymin + (height / 2) + halfSize, | |
| cursor: "ew-resize", | |
| }, | |
| ]; | |
| } | |
| getWidth(): number { | |
| return this.xmax - this.xmin; | |
| } | |
| getHeight(): number { | |
| return this.ymax - this.ymin; | |
| } | |
| getArea(): number { | |
| return this.getWidth() * this.getHeight(); | |
| } | |
| toCanvasCoordinates(x: number, y: number): [number, number] { | |
| x = x + this.canvasXmin; | |
| y = y + this.canvasYmin; | |
| return [x, y]; | |
| } | |
| toBoxCoordinates(x: number, y: number): [number, number] { | |
| x = x - this.canvasXmin; | |
| y = y - this.canvasYmin; | |
| return [x, y]; | |
| } | |
| render(ctx: CanvasRenderingContext2D): void { | |
| let xmin: number, ymin: number; | |
| // Render the box and border | |
| ctx.beginPath(); | |
| [xmin, ymin] = this.toCanvasCoordinates(this.xmin, this.ymin); | |
| ctx.rect(xmin, ymin, this.getWidth(), this.getHeight()); | |
| ctx.fillStyle = setAlpha(this.color, this.alpha); | |
| ctx.fill(); | |
| if (this.isSelected) { | |
| ctx.lineWidth = this.selectedThickness; | |
| } else { | |
| ctx.lineWidth = this.thickness; | |
| } | |
| ctx.strokeStyle = setAlpha(this.color, 1); | |
| ctx.stroke(); | |
| ctx.closePath(); | |
| // Render the label and background | |
| if (this.label !== null && this.label.trim() !== ""){ | |
| if (this.isSelected) { | |
| ctx.font = "bold 14px Arial"; | |
| } else { | |
| ctx.font = "12px Arial"; | |
| } | |
| const labelWidth = ctx.measureText(this.label).width + 10; | |
| const labelHeight = 20; | |
| let labelX = this.xmin; | |
| let labelY = this.ymin - labelHeight; | |
| ctx.fillStyle = "white"; | |
| [labelX, labelY] = this.toCanvasCoordinates(labelX, labelY); | |
| ctx.fillRect(labelX, labelY, labelWidth, labelHeight); | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = "black"; | |
| ctx.strokeRect(labelX, labelY, labelWidth, labelHeight); | |
| ctx.fillStyle = "black"; | |
| ctx.fillText(this.label, labelX + 5, labelY + 15); | |
| } | |
| // Render the handles | |
| ctx.fillStyle = setAlpha(this.color, 1); | |
| for (const handle of this.resizeHandles) { | |
| [xmin, ymin] = this.toCanvasCoordinates(handle.xmin, handle.ymin); | |
| ctx.fillRect( | |
| xmin, | |
| ymin, | |
| handle.xmax - handle.xmin, | |
| handle.ymax - handle.ymin, | |
| ); | |
| } | |
| } | |
| startDrag(event: MouseEvent): void { | |
| this.isDragging = true; | |
| this.offsetMouseX = event.clientX - this.xmin; | |
| this.offsetMouseY = event.clientY - this.ymin; | |
| document.addEventListener("pointermove", this.handleDrag); | |
| document.addEventListener("pointerup", this.stopDrag); | |
| } | |
| stopDrag = (): void => { | |
| this.isDragging = false; | |
| document.removeEventListener("pointermove", this.handleDrag); | |
| document.removeEventListener("pointerup", this.stopDrag); | |
| }; | |
| handleDrag = (event: MouseEvent): void => { | |
| if (this.isDragging) { | |
| let deltaX = event.clientX - this.offsetMouseX - this.xmin; | |
| let deltaY = event.clientY - this.offsetMouseY - this.ymin; | |
| const canvasW = this.canvasXmax - this.canvasXmin; | |
| const canvasH = this.canvasYmax - this.canvasYmin; | |
| deltaX = clamp(deltaX, -this.xmin, canvasW-this.xmax); | |
| deltaY = clamp(deltaY, -this.ymin, canvasH-this.ymax); | |
| this.xmin += deltaX; | |
| this.ymin += deltaY; | |
| this.xmax += deltaX; | |
| this.ymax += deltaY; | |
| this.updateHandles(); | |
| this.renderCallBack(); | |
| } | |
| }; | |
| isPointInsideBox(x: number, y: number): boolean { | |
| [x, y] = this.toBoxCoordinates(x, y); | |
| return ( | |
| x >= this.xmin && | |
| x <= this.xmax && | |
| y >= this.ymin && | |
| y <= this.ymax | |
| ); | |
| } | |
| indexOfPointInsideHandle(x: number, y: number): number { | |
| [x, y] = this.toBoxCoordinates(x, y); | |
| for (let i = 0; i < this.resizeHandles.length; i++) { | |
| const handle = this.resizeHandles[i]; | |
| if ( | |
| x >= handle.xmin && | |
| x <= handle.xmax && | |
| y >= handle.ymin && | |
| y <= handle.ymax | |
| ) { | |
| this.resizingHandleIndex = i; | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| startCreating(event: MouseEvent, canvasX: number, canvasY: number): void { | |
| this.isCreating = true; | |
| this.offsetMouseX = canvasX; | |
| this.offsetMouseY = canvasY; | |
| document.addEventListener("pointermove", this.handleCreating); | |
| document.addEventListener("pointerup", this.stopCreating); | |
| } | |
| handleCreating = (event: MouseEvent): void => { | |
| if (this.isCreating) { | |
| let [x, y] = this.toBoxCoordinates(event.clientX, event.clientY); | |
| x -= this.offsetMouseX; | |
| y -= this.offsetMouseY; | |
| if (x > this.xmax) { | |
| if (this.creatingAnchorX == "xmax") { | |
| this.xmin = this.xmax; | |
| } | |
| this.xmax = x; | |
| this.creatingAnchorX = "xmin"; | |
| } else if (x > this.xmin && x < this.xmax && this.creatingAnchorX == "xmin") { | |
| this.xmax = x; | |
| } else if (x > this.xmin && x < this.xmax && this.creatingAnchorX == "xmax") { | |
| this.xmin = x; | |
| } else if (x < this.xmin) { | |
| if (this.creatingAnchorX == "xmin") { | |
| this.xmax = this.xmin; | |
| } | |
| this.xmin = x; | |
| this.creatingAnchorX = "xmax"; | |
| } | |
| if (y > this.ymax) { | |
| if (this.creatingAnchorY == "ymax") { | |
| this.ymin = this.ymax; | |
| } | |
| this.ymax = y; | |
| this.creatingAnchorY = "ymin"; | |
| } else if (y > this.ymin && y < this.ymax && this.creatingAnchorY == "ymin") { | |
| this.ymax = y; | |
| } else if (y > this.ymin && y < this.ymax && this.creatingAnchorY == "ymax") { | |
| this.ymin = y; | |
| } else if (y < this.ymin) { | |
| if (this.creatingAnchorY == "ymin") { | |
| this.ymax = this.ymin; | |
| } | |
| this.ymin = y; | |
| this.creatingAnchorY = "ymax"; | |
| } | |
| this.updateHandles(); | |
| this.renderCallBack(); | |
| } | |
| } | |
| stopCreating = (event: MouseEvent): void => { | |
| this.isCreating = false; | |
| document.removeEventListener("pointermove", this.handleCreating); | |
| document.removeEventListener("pointerup", this.stopCreating); | |
| if (this.getArea() > 0) { | |
| const canvasW = this.canvasXmax - this.canvasXmin; | |
| const canvasH = this.canvasYmax - this.canvasYmin; | |
| this.xmin = clamp(this.xmin, 0, canvasW - this.minSize); | |
| this.ymin = clamp(this.ymin, 0, canvasH - this.minSize); | |
| this.xmax = clamp(this.xmax, this.minSize, canvasW); | |
| this.ymax = clamp(this.ymax, this.minSize, canvasH); | |
| if (this.minSize > 0) { | |
| if (this.getWidth() < this.minSize) { | |
| if (this.creatingAnchorX == "xmin") { | |
| this.xmax = this.xmin + this.minSize; | |
| } else { | |
| this.xmin = this.xmax - this.minSize; | |
| } | |
| } | |
| if (this.getHeight() < this.minSize) { | |
| if (this.creatingAnchorY == "ymin") { | |
| this.ymax = this.ymin + this.minSize; | |
| } else { | |
| this.ymin = this.ymax - this.minSize; | |
| } | |
| } | |
| if (this.xmax > canvasW) { | |
| this.xmin -= this.xmax - canvasW; | |
| this.xmax = canvasW; | |
| } else if (this.xmin < 0) { | |
| this.xmax -= this.xmin; | |
| this.xmin = 0; | |
| } | |
| if (this.ymax > canvasH) { | |
| this.ymin -= this.ymax - canvasH; | |
| this.ymax = canvasH; | |
| } else if (this.ymin < 0) { | |
| this.ymax -= this.ymin; | |
| this.ymin = 0; | |
| } | |
| } | |
| this.updateHandles(); | |
| this.renderCallBack(); | |
| } | |
| this.onFinishCreation(); | |
| } | |
| startResize(handleIndex: number, event: MouseEvent): void { | |
| this.resizingHandleIndex = handleIndex; | |
| this.isResizing = true; | |
| this.offsetMouseX = event.clientX - this.resizeHandles[handleIndex].xmin; | |
| this.offsetMouseY = event.clientY - this.resizeHandles[handleIndex].ymin; | |
| document.addEventListener("pointermove", this.handleResize); | |
| document.addEventListener("pointerup", this.stopResize); | |
| } | |
| handleResize = (event: MouseEvent): void => { | |
| if (this.isResizing) { | |
| const mouseX = event.clientX; | |
| const mouseY = event.clientY; | |
| const deltaX = mouseX - this.resizeHandles[this.resizingHandleIndex].xmin - this.offsetMouseX; | |
| const deltaY = mouseY - this.resizeHandles[this.resizingHandleIndex].ymin - this.offsetMouseY; | |
| const canvasW = this.canvasXmax - this.canvasXmin; | |
| const canvasH = this.canvasYmax - this.canvasYmin; | |
| switch (this.resizingHandleIndex) { | |
| case 0: // Top-left handle | |
| this.xmin += deltaX; | |
| this.ymin += deltaY; | |
| this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize); | |
| this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize); | |
| break; | |
| case 1: // Top-right handle | |
| this.xmax += deltaX; | |
| this.ymin += deltaY; | |
| this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW); | |
| this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize); | |
| break; | |
| case 2: // Bottom-right handle | |
| this.xmax += deltaX; | |
| this.ymax += deltaY; | |
| this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW); | |
| this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH); | |
| break; | |
| case 3: // Bottom-left handle | |
| this.xmin += deltaX; | |
| this.ymax += deltaY; | |
| this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize); | |
| this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH); | |
| break; | |
| case 4: // Top center handle | |
| this.ymin += deltaY; | |
| this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize); | |
| break; | |
| case 5: // Right center handle | |
| this.xmax += deltaX; | |
| this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW); | |
| break; | |
| case 6: // Bottom center handle | |
| this.ymax += deltaY; | |
| this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH); | |
| break; | |
| case 7: // Left center handle | |
| this.xmin += deltaX; | |
| this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize); | |
| break; | |
| } | |
| // Update the resize handles | |
| this.updateHandles(); | |
| this.renderCallBack(); | |
| } | |
| }; | |
| stopResize = (): void => { | |
| this.isResizing = false; | |
| document.removeEventListener("pointermove", this.handleResize); | |
| document.removeEventListener("pointerup", this.stopResize); | |
| }; | |
| } | |