So my idea is to make some simple canvas editor.
My current problem is I can’t resize my object properly.
Currently it’s being resized from it’s center and not from the edge I’m dragging.
Here’s what I expect to have:
My code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Object Editor</title>
<style>
body { display: flex; font-family: Arial, sans-serif; }
#canvasContainer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
canvas { border: 1px solid black; }
.transform-handle{
width: 8px;
height: 8px;
background-color: lightblue;
position: absolute;
cursor: grab;
z-index: 1000;
border: 1px solid #007BFF;
border-radius: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translate(-50%, -50%);
}
.rotate-handle {
width: 8px;
height: 8px;
background-color: rgb(225, 173, 230);
position: absolute;
cursor: grab;
z-index: 1000;
border: 1px solid #ff00aa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div id="canvasContainer">
<canvas id="editorCanvas" width="800" height="700"></canvas>
</div>
<div id="top-left-handle" class="transform-handle"></div>
<div id="top-right-handle" class="transform-handle"></div>
<div id="bottom-left-handle" class="transform-handle"></div>
<div id="bottom-right-handle" class="transform-handle"></div>
<div id="middle-left-handle" class="transform-handle"></div>
<div id="middle-right-handle" class="transform-handle"></div>
<div id="middle-top-handle" class="transform-handle"></div>
<div id="middle-bottom-handle" class="transform-handle"></div>
<div id="rotate-top-left-handle" class="rotate-handle"></div>
<div id="rotate-top-right-handle" class="rotate-handle"></div>
<div id="rotate-bottom-left-handle" class="rotate-handle"></div>
<div id="rotate-bottom-right-handle" class="rotate-handle"></div>
<script>
const canvas = document.getElementById("editorCanvas");
const ctx = canvas.getContext("2d");
const canvasContainer = document.getElementById("canvasContainer");
let isDragging = false;
let offsetX, offsetY;
let isResizing = false;
let isRotating = false;
let currentHandle = null;
let initialMouseX, initialMouseY, initialWidth, initialHeight, initialAngle;
let objects = [
{ id: 1, name: "Object 1", x: 50, y: 50, width: 100, height: 100, angle: 0, opacity: 1, layer: 0, color: "#FFC080", behaviours: {} },
{ id: 2, name: "Object 2", x: 250, y: 150, width: 80, height: 80, angle: 45, opacity: 1, layer: 1, color: "#FF9900", behaviours: {} },
{ id: 3, name: "Object 3", x: 500, y: 120, width: 50, height: 60, angle: 80, opacity: 1, layer: 2, color: "#FFD700", behaviours: {} }
];
var selectedObject = null;
function drawObjects() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
objects.sort((a, b) => a.layer - b.layer).forEach(drawObject);
}
function drawObject(obj) {
ctx.save(); // Save the canvas state before any transformations
ctx.globalAlpha = obj.opacity;
// Move the canvas to the center of the object, then rotate
ctx.translate(obj.x, obj.y); // Use top-left as the origin, not center
ctx.translate(obj.width / 2, obj.height / 2); // Shift to the center
ctx.rotate((obj.angle * Math.PI) / 180); // Apply rotation
// Now, draw the object centered on the (0, 0) point (its center)
ctx.fillStyle = obj.color;
ctx.fillRect(-obj.width / 2, -obj.height / 2, obj.width, obj.height);
// Draw the center point for reference
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(0, 0, 2, 0, 2 * Math.PI);
ctx.fill();
// Draw the size text
ctx.fillStyle = "black";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(`${Math.round(obj.width)}x${Math.round(obj.height)}`, 0, obj.height / 2 + 15);
// Restore canvas state after drawing the object
ctx.restore();
// Now for the outline, ensure it's drawn at the correct location, accounting for the transformation
if (obj === selectedObject && isResizing) {
ctx.save(); // Save the current state
ctx.translate(obj.x, obj.y);
ctx.translate(obj.width / 2, obj.height / 2); // Move to center
ctx.rotate((obj.angle * Math.PI) / 180);
// Outline of the object (square outline)
ctx.beginPath();
ctx.moveTo(-obj.width / 2, -obj.height / 2);
ctx.lineTo(obj.width / 2, -obj.height / 2);
ctx.lineTo(obj.width / 2, obj.height / 2);
ctx.lineTo(-obj.width / 2, obj.height / 2);
ctx.closePath();
ctx.stroke();
ctx.restore(); // Restore state for normal drawing behavior
}
}
function updateHandles() {
if (selectedObject) {
const canvasRect = canvas.getBoundingClientRect();
const cx = selectedObject.x + selectedObject.width / 2;
const cy = selectedObject.y + selectedObject.height / 2;
const width = selectedObject.width;
const height = selectedObject.height;
const angle = selectedObject.angle;
const angleRad = angle * Math.PI / 180;
const handles = [
{ id: 'top-left-handle', dx: -width / 2, dy: -height / 2 },
{ id: 'top-right-handle', dx: width / 2, dy: -height / 2 },
{ id: 'bottom-left-handle', dx: -width / 2, dy: height / 2 },
{ id: 'bottom-right-handle', dx: width / 2, dy: height / 2 },
{ id: 'middle-left-handle', dx: -width / 2, dy: 0 },
{ id: 'middle-right-handle', dx: width / 2, dy: 0 },
{ id: 'middle-top-handle', dx: 0, dy: -height / 2 },
{ id: 'middle-bottom-handle', dx: 0, dy: height / 2 },
{ id: 'rotate-top-left-handle', dx: -width / 2 - 10, dy: -height / 2 - 10 },
{ id: 'rotate-top-right-handle', dx: width / 2 + 10, dy: -height / 2 - 10 },
{ id: 'rotate-bottom-left-handle', dx: -width / 2 - 10, dy: height / 2 + 10 },
{ id: 'rotate-bottom-right-handle', dx: width / 2 + 10, dy: height / 2 + 10 }
];
handles.forEach(handle => {
const element = document.getElementById(handle.id);
const rotatedX = handle.dx * Math.cos(angleRad) - handle.dy * Math.sin(angleRad);
const rotatedY = handle.dx * Math.sin(angleRad) + handle.dy * Math.cos(angleRad);
const x = cx + rotatedX;
const y = cy + rotatedY;
element.style.left = `${x + canvasRect.left}px`;
element.style.top = `${y + canvasRect.top}px`;
element.style.display = 'block';
});
}
}
function rotatePoint(x, y, cx, cy, angle) {
const rad = angle * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return {
x: (x - cx) * cos - (y - cy) * sin + cx,
y: (x - cx) * sin + (y - cy) * cos + cy
};
}
function unrotatePoint(x, y, cx, cy, angle) {
return rotatePoint(x, y, cx, cy, -angle);
}
function getCanvasMousePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
// SELECT OBJECT
canvas.addEventListener("mousedown", (event) => {
const pos = getCanvasMousePosition(event);
const objectsUnderCursor = objects.filter(obj => {
const centerX = obj.x + obj.width / 2;
const centerY = obj.y + obj.height / 2;
// Transform cursor position relative to object's center and rotation
const dx = pos.x - centerX;
const dy = pos.y - centerY;
const angle = -obj.angle * Math.PI / 180;
const rotatedX = dx * Math.cos(angle) - dy * Math.sin(angle);
const rotatedY = dx * Math.sin(angle) + dy * Math.cos(angle);
// Check if the rotated point is within the object's bounds
const absWidth = Math.abs(obj.width);
const absHeight = Math.abs(obj.height);
return Math.abs(rotatedX) <= absWidth / 2 && Math.abs(rotatedY) <= absHeight / 2;
});
selectedObject = objectsUnderCursor.reduce((highest, current) => {
return current.layer > highest.layer ? current : highest;
}, { layer: -999 });
if (selectedObject && selectedObject.layer !== -999) {
updateHandles();
offsetX = pos.x - selectedObject.x;
offsetY = pos.y - selectedObject.y;
isDragging = true;
} else {
selectedObject = null;
// console.log('selectedObject = null');
// Hide handles when no object is selected
const handles = document.querySelectorAll('.transform-handle, .rotate-handle');
handles.forEach(handle => {
handle.style.display = 'none';
});
}
});
canvas.addEventListener("mousemove", (event) => {
if (isDragging && selectedObject) {
const pos = getCanvasMousePosition(event);
selectedObject.x = pos.x - offsetX;
selectedObject.y = pos.y - offsetY;
updateHandles();
}
});
function onHandleMouseDown(event, type) {
event.preventDefault();
event.stopPropagation();
if (!selectedObject) return;
currentHandle = event.target.id;
const pos = getCanvasMousePosition(event);
if (currentHandle.startsWith('rotate-')) {
isRotating = true;
const center = {
x: selectedObject.x + selectedObject.width / 2,
y: selectedObject.y + selectedObject.height / 2
};
initialAngle = Math.atan2(pos.y - center.y, pos.x - center.x) * 180 / Math.PI - selectedObject.angle;
} else {
isResizing = true;
initialMouseX = pos.x;
initialMouseY = pos.y;
initialWidth = selectedObject.width;
initialHeight = selectedObject.height;
initialX = selectedObject.x;
initialY = selectedObject.y;
}
}
// RESIZE
window.addEventListener('mousemove', (event) => {
if (!selectedObject) return;
const pos = getCanvasMousePosition(event);
const center = {
x: selectedObject.x + selectedObject.width / 2,
y: selectedObject.y + selectedObject.height / 2
};
if (isResizing) {
// Convert mouse coordinates to object's local space
const angleRad = -selectedObject.angle * Math.PI / 180;
const dx = pos.x - center.x;
const dy = pos.y - center.y;
// Get rotated mouse position
const rotatedX = dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
const rotatedY = dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
// Get initial rotated position
const initialDx = initialMouseX - center.x;
const initialDy = initialMouseY - center.y;
const initialRotatedX = initialDx * Math.cos(angleRad) - initialDy * Math.sin(angleRad);
const initialRotatedY = initialDx * Math.sin(angleRad) + initialDy * Math.cos(angleRad);
// Calculate deltas in rotated space
const deltaX = rotatedX - initialRotatedX;
const deltaY = rotatedY - initialRotatedY;
let newWidth = selectedObject.width;
let newHeight = selectedObject.height;
let newX = selectedObject.x;
let newY = selectedObject.y;
switch (currentHandle) {
case 'bottom-right-handle':
newWidth = initialWidth + deltaX * 2;
newHeight = initialHeight + deltaY * 2;
break;
case 'bottom-left-handle':
newWidth = initialWidth - deltaX * 2;
newHeight = initialHeight + deltaY * 2;
break;
case 'top-right-handle':
newWidth = initialWidth + deltaX * 2;
newHeight = initialHeight - deltaY * 2;
break;
case 'top-left-handle':
newWidth = initialWidth - deltaX * 2;
newHeight = initialHeight - deltaY * 2;
break;
case 'middle-right-handle':
newWidth = initialWidth + deltaX * 2;
break;
case 'middle-left-handle':
newWidth = initialWidth - deltaX * 2;
break;
case 'middle-top-handle':
newHeight = initialHeight - deltaY * 2;
break;
case 'middle-bottom-handle':
newHeight = initialHeight + deltaY * 2;
break;
}
// Calculate new center position
const widthDiff = newWidth - initialWidth;
const heightDiff = newHeight - initialHeight;
// Update the object's position to maintain its center
newX = center.x - newWidth / 2;
newY = center.y - newHeight / 2;
// Apply the changes
selectedObject.width = newWidth;
selectedObject.height = newHeight;
selectedObject.x = newX;
selectedObject.y = newY;
} else if (isRotating) {
const angle = Math.atan2(pos.y - center.y, pos.x - center.x) * 180 / Math.PI - initialAngle;
selectedObject.angle = angle;
}
updateHandles();
});
window.addEventListener('mouseup', () => {
isDragging = false;
isResizing = false;
isRotating = false;
});
const handles = document.querySelectorAll('.transform-handle, .rotate-handle');
handles.forEach(handle => {
handle.addEventListener('mousedown', (event) => onHandleMouseDown(event, handle.id === 'rotate-handle' ? 'rotate' : 'resize'));
// Initially hide handles
handle.style.display = 'none';
});
function editorLoop() {
drawObjects();
if (selectedObject) {
handles.forEach(handle => handle.style.display = 'block');
updateHandles();
}
requestAnimationFrame(editorLoop);
}
editorLoop();
</script>
</body>
</html>
What I’ve tried:
Implemented resizing using handles:
I added multiple resize handles around the object.
Each handle adjusts the width and height when dragged.
Handled rotation separately:
Rotation is applied using additional handles positioned around the object.
The angle is updated based on the cursor’s position relative to the object’s center.
Adjusted resizing calculations:
I tried updating width and height based on mouse movement.
However, resizing currently happens from the center instead of the edge being dragged.
Attempted coordinate transformations:
I used trigonometric functions to account for rotation.
The issue persists - handles do not behave as expected when resizing.
Maintained object selection state:
Only the selected object should show resize/rotate handles.
Clicking outside should deselect the object.
Despite these attempts, I am struggling to get the resizing to happen correctly from the dragged edge, rather than resizing the object symmetrically from the center.