从零实现 Canvas 图形拖拽:让你的网页动起来!

前言

最近在做一个可视化项目时,需要实现在 Canvas 上绘制矩形并支持拖拽功能。这个看似简单的需求,实际上涉及了不少前端技术细节。今天就来分享一下如何从零开始实现一个支持绘制、拖拽、自定义光标的 Canvas 应用。

最终实现的效果:

  • 🎨 点击空白区域绘制彩色矩形
  • 🖱️ 点击已有矩形进行拖拽移动
  • 🎯 精确的相对位置拖拽(不会跳到左上角)

技术栈

  • 原生 JavaScript
  • HTML5 Canvas API
  • CSS 自定义光标
  • SVG 图标

核心实现

1. Canvas 初始化与高 DPI 适配

javascript 复制代码
function init() {
  const w = 800,
    h = 500
  cvs.width = w * devicePixelRatio
  cvs.height = h * devicePixelRatio
  cvs.style.width = w + 'px'
  cvs.style.height = h + 'px'
}

关键点 :通过devicePixelRatio适配高 DPI 屏幕,避免图形模糊。

2. 矩形类设计

javascript 复制代码
class Rectangle {
  constructor(startX, startY, color) {
    this.startX = startX
    this.startY = startY
    this.color = color
    this.endX = startX
    this.endY = startY
    this.dragOffsetX = 0 // 拖拽偏移量
    this.dragOffsetY = 0
  }

  // 获取矩形边界
  get minX() {
    return Math.min(this.startX, this.endX)
  }
  get minY() {
    return Math.min(this.startY, this.endY)
  }
  get maxX() {
    return Math.max(this.startX, this.endX)
  }
  get maxY() {
    return Math.max(this.startY, this.endY)
  }

  // 检测点击是否在矩形内
  isInside(x, y) {
    return this.minX <= x && this.maxX >= x && this.minY <= y && this.maxY >= y
  }
}

3. 拖拽功能的核心难点

问题:拖拽时矩形跳到左上角

最初的 naive 实现:

javascript 复制代码
// ❌ 错误实现
move(newX, newY) {
  this.startX = newX
  this.startY = newY
  // 矩形会跳到鼠标位置的左上角
}

解决方案:记录相对偏移量

javascript 复制代码
// ✅ 正确实现
move(newX, newY) {
  const width = this.maxX - this.minX
  const height = this.maxY - this.minY

  // 考虑鼠标点击位置的偏移
  this.startX = newX - this.dragOffsetX
  this.startY = newY - this.dragOffsetY
  this.endX = this.startX + width
  this.endY = this.startY + height
}

4. 事件处理逻辑

javascript 复制代码
cvs.onmousedown = (e) => {
  const startX = e.offsetX
  const startY = e.offsetY

  const shape = getShape(startX, startY)
  if (shape) {
    // 拖拽模式:记录偏移量
    shape.dragOffsetX = startX - shape.minX
    shape.dragOffsetY = startY - shape.minY

    const rect = cvs.getBoundingClientRect()
    window.onmousemove = (e) => {
      const ex = e.clientX - rect.left
      const ey = e.clientY - rect.top
      shape.move(ex, ey)
    }
  } else {
    // 绘制模式:创建新矩形
    const shape = new Rectangle(startX, startY, colorPicker.value)
    shapes.push(shape)
    // ... 绘制逻辑
  }
}

5. 自定义彩色光标

系统默认光标无法修改颜色,解决方案是使用 SVG 自定义光标:

css 复制代码
.custom-crosshair {
  cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23ff0000" stroke-width="2"><path d="M12 2v20M2 12h20"/></svg>')
      12 12, crosshair;
}

.custom-grab {
  cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%230000ff" stroke-width="2"><path d="M9 11V7a3 3 0 0 1 6 0v4"/><path d="M12 12v9"/></svg>')
      12 12, grab;
}

技巧

  • 使用data:image/svg+xml内联 SVG
  • %23#的 URL 编码
  • 12 12指定光标热点位置

6. 智能光标切换

javascript 复制代码
cvs.onmousemove = (e) => {
  const x = e.offsetX
  const y = e.offsetY
  const shape = getShape(x, y)

  if (shape) {
    cvs.classList.remove('custom-crosshair')
    cvs.classList.add('custom-grab')
  } else {
    cvs.classList.remove('custom-grab')
    cvs.classList.add('custom-crosshair')
  }
}

踩坑记录

坑 1:坐标系统混乱

Canvas 有多套坐标系统:

  • e.offsetX/Y:相对于 Canvas 元素
  • e.clientX/Y:相对于视口
  • Canvas 内部坐标需要乘以devicePixelRatio

坑 2:事件监听器清理

javascript 复制代码
window.onmouseup = () => {
  window.onmousemove = null // 必须清理
  window.onmouseup = null // 避免内存泄漏
}

坑 3:拖拽偏移计算

关键是在mousedown时就计算好偏移量,而不是在mousemove时才计算。

性能优化

1. requestAnimationFrame 渲染循环

javascript 复制代码
function draw() {
  requestAnimationFrame(draw)
  ctx.clearRect(0, 0, cvs.width, cvs.height)
  for (const shape of shapes) {
    shape.draw(ctx)
  }
}

2. 事件委托

使用window监听mousemove避免频繁绑定/解绑事件。

3. 碰撞检测优化

javascript 复制代码
function getShape(x, y) {
  // 从后往前遍历,优先选择上层图形
  for (let i = shapes.length - 1; i >= 0; i--) {
    if (shapes[i].isInside(x, y)) {
      return shapes[i]
    }
  }
  return null
}

扩展思路

基于这个基础框架,可以扩展出更多功能:

  1. 多选功能:Ctrl+点击支持多选
  2. 缩放旋转:添加控制点实现图形变换
  3. 撤销重做:使用命令模式
  4. 图层管理:Z-index 排序
  5. 导出功能:toDataURL 导出图片
  6. 框选功能:矩形选择区域

总结

Canvas 拖拽看似简单,实际上需要考虑:

  • 坐标系统转换
  • 事件处理机制
  • 拖拽偏移计算
  • 性能优化
  • 用户体验细节

通过这个项目,我们不仅实现了基础功能,还学会了如何处理复杂的交互逻辑。Canvas 的强大之处在于给了我们完全的控制权,但也需要我们自己处理所有的细节。

希望这篇文章对正在学习 Canvas 开发的小伙伴有所帮助!如果你有任何问题或者更好的实现方案,欢迎在评论区讨论。

相关推荐
倔强青铜三7 分钟前
苦练Python第3天:Hello, World! + input()
前端·后端·python
上单带刀不带妹8 分钟前
JavaScript中的Request详解:掌握Fetch API与XMLHttpRequest
开发语言·前端·javascript·ecmascript
倔强青铜三25 分钟前
苦练Python第2天:安装 Python 与设置环境
前端·后端·python
我是若尘38 分钟前
Webpack 入门到实战 - 复习强化版
前端
晓131341 分钟前
JavaScript基础篇——第五章 对象(最终篇)
开发语言·前端·javascript
倔强青铜三42 分钟前
苦练Python第1天:为何要在2025年学习Python
前端·后端·python
满分观察网友z1 小时前
uniapp使用video实现沉浸式在线课程学习平台
前端
当牛作馬2 小时前
React——ant-design组件库使用问题记录
前端·react.js·前端框架
0wioiw02 小时前
Flutter基础(前端教程⑨-图片)
前端·flutter