| 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 = [ |
| { |
| |
| xmin: this.xmin - halfSize, |
| ymin: this.ymin - halfSize, |
| xmax: this.xmin + halfSize, |
| ymax: this.ymin + halfSize, |
| cursor: "nwse-resize", |
| }, |
| { |
| |
| xmin: this.xmax - halfSize, |
| ymin: this.ymin - halfSize, |
| xmax: this.xmax + halfSize, |
| ymax: this.ymin + halfSize, |
| cursor: "nesw-resize", |
| }, |
| { |
| |
| xmin: this.xmax - halfSize, |
| ymin: this.ymax - halfSize, |
| xmax: this.xmax + halfSize, |
| ymax: this.ymax + halfSize, |
| cursor: "nwse-resize", |
| }, |
| { |
| |
| xmin: this.xmin - halfSize, |
| ymin: this.ymax - halfSize, |
| xmax: this.xmin + halfSize, |
| ymax: this.ymax + halfSize, |
| cursor: "nesw-resize", |
| }, |
| { |
| |
| xmin: this.xmin + (width / 2) - halfSize, |
| ymin: this.ymin - halfSize, |
| xmax: this.xmin + (width / 2) + halfSize, |
| ymax: this.ymin + halfSize, |
| cursor: "ns-resize", |
| }, |
| { |
| |
| xmin: this.xmax - halfSize, |
| ymin: this.ymin + (height / 2) - halfSize, |
| xmax: this.xmax + halfSize, |
| ymax: this.ymin + (height / 2) + halfSize, |
| cursor: "ew-resize", |
| }, |
| { |
| |
| xmin: this.xmin + (width / 2) - halfSize, |
| ymin: this.ymax - halfSize, |
| xmax: this.xmin + (width / 2) + halfSize, |
| ymax: this.ymax + halfSize, |
| cursor: "ns-resize", |
| }, |
| { |
| |
| 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; |
|
|
| |
| 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(); |
|
|
| |
| 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); |
| } |
|
|
| |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| this.ymin += deltaY; |
| this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize); |
| break; |
| case 5: |
| this.xmax += deltaX; |
| this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW); |
| break; |
| case 6: |
| this.ymax += deltaY; |
| this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH); |
| break; |
| case 7: |
| this.xmin += deltaX; |
| this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize); |
| break; |
| } |
| |
| this.updateHandles(); |
| this.renderCallBack(); |
| } |
| }; |
|
|
| stopResize = (): void => { |
| this.isResizing = false; |
| document.removeEventListener("pointermove", this.handleResize); |
| document.removeEventListener("pointerup", this.stopResize); |
| }; |
| } |
|
|