在Canvas中实现可拖动并可调整大小的矩形,核心是通过监听鼠标事件来更新矩形的位置和尺寸状态,再结合Canvas的重绘机制实现交互效果。整个过程不需要依赖任何第三方库,仅用原生JS就能完成。

实现原理概述
要实现可交互的矩形,首先需要定义矩形的基础状态,包括位置坐标、宽度、高度,以及当前的交互状态(比如是否处于拖动中、是否处于缩放中)。然后通过监听Canvas的鼠标按下、移动、抬起事件,判断用户的操作意图,更新矩形状态后重新绘制Canvas即可。
矩形状态定义
我们需要用一个对象来存储矩形的所有状态信息,方便后续操作和修改:
// 矩形状态对象
const rect = {
x: 100, // 矩形左上角x坐标
y: 100, // 矩形左上角y坐标
width: 200, // 矩形宽度
height: 150, // 矩形高度
isDragging: false, // 是否处于拖动状态
isResizing: false, // 是否处于缩放状态
dragOffsetX: 0, // 拖动时鼠标相对于矩形左上角的x偏移
dragOffsetY: 0, // 拖动时鼠标相对于矩形左上角的y偏移
resizeDirection: null // 缩放方向,比如'right-bottom'表示右下角缩放
};
Canvas基础绘制
首先初始化Canvas上下文,编写矩形的绘制函数,后续每次矩形状态更新后都调用这个函数重绘:
// 获取Canvas元素和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 设置Canvas尺寸,这里设置为800*500
canvas.width = 800;
canvas.height = 500;
// 绘制矩形的函数
function drawRect() {
// 清空Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制矩形
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
// 绘制右下角缩放手柄,方便用户识别可缩放区域
ctx.fillStyle = '#666';
ctx.fillRect(rect.x + rect.width - 8, rect.y + rect.height - 8, 8, 8);
}
// 初始绘制一次
drawRect();
鼠标事件监听与交互逻辑
接下来需要监听Canvas的鼠标事件,判断用户的操作是点击、拖动还是缩放,然后更新矩形状态。
鼠标按下事件处理
鼠标按下时,需要判断点击的位置是在矩形内部(触发拖动)、缩放手柄上(触发缩放),还是其他区域:
// 缩放手柄的尺寸
const resizeHandleSize = 8;
canvas.addEventListener('mousedown', (e) => {
// 获取鼠标在Canvas中的坐标
const mouseX = e.offsetX;
const mouseY = e.offsetY;
// 判断是否在右下角缩放手柄范围内
const isInResizeHandle = mouseX >= rect.x + rect.width - resizeHandleSize &&
mouseX <= rect.x + rect.width &&
mouseY >= rect.y + rect.height - resizeHandleSize &&
mouseY <= rect.y + rect.height;
if (isInResizeHandle) {
// 进入缩放状态
rect.isResizing = true;
rect.resizeDirection = 'right-bottom';
} else if (mouseX >= rect.x && mouseX <= rect.x + rect.width &&
mouseY >= rect.y && mouseY <= rect.y + rect.height) {
// 进入拖动状态,计算鼠标相对于矩形左上角的偏移
rect.isDragging = true;
rect.dragOffsetX = mouseX - rect.x;
rect.dragOffsetY = mouseY - rect.y;
}
});
鼠标移动事件处理
鼠标移动时,根据当前的交互状态更新矩形的位置或尺寸:
canvas.addEventListener('mousemove', (e) => {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
// 拖动状态处理
if (rect.isDragging) {
// 更新矩形左上角坐标,减去偏移量保证鼠标和矩形的相对位置不变
rect.x = mouseX - rect.dragOffsetX;
rect.y = mouseY - rect.dragOffsetY;
// 限制矩形不超出Canvas边界
rect.x = Math.max(0, Math.min(rect.x, canvas.width - rect.width));
rect.y = Math.max(0, Math.min(rect.y, canvas.height - rect.height));
// 重绘
drawRect();
}
// 缩放状态处理
if (rect.isResizing) {
// 右下角缩放,更新宽度和高度,最小尺寸限制为20
rect.width = Math.max(20, mouseX - rect.x);
rect.height = Math.max(20, mouseY - rect.y);
// 限制矩形不超出Canvas边界
if (rect.x + rect.width > canvas.width) {
rect.width = canvas.width - rect.x;
}
if (rect.y + rect.height > canvas.height) {
rect.height = canvas.height - rect.y;
}
// 重绘
drawRect();
}
});
鼠标抬起事件处理
鼠标抬起时,需要重置交互状态,结束拖动或缩放:
canvas.addEventListener('mouseup', () => {
rect.isDragging = false;
rect.isResizing = false;
rect.resizeDirection = null;
});
// 鼠标移出Canvas时也结束交互,避免状态异常
canvas.addEventListener('mouseleave', () => {
rect.isDragging = false;
rect.isResizing = false;
rect.resizeDirection = null;
});
完整示例代码
将上面的代码整合起来,就是完整的实现代码,你可以直接复制到HTML文件中运行:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas可拖动可缩放矩形</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
}
#canvas {
border: 1px solid #ccc;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h3>Canvas可拖动可调整大小的矩形</h3>
<p>点击矩形内部拖动,点击右下角小方块调整大小</p>
<canvas id="canvas"></canvas>
<script>
const rect = {
x: 100,
y: 100,
width: 200,
height: 150,
isDragging: false,
isResizing: false,
dragOffsetX: 0,
dragOffsetY: 0,
resizeDirection: null
};
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 500;
const resizeHandleSize = 8;
function drawRect() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
ctx.fillStyle = '#666';
ctx.fillRect(rect.x + rect.width - resizeHandleSize, rect.y + rect.height - resizeHandleSize, resizeHandleSize, resizeHandleSize);
}
drawRect();
canvas.addEventListener('mousedown', (e) => {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
const isInResizeHandle = mouseX >= rect.x + rect.width - resizeHandleSize &&
mouseX <= rect.x + rect.width &&
mouseY >= rect.y + rect.height - resizeHandleSize &&
mouseY <= rect.y + rect.height;
if (isInResizeHandle) {
rect.isResizing = true;
rect.resizeDirection = 'right-bottom';
} else if (mouseX >= rect.x && mouseX <= rect.x + rect.width &&
mouseY >= rect.y && mouseY <= rect.y + rect.height) {
rect.isDragging = true;
rect.dragOffsetX = mouseX - rect.x;
rect.dragOffsetY = mouseY - rect.y;
}
});
canvas.addEventListener('mousemove', (e) => {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
if (rect.isDragging) {
rect.x = mouseX - rect.dragOffsetX;
rect.y = mouseY - rect.dragOffsetY;
rect.x = Math.max(0, Math.min(rect.x, canvas.width - rect.width));
rect.y = Math.max(0, Math.min(rect.y, canvas.height - rect.height));
drawRect();
}
if (rect.isResizing) {
rect.width = Math.max(20, mouseX - rect.x);
rect.height = Math.max(20, mouseY - rect.y);
if (rect.x + rect.width > canvas.width) {
rect.width = canvas.width - rect.x;
}
if (rect.y + rect.height > canvas.height) {
rect.height = canvas.height - rect.y;
}
drawRect();
}
});
canvas.addEventListener('mouseup', () => {
rect.isDragging = false;
rect.isResizing = false;
rect.resizeDirection = null;
});
canvas.addEventListener('mouseleave', () => {
rect.isDragging = false;
rect.isResizing = false;
rect.resizeDirection = null;
});
</script>
</body>
</html>
扩展优化方向
上面的示例实现了基础功能,还可以根据需求做更多扩展:
- 增加更多缩放手柄,比如左上角、右上角、左下角,实现多方向的缩放
- 添加矩形选中状态,只有选中时才显示缩放手柄,避免干扰
- 增加键盘辅助操作,比如按住Shift键等比例缩放
- 支持多个矩形的交互,需要维护矩形数组和当前选中矩形的索引