1. 基础准备
1.1 创建画布与环境
原生 JavaScript Canvas 提供底层绘图能力,能够在不依赖第三方库的情况下完成矩形的绘制、拖拽与缩放等交互。为了实现高效绘制,必须先建立一个可绘制的画布环境,并正确处理像素密度。初始阶段的重点在于搭建画布、获取绘图上下文以及设置合理的尺寸。
在实现过程中,设备像素比(devicePixelRatio)是提升清晰度的关键。通过将画布实际像素尺寸设为 CSS 尺寸乘以像素比,可以避免模糊边缘,保证矩形随移动或缩放保持锐利。
Canvas Rect Interactive
随后需要在 JavaScript 中获取画布上下文,并准备一个存放矩形信息的对象,以便后续的绘制、拖拽和缩放操作使用。这一步的核心在于建立一个可更新的状态模型,便于在每次绘制前对矩形进行重置和重新绘制。
const canvas = document.getElementById('boxCanvas');
const ctx = canvas.getContext('2d');const rect = { x: 120, y: 100, w: 200, h: 140 };
let dragging = false;
let resizing = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
2. 绘制矩形
2.1 使用原生 Canvas 绘制矩形
绘制矩形的核心在于两种形态:填充矩形和描边矩形。fillRect 提供填充,strokeRect 提供边框,组合使用能获得清晰的视觉效果。
在每次状态改变后,需要先清屏再进行重绘,以确保交互过程中的矩形不会产生残影。清屏+重绘是实现“实时反馈”的关键步骤。
function drawRect(r) {ctx.clearRect(0, 0, canvas.width, canvas.height); // 清屏// 填充矩形ctx.fillStyle = '#4CAF50';ctx.globalAlpha = 0.9;ctx.fillRect(r.x, r.y, r.w, r.h);// 边框ctx.lineWidth = 2;ctx.strokeStyle = '#333';ctx.strokeRect(r.x, r.y, r.w, r.h);ctx.globalAlpha = 1.0;// 绘制缩放把手(简易实现:只在角落绘制)drawHandles(r);
}function drawHandles(r) {ctx.fillStyle = '#fff';ctx.strokeStyle = '#000';const s = 8;// 左上角ctx.fillRect(r.x - s/2, r.y - s/2, s, s);ctx.strokeRect(r.x - s/2, r.y - s/2, s, s);// 右下角ctx.fillRect(r.x + r.w - s/2, r.y + r.h - s/2, s, s);ctx.strokeRect(r.x + r.w - s/2, r.y + r.h - s/2, s, s);
}
通过上述绘制封装,可以确保矩形在画布上的表现稳定,并为后续的拖拽与缩放提供可视参考点。保持绘制函数幂等,以方便在拖拽和缩放时快速重绘。
3. 实现拖拽
3.1 启动拖拽的条件
实现拖拽的第一步,是判断鼠标按下时是否落在矩形内部。若在矩形范围内按下,则进入拖拽状态,并记录相对位置以便在移动时保持与鼠标的偏移量一致。
在拖拽过程中,鼠标移动事件会更新矩形的位置,并在每次移动后重新绘制。为避免抖动,通常需要在 requestAnimationFrame 循环中触发重绘,确保流畅性。
// 判断点在矩形内部
function pointInRect(px, py, r) {return px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.h;
}// 事件绑定
canvas.addEventListener('mousedown', (e) => {const rectBounds = canvas.getBoundingClientRect();const mouseX = e.clientX - rectBounds.left;const mouseY = e.clientY - rectBounds.top;if (pointInRect(mouseX, mouseY, rect)) {dragging = true;dragOffsetX = mouseX - rect.x;dragOffsetY = mouseY - rect.y;}
});canvas.addEventListener('mousemove', (e) => {if (!dragging) return;const rectBounds = canvas.getBoundingClientRect();const mouseX = e.clientX - rectBounds.left;const mouseY = e.clientY - rectBounds.top;// 更新矩形位置rect.x = mouseX - dragOffsetX;rect.y = mouseY - dragOffsetY;drawRect(rect);
});canvas.addEventListener('mouseup', () => {dragging = false;
});// 初始绘制
drawRect(rect);
以上实现中,拖拽操作仅在鼠标按下并且在矩形内部时生效,从而避免误触发。为保证交互的稳定性,通常还需要在 mouseup 时重置状态。
4. 实现大小调整(拖拽缩放)
4.1 设置并检测缩放把手
为了实现矩形的大小调整,我们在矩形的四个角上添加缩放把手,并在鼠标按下时检测是否点击了某个把手。若按下的是把手,则进入缩放状态,随后根据鼠标移动来改变矩形的宽高。

缩放逻辑需要处理最小宽高、避免出现负值以及在缩放过程中重新绘制。边界判断和 最小尺寸的约束,是保证用户体验的关键。下面给出一个简化的实现框架。
function getHandleIndex(x, y, r) {const s = 8;// 左上角if (x >= r.x - s/2 && x <= r.x + s/2 && y >= r.y - s/2 && y <= r.y + s/2) return 0;// 右下角if (x >= r.x + r.w - s/2 && x <= r.x + r.w + s/2 && y >= r.y + r.h - s/2 && y <= r.y + r.h + s/2) return 1;return -1;
}canvas.addEventListener('mousedown', (e) => {const rectBounds = canvas.getBoundingClientRect();const mouseX = e.clientX - rectBounds.left;const mouseY = e.clientY - rectBounds.top;const idx = getHandleIndex(mouseX, mouseY, rect);if (idx === 0 || idx === 1) {resizing = true;activeHandle = idx;} else if (pointInRect(mouseX, mouseY, rect)) {dragging = true;dragOffsetX = mouseX - rect.x;dragOffsetY = mouseY - rect.y;}
});// 假设 activeHandle=0 表示左上,1 表示右下
canvas.addEventListener('mousemove', (e) => {const rectBounds = canvas.getBoundingClientRect();const mouseX = e.clientX - rectBounds.left;const mouseY = e.clientY - rectBounds.top;if (dragging) {rect.x = mouseX - dragOffsetX;rect.y = mouseY - dragOffsetY;drawRect(rect);return;}if (resizing) {if (activeHandle === 0) {// 左上角缩放const newW = rect.x + rect.w - mouseX;const newH = rect.y + rect.h - mouseY;rect.x = mouseX;rect.y = mouseY;rect.w = newW;rect.h = newH;} else {// 右下角缩放rect.w = Math.max(20, mouseX - rect.x);rect.h = Math.max(20, mouseY - rect.y);}drawRect(rect);return;}
});canvas.addEventListener('mouseup', () => {dragging = false;resizing = false;activeHandle = -1;
});
在上述代码的基础上,可以继续扩展为支持多把手(四个角、边中点等)以及触控设备的触摸事件,以让交互在手机端也同样顺畅。最小尺寸控制确保用户在缩放时不会让矩形变得不可用。
5. 进阶优化与兼容性
5.1 性能与跨设备兼容
为了提升性能,建议在每次交互前后仅绘制必要区域,或采用离屏 Canvas 做离屏绘制后再一次性绘制到主画布。离屏绘制可以显著降低重绘成本,特别是在复杂场景中。
兼容性方面,前端需要考虑 鼠标事件与触摸事件的统一处理。通过监听 touchstart、touchmove、touchend,并将触摸点映射到画布坐标,可以实现同样的拖拽与缩放体验。此外,设备像素比的处理依然不可省略,否则在移动端也可能出现模糊或偏移。
// 简单的触屏支持示例(事件映射为鼠标事件的统一处理)
function getPointerPos(e) {const rect = canvas.getBoundingClientRect();const x = (e.clientX ?? e.touches[0].clientX) - rect.left;const y = (e.clientY ?? e.touches[0].clientY) - rect.top;return { x, y };
}
canvas.addEventListener('touchstart', (e) => {const pos = getPointerPos(e);// 参考:在这里调用同样的逻辑
});
通过以上方法,原生 Canvas 的矩形绘制、拖拽和缩放可以在多端保持一致的交互体验。持续关注性能瓶颈、事件处理的节流以及重绘策略,是实现高质量前端交互的关键。
