切换明暗主题
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 的偏移量才是画布内坐标。
学习顺序建议
- 先画一个静态矩形和圆 → 熟悉坐标和 fill/stroke
- 画一个圆角矩形 → 理解路径和 quadraticCurveTo
- 加上文字 → fillText 和 font
- 封装成函数 → drawCard(x, y, ...)
- 加 requestAnimationFrame 循环 → 理解清屏重绘
- 加数据变化 → 理解动画原理
- 加鼠标事件 → 理解坐标转换
- 加 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,避免恢复后物体突然跳很远。