Spaces:
Running
Running
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); | |
}; | |
} | |