I am trying to program a selection tool for a canvas element but I am having trouble with math I think.... Essentially I am having an issue where after rotating the selection, then trying to adjust/resize.. it no longer functions as I would expect it to.
To see the issue that I am referring to, please make a selection and then rotate the selection at least 90 degrees. If you select the any of the corners and begin to resize, you will notice that as you move that corner downwards, the opposite corner moves to the right. If you move upwards, it moves to the left, if you move to the right it moves upwards.... etc....
This opposite corner should be stationary... but it's not. Any help would be appreciated if you have the time to take a look :)
Sorry for not being more clear on my first edit. I tried to explain the issue in more detail. I believe what Wyck suggested might be on the right track, but I think it may be more complicated as the code I am using uses both the width and the x/y coordinate of the overlayCanvas. I definetily think there is some coordinate translation issue that is more apparent with increased rotation angles.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const overlayCanvas = document.getElementById('overlayCanvas');
const overlayCtx = overlayCanvas.getContext('2d');
let isSelecting = false;
let isMoving = false;
let isResizing = false;
let isRotating = false;
let resizeHandle = null;
let rotationAngle = 0;
let selection = null;
let mouseDownCoords = null;
let offset = {
x: 0,
y: 0
};
let selectionImage = null; // Image of the selected content
const resizeHandleSize = 10;
const rotationHandleOffset = 30;
// Initial canvas drawing
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(400, 300, 100, 0, Math.PI * 2);
ctx.fill();
canvas.addEventListener('mousedown', (e) => {
const x = e.offsetX;
const y = e.offsetY;
if (!selection) {
isSelecting = true;
mouseDownCoords = {
x,
y
};
selection = {
x: mouseDownCoords.x,
y: mouseDownCoords.y,
width: 0,
height: 0,
originalWidth: 0,
originalHeight: 0
};
} else {
const handle = getHandleUnderCursor(x, y);
if (handle) {
isResizing = true;
resizeHandle = handle;
selection.originalWidth = selection.width;
selection.originalHeight = selection.height;
} else if (isInsideSelection(x, y)) {
isMoving = true;
offset = {
x: x - selection.x,
y: y - selection.y,
};
} else if (isRotationHandle(x, y)) {
isRotating = true;
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
rotationAngle = Math.atan2(y - centerY, x - centerX);
} else {
applySelection();
}
}
});
canvas.addEventListener('mousemove', (e) => {
if (isSelecting) {
updateSelection(mouseDownCoords.x, mouseDownCoords.y, e.offsetX, e.offsetY);
} else if (isMoving) {
moveSelection(e.offsetX, e.offsetY);
} else if (isResizing) {
resizeSelection(e.offsetX, e.offsetY);
} else if (isRotating) {
rotateSelection(e.offsetX, e.offsetY);
}
});
canvas.addEventListener('mouseup', () => {
if (isSelecting) {
finalizeSelection();
}
isSelecting = isMoving = isResizing = isRotating = false;
resizeHandle = null;
});
function updateSelection(startX, startY, currentX, currentY) {
selection.x = Math.min(startX, currentX);
selection.y = Math.min(startY, currentY);
selection.width = Math.abs(startX - currentX);
selection.height = Math.abs(startY - currentY);
drawOverlay();
}
function moveSelection(x, y) {
selection.x = x - offset.x;
selection.y = y - offset.y;
drawOverlay();
}
function resizeSelection(x, y) {
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
// Transform the mouse position into the selection's local space
const {
x: localX,
y: localY
} = getRotatedPoint(x, y, -rotationAngle, centerX, centerY);
// Adjust selection bounds based on the handle being used
if (resizeHandle === 'top') {
const bottomY = selection.y + selection.height; // Keep the bottom stationary
selection.y = Math.min(localY, bottomY);
selection.height = Math.abs(bottomY - selection.y);
} else if (resizeHandle === 'bottom') {
const topY = selection.y; // Keep the top stationary
selection.height = Math.abs(localY - topY);
} else if (resizeHandle === 'left') {
const rightX = selection.x + selection.width; // Keep the right stationary
selection.x = Math.min(localX, rightX);
selection.width = Math.abs(rightX - selection.x);
} else if (resizeHandle === 'right') {
const leftX = selection.x; // Keep the left stationary
selection.width = Math.abs(localX - leftX);
} else if (resizeHandle === 'top-left') {
const bottomRight = {
x: selection.x + selection.width,
y: selection.y + selection.height
}; // Keep bottom-right stationary
selection.x = Math.min(localX, bottomRight.x);
selection.y = Math.min(localY, bottomRight.y);
selection.width = Math.abs(bottomRight.x - selection.x);
selection.height = Math.abs(bottomRight.y - selection.y);
} else if (resizeHandle === 'top-right') {
const bottomLeft = {
x: selection.x,
y: selection.y + selection.height
}; // Keep bottom-left stationary
selection.y = Math.min(localY, bottomLeft.y);
selection.width = Math.abs(localX - bottomLeft.x);
selection.height = Math.abs(bottomLeft.y - selection.y);
} else if (resizeHandle === 'bottom-left') {
const topRight = {
x: selection.x + selection.width,
y: selection.y
}; // Keep top-right stationary
selection.x = Math.min(localX, topRight.x);
selection.height = Math.abs(localY - topRight.y);
selection.width = Math.abs(topRight.x - selection.x);
} else if (resizeHandle === 'bottom-right') {
const topLeft = {
x: selection.x,
y: selection.y
}; // Keep top-left stationary
selection.width = Math.abs(localX - topLeft.x);
selection.height = Math.abs(localY - topLeft.y);
}
// No need to adjust the center of rotation because the opposite side is stationary
drawOverlay();
}
function rotateSelection(x, y) {
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
const angle = Math.atan2(y - centerY, x - centerX);
rotationAngle = angle + Math.PI / 2;
drawOverlay();
}
function finalizeSelection() {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = selection.width;
tempCanvas.height = selection.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(
canvas,
selection.x,
selection.y,
selection.width,
selection.height,
0,
0,
selection.width,
selection.height
);
selectionImage = new Image();
selectionImage.src = tempCanvas.toDataURL();
drawOverlay();
}
function applySelection() {
if (selection) {
ctx.save();
ctx.translate(selection.x + selection.width / 2, selection.y + selection.height / 2);
ctx.rotate(rotationAngle);
if (selectionImage) {
ctx.drawImage(
selectionImage,
0,
0,
selectionImage.width,
selectionImage.height,
-selection.width / 2,
-selection.height / 2,
selection.width,
selection.height
);
}
ctx.restore();
selectionImage = null;
selection = null;
rotationAngle = 0;
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
}
}
function drawOverlay() {
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
if (selection) {
overlayCtx.save();
overlayCtx.translate(selection.x + selection.width / 2, selection.y + selection.height / 2);
overlayCtx.rotate(rotationAngle);
if (selectionImage) {
overlayCtx.drawImage(
selectionImage,
0,
0,
selectionImage.width,
selectionImage.height,
-selection.width / 2,
-selection.height / 2,
selection.width,
selection.height
);
}
overlayCtx.restore();
drawHandles();
}
}
function drawHandles() {
if (!selection) return;
const corners = getRotatedCorners();
const edges = getRotatedEdges(corners);
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
const rotation = getRotatedPoint(
selection.x + selection.width / 2,
selection.y - rotationHandleOffset,
rotationAngle,
centerX,
centerY
);
overlayCtx.fillStyle = 'red';
corners.forEach(point => {
overlayCtx.fillRect(point.x - resizeHandleSize / 2, point.y - resizeHandleSize / 2, resizeHandleSize, resizeHandleSize);
});
edges.forEach(point => {
overlayCtx.fillRect(point.x - resizeHandleSize / 2, point.y - resizeHandleSize / 2, resizeHandleSize, resizeHandleSize);
});
overlayCtx.fillStyle = 'green';
overlayCtx.beginPath();
overlayCtx.arc(rotation.x, rotation.y, resizeHandleSize / 2, 0, Math.PI * 2);
overlayCtx.fill();
}
function getHandleUnderCursor(x, y) {
const corners = getRotatedCorners();
const edges = getRotatedEdges(corners);
for (let i = 0; i < corners.length; i++) {
const corner = corners[i];
if (
x >= corner.x - resizeHandleSize / 2 &&
x <= corner.x + resizeHandleSize / 2 &&
y >= corner.y - resizeHandleSize / 2 &&
y <= corner.y + resizeHandleSize / 2
) {
return ['top-left', 'top-right', 'bottom-left', 'bottom-right'][i];
}
}
for (let i = 0; i < edges.length; i++) {
const edge = edges[i];
if (
x >= edge.x - resizeHandleSize / 2 &&
x <= edge.x + resizeHandleSize / 2 &&
y >= edge.y - resizeHandleSize / 2 &&
y <= edge.y + resizeHandleSize / 2
) {
return ['top', 'right', 'bottom', 'left'][i];
}
}
return null;
}
function getRotatedCorners() {
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
return [
getRotatedPoint(selection.x, selection.y, rotationAngle, centerX, centerY),
getRotatedPoint(selection.x + selection.width, selection.y, rotationAngle, centerX, centerY),
getRotatedPoint(selection.x, selection.y + selection.height, rotationAngle, centerX, centerY),
getRotatedPoint(selection.x + selection.width, selection.y + selection.height, rotationAngle, centerX, centerY),
];
}
function getRotatedEdges(corners) {
return [
midpoint(corners[0], corners[1]),
midpoint(corners[1], corners[3]),
midpoint(corners[2], corners[3]),
midpoint(corners[0], corners[2]),
];
}
function getRotatedPoint(x, y, angle, centerX, centerY) {
const dx = x - centerX;
const dy = y - centerY;
const rotatedX = dx * Math.cos(angle) - dy * Math.sin(angle) + centerX;
const rotatedY = dx * Math.sin(angle) + dy * Math.cos(angle) + centerY;
return {
x: rotatedX,
y: rotatedY
};
}
function midpoint(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
}
function isInsideSelection(x, y) {
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
const {
x: localX,
y: localY
} = getRotatedPoint(x, y, -rotationAngle, centerX, centerY);
return (
localX >= selection.x &&
localX <= selection.x + selection.width &&
localY >= selection.y &&
localY <= selection.y + selection.height
);
}
function isRotationHandle(x, y) {
const centerX = selection.x + selection.width / 2;
const centerY = selection.y + selection.height / 2;
const rotation = getRotatedPoint(
selection.x + selection.width / 2,
selection.y - rotationHandleOffset,
rotationAngle,
centerX,
centerY
);
return (
x >= rotation.x - resizeHandleSize / 2 &&
x <= rotation.x + resizeHandleSize / 2 &&
y >= rotation.y - resizeHandleSize / 2 &&
y <= rotation.y + resizeHandleSize / 2
);
}
/* Body styling */
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #FFF;
font-family: Arial, sans-serif;
overflow: hidden;
}
/* Canvas container */
#canvas,
#overlayCanvas {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 6px 20px rgba(0, 0, 0, 0.19);
border: 2px solid #ffffff;
border-radius: 8px;
}
/* Set a fixed size for the canvases */
canvas {
width: 800px;
height: 600px;
}
/* Overlay canvas styling */
#overlayCanvas {
z-index: 10;
pointer-events: none;
/* Ensures overlay canvas does not block interactions */
}
<canvas id="canvas" width="800" height="600"></canvas>
<canvas id="overlayCanvas" width="800" height="600"></canvas>