Skip to content

Canvas 2D 基础知识 — 实现监控面板前必学 ​


1. Canvas 基本结构 ​

html
<canvas id="canvas" width="800" height="500"></canvas>
<script>
  let canvas = document.getElementById('canvas')
  let ctx = canvas.getContext('2d')
</script>
  • <canvas> 只是一个画布,本身什么都没有
  • 所有绘制都通过 ctx(上下文)来操作
  • width/height 是画布尺寸,不是 CSS 尺寸

2. 坐标系 ​

(0,0) ———→ x
  |
  |
  ↓ y
  • 原点在左上角
  • x 向右增大
  • y 向下增大(和数学相反)
  • 画布 800x500 意味着:x 范围 0~800,y 范围 0~500

3. 矩形绘制 ​

js
// 填充矩形
ctx.fillStyle = 'red'
ctx.fillRect(100, 100, 200, 80)  // x, y, 宽, 高

// 描边矩形
ctx.strokeStyle = 'blue'
ctx.lineWidth = 2
ctx.strokeRect(100, 100, 200, 80)

// 清除矩形区域(变透明)
ctx.clearRect(100, 100, 200, 80)

fillRect 和 strokeRect 直接画,不需要 beginPath。


4. 路径绘制(画任意形状) ​

js
ctx.beginPath()       // 开始一条新路径
ctx.moveTo(100, 50)   // 把画笔移到起点
ctx.lineTo(50, 150)   // 画直线到这个点
ctx.lineTo(150, 150)  // 再画直线到这个点
ctx.closePath()       // 闭合路径(自动画回起点)
ctx.fill()            // 填充
ctx.stroke()          // 描边

流程:beginPath → moveTo/lineTo → closePath → fill/stroke


5. 圆角矩形(二次贝塞尔曲线) ​

js
function roundRect(x, y, w, h, r) {
  ctx.beginPath()
  ctx.moveTo(x + r, y)
  ctx.lineTo(x + w - r, y)
  ctx.quadraticCurveTo(x + w, y, x + w, y + r)       // 右上角
  ctx.lineTo(x + w, y + h - r)
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h) // 右下角
  ctx.lineTo(x + r, y + h)
  ctx.quadraticCurveTo(x, y + h, x, y + h - r)       // 左下角
  ctx.lineTo(x, y + r)
  ctx.quadraticCurveTo(x, y, x + r, y)               // 左上角
  ctx.closePath()
}

quadraticCurveTo(控制点x, 控制点y, 终点x, 终点y) 画曲线,四个角各用一条。

调用方式:

js
roundRect(50, 50, 200, 100, 10)  // 圆角半径 10
ctx.fillStyle = '#1a2742'
ctx.fill()
ctx.strokeStyle = '#2c3e50'
ctx.stroke()

6. 圆形(arc) ​

js
ctx.beginPath()
ctx.arc(x, y, radius, startAngle, endAngle)
ctx.fill()
ctx.stroke()
  • startAngle 和 endAngle 用弧度,不是角度
  • 画完整圆:0 到 Math.PI * 2
  • 弧度换算:角度 × Math.PI / 180
js
// 画一个圆
ctx.beginPath()
ctx.arc(100, 100, 20, 0, Math.PI * 2)
ctx.fillStyle = '#27ae60'
ctx.fill()

7. 文字绘制 ​

js
ctx.font = 'bold 16px Microsoft YaHei'  // 粗细 大小 字体
ctx.fillStyle = '#fff'
ctx.textAlign = 'center'       // left / center / right
ctx.textBaseline = 'middle'    // top / middle / bottom
ctx.fillText('设备名称', 100, 50)
  • fillText(文字, x, y) — 填充文字
  • strokeText(文字, x, y) — 描边文字(空心字)
  • textAlign 控制 x 位置是文字的左/中/右
  • textBaseline 控制 y 位置是文字的顶/中/底

8. 样式设置 ​

js
// 颜色(写在 fill/stroke 之前)
ctx.fillStyle = '#27ae60'        // 填充色
ctx.strokeStyle = '#e74c3c'      // 描边色

// 线宽
ctx.lineWidth = 2

// 透明度(0~1)
ctx.globalAlpha = 0.5

// 恢复透明度
ctx.globalAlpha = 1.0

注意:fillStyle 和 strokeStyle 互不影响,各自独立。


9. 变换(translate / scale / rotate) ​

平移 ​

js
ctx.translate(100, 50)  // 把原点移到 (100, 50)
// 之后所有坐标都是相对于新原点

缩放 ​

js
ctx.translate(150, 150)   // 移到目标中心
ctx.scale(1.1, 1.1)       // 放大 1.1 倍
ctx.translate(-150, -150)  // 移回来

悬停放大效果的原理:以目标中心为原点 → 放大 → 移回来

保存和恢复状态 ​

js
ctx.save()         // 保存当前状态(变换、样式等)
ctx.translate(100, 50)
ctx.scale(1.1, 1.1)
// ... 绘制 ...
ctx.restore()      // 恢复到 save 时的状态

save/restore 必须成对使用,变换前 save,变换后 restore。


10. 清屏 + 重绘循环 ​

js
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)  // 清除整个画布

  // 重新绘制所有内容
  drawBackground()
  drawCards()
  drawText()

  requestAnimationFrame(render)  // 请求下一帧
}

render()  // 启动循环

每一帧都是:清屏 → 重画 → 请求下一帧


11. 动画原理 ​

动画就是每帧改变一点数值,然后重绘:

js
let x = 0

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  x += 2                    // 每帧移动 2 像素
  ctx.fillRect(x, 50, 40, 40)

  requestAnimationFrame(render)
}

render()
  • 约 60 帧/秒,所以 x += 2 大约每秒移动 120 像素
  • 用 时间差 控制速度更精确:
js
let lastTime = 0
function render(timestamp) {
  let dt = (timestamp - lastTime) / 1000  // 转为秒
  lastTime = timestamp

  x += 100 * dt  // 每秒移动 100 像素
  // ...
}

12. 鼠标事件 ​

js
canvas.addEventListener('click', function(e) {
  let rect = canvas.getBoundingClientRect()
  let mx = e.clientX - rect.left   // 鼠标在 canvas 内的 x
  let my = e.clientY - rect.top    // 鼠标在 canvas 内的 y

  // 判断是否点中某个区域
  if (mx >= 100 && mx <= 300 && my >= 100 && my <= 200) {
    console.log('点中了!')
  }
})

canvas.addEventListener('mousemove', function(e) {
  // 同样的方式获取坐标
})

关键:e.clientX 是相对浏览器窗口的,要减去 canvas 的偏移量才是画布内坐标。


学习顺序建议 ​

  1. 先画一个静态矩形和圆 → 熟悉坐标和 fill/stroke
  2. 画一个圆角矩形 → 理解路径和 quadraticCurveTo
  3. 加上文字 → fillText 和 font
  4. 封装成函数 → drawCard(x, y, ...)
  5. 加 requestAnimationFrame 循环 → 理解清屏重绘
  6. 加数据变化 → 理解动画原理
  7. 加鼠标事件 → 理解坐标转换
  8. 加 save/restore + scale → 理解变换

补充:尺寸适配与高 DPI 注意事项 ​

以下内容是在原始笔记基础上的补充,原稿中的 12 个主题、代码示例、注意事项和练习顺序均保留在上方。

CSS 尺寸、画布位图尺寸与清晰度 ​

canvas.width 和 canvas.height 决定内部位图分辨率,CSS 宽高决定页面显示尺寸。两者不一致时浏览器会缩放位图,可能使图形模糊。高 DPI 屏幕可按设备像素比扩大位图,同时继续使用 CSS 像素绘制:

js
function resizeCanvas(canvas, ctx) {
  const width = canvas.clientWidth
  const height = canvas.clientHeight
  const dpr = window.devicePixelRatio || 1

  canvas.width = Math.round(width * dpr)
  canvas.height = Math.round(height * dpr)
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
}
css
canvas {
  display: block;
  width: 100%;
  height: 400px;
}

注意:给 canvas.width 或 canvas.height 赋值会清空画布并重置上下文状态。 容器大小变化时要重新设置尺寸、初始化绘图状态并重绘。高 DPR 也会增加位图像素数和内存占用,不必无条件使用很大的倍率。

鼠标坐标换算的补充 ​

原稿中的 e.clientX - rect.left 适用于绘图坐标按 CSS 像素使用的情况。若绘图坐标直接使用内部位图像素,需要乘以画布位图尺寸与 CSS 显示尺寸之比:

js
const rect = canvas.getBoundingClientRect()
const x = (e.clientX - rect.left) * canvas.width / rect.width
const y = (e.clientY - rect.top) * canvas.height / rect.height

命中检测与绘图必须使用同一坐标空间。若还应用了平移或缩放变换,命中测试也要把指针坐标转换到相同的逻辑坐标空间。

动画清理 ​

如果页面可能暂停或销毁动画循环,应保存 requestAnimationFrame 返回的 ID,并在不需要时调用 cancelAnimationFrame(id),避免无用的持续绘制。需要处理窗口切换或长时间暂停时,可限制单帧的 dt,避免恢复后物体突然跳很远。