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; offsetMouseX: number; offsetMouseY: number; resizeHandleSize: number; resizingHandleIndex: number; minSize: number; renderCallBack: () => void; canvasXmin: number; canvasYmin: number; canvasXmax: number; canvasYmax: number; scaleFactor: number; thickness: number; selectedThickness: number; resizeHandles: { xmin: number; ymin: number; xmax: number; ymax: number; }[]; constructor( renderCallBack: () => 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.canvasXmin = canvasXmin; this.canvasYmin = canvasYmin; this.canvasXmax = canvasXmax; this.canvasYmax = canvasYmax; this.scaleFactor = scaleFactor; this.label = label; this.isDragging = 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; } 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, }, { // Top right xmin: this.xmax - halfSize, ymin: this.ymin - halfSize, xmax: this.xmax + halfSize, ymax: this.ymin + halfSize, }, { // Bottom right xmin: this.xmax - halfSize, ymin: this.ymax - halfSize, xmax: this.xmax + halfSize, ymax: this.ymax + halfSize, }, { // Bottom left xmin: this.xmin - halfSize, ymin: this.ymax - halfSize, xmax: this.xmin + halfSize, ymax: this.ymax + halfSize, }, { // Top center xmin: this.xmin + (width / 2) - halfSize, ymin: this.ymin - halfSize, xmax: this.xmin + (width / 2) + halfSize, ymax: this.ymin + halfSize, }, { // Right center xmin: this.xmax - halfSize, ymin: this.ymin + (height / 2) - halfSize, xmax: this.xmax + halfSize, ymax: this.ymin + (height / 2) + halfSize, }, { // Bottom center xmin: this.xmin + (width / 2) - halfSize, ymin: this.ymax - halfSize, xmax: this.xmin + (width / 2) + halfSize, ymax: this.ymax + halfSize, }, { // Left center xmin: this.xmin - halfSize, ymin: this.ymin + (height / 2) - halfSize, xmax: this.xmin + halfSize, ymax: this.ymin + (height / 2) + halfSize, }, ]; } getWidth(): number { return this.xmax - this.xmin; } getHeight(): number { return this.ymax - this.ymin; } 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("mousemove", this.handleDrag); document.addEventListener("mouseup", this.stopDrag); } stopDrag = (): void => { this.isDragging = false; document.removeEventListener("mousemove", this.handleDrag); document.removeEventListener("mouseup", 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; } 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("mousemove", this.handleResize); document.addEventListener("mouseup", 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("mousemove", this.handleResize); document.removeEventListener("mouseup", this.stopResize); }; }