HTML Srtucture
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Rect Interaction</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="ui-panel">
<label for="fillPicker">Shape Color:</label>
<input type="color" id="fillPicker" value="#4a90e2">
</div>
<canvas id="workspace"></canvas>
<script src="renderer.js"></script>
</body>
</html>
CSS Layout
:root {
--canvas-bg: #2d3436;
--panel-gap: 1.5rem;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
gap: var(--panel-gap);
background: #dfe6e9;
font-family: system-ui, sans-serif;
}
.ui-panel {
display: flex;
align-items: center;
gap: 0.75rem;
}
#workspace {
background: var(--canvas-bg);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
cursor: crosshair;
}
JavaScript Interactino Logic
const canvasEl = document.getElementById('workspace');
const renderCtx = canvasEl.getContext('2d');
const colorInput = document.getElementById('fillPicker');
const PIXEL_RATIO = window.devicePixelRatio || 1;
const VIEW_W = 640;
const VIEW_H = 420;
function configureCanvas() {
canvasEl.width = VIEW_W * PIXEL_RATIO;
canvasEl.height = VIEW_H * PIXEL_RATIO;
canvasEl.style.width = `${VIEW_W}px`;
canvasEl.style.height = `${VIEW_H}px`;
renderCtx.scale(PIXEL_RATIO, PIXEL_RATIO);
}
configureCanvas();
const shapeStore = [];
let currentShape = null;
let mode = 'idle';
let dragDelta = { x: 0, y: 0 };
class RectEntity {
constructor(hex, originX, originY) {
this.hex = hex;
this.originX = originX;
this.originY = originY;
this.targetX = originX;
this.targetY = originY;
}
get metrics() {
const left = Math.min(this.originX, this.targetX);
const top = Math.min(this.originY, this.targetY);
const w = Math.abs(this.targetX - this.originX);
const h = Math.abs(this.targetY - this.originY);
return { left, top, w, h };
}
paint(context) {
const { left, top, w, h } = this.metrics;
context.beginPath();
context.rect(left, top, w, h);
context.fillStyle = this.hex;
context.fill();
context.lineWidth = 2;
context.strokeStyle = '#ffffff';
context.lineJoin = 'miter';
context.stroke();
}
hitTest(px, py) {
const { left, top, w, h } = this.metrics;
return px >= left && px <= left + w && py >= top && py <= top + h;
}
}
function resolvePointer(e) {
const bounds = canvasEl.getBoundingClientRect();
return {
x: e.clientX - bounds.left,
y: e.clientY - bounds.top
};
}
function findTopmostShape(px, py) {
for (let i = shapeStore.length - 1; i >= 0; i--) {
if (shapeStore[i].hitTest(px, py)) return shapeStore[i];
}
return null;
}
canvasEl.addEventListener('mousedown', (e) => {
const ptr = resolvePointer(e);
const hit = findTopmostShape(ptr.x, ptr.y);
if (hit) {
mode = 'dragging';
currentShape = hit;
dragDelta.x = ptr.x - hit.originX;
dragDelta.y = ptr.y - hit.originY;
currentShape._cachedW = Math.abs(hit.targetX - hit.originX);
currentShape._cachedH = Math.abs(hit.targetY - hit.originY);
currentShape._dirX = hit.targetX >= hit.originX ? 1 : -1;
currentShape._dirY = hit.targetY >= hit.originY ? 1 : -1;
} else {
mode = 'drawing';
currentShape = new RectEntity(colorInput.value, ptr.x, ptr.y);
shapeStore.push(currentShape);
}
});
window.addEventListener('mousemove', (e) => {
if (!currentShape) return;
const ptr = resolvePointer(e);
if (mode === 'dragging') {
currentShape.originX = ptr.x - dragDelta.x;
currentShape.originY = ptr.y - dragDelta.y;
currentShape.targetX = currentShape.originX + (currentShape._cachedW * currentShape._dirX);
currentShape.targetY = currentShape.originY + (currentShape._cachedH * currentShape._dirY);
} else if (mode === 'drawing') {
currentShape.targetX = ptr.x;
currentShape.targetY = ptr.y;
}
});
window.addEventListener('mouseup', () => {
mode = 'idle';
currentShape = null;
});
function animate() {
renderCtx.clearRect(0, 0, VIEW_W, VIEW_H);
shapeStore.forEach(entity => entity.paint(renderCtx));
requestAnimationFrame(animate);
}
animate();