学完这篇,你能用原生 Canvas 从零画出柱状图、做一个流畅的循环动画,并对图片做像素级处理(灰阶、导出)。
第一步:拿到画布和上下文
页面里放一个 canvas 标签,用 JS 取 2D 上下文,所有绘制都通过它完成:
<canvas id="cv" width="600" height="400"></canvas>
<script>
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
ctx.fillStyle = '#2f6fed';
ctx.fillRect(20, 20, 120, 60);
</script>
width/height 是画布的像素缓冲区大小,CSS 里的宽高才是显示尺寸,两者不一致画面会模糊。
注意:不用 canvas 标签而用 CSS 撑大它,等于把 600×400 的图拉成 1200×800,一定糊。要改尺寸就改属性,别只改样式。
第二步:搞懂坐标系和路径
Canvas 原点在左上角,x 向右、y 向下,这点和数学课本相反,画图表时最容易翻车。
画线用路径三件套:beginPath → moveTo/lineTo → stroke(描边)或 fill(填充):
ctx.beginPath();
ctx.moveTo(50, 150);
ctx.lineTo(150, 50);
ctx.lineTo(250, 120);
ctx.strokeStyle = '#e5484d';
ctx.lineWidth = 4;
ctx.stroke();
注意:每次画新图形前都要 beginPath(),否则旧路径会被反复重绘,出现莫名连线。
第三步:实战——画一个柱状图
思路是:先算最大值做比例尺,再逐个 fillRect。
const data = [120, 200, 80, 260, 180];
const pad = 40, W = 600, H = 400;
const max = Math.max(...data);
const bw = (W - pad * 2) / data.length;
data.forEach((v, i) => {
const bh = (H - pad * 2) * v / max; // 高度按比例
const x = pad + i * bw + 10;
const y = H - pad - bh; // 从底部往上长
ctx.fillStyle = '#2f6fed';
ctx.fillRect(x, y, bw - 20, bh);
ctx.fillStyle = '#333';
ctx.fillText(v, x, y - 8); // 数值标注画在柱顶
});
fillText 的坐标是文字基线,不是文字顶部,看着偏高就再加几像素。
折线图同理:把每个点换算成坐标,循环 lineTo 后一次 stroke。
第四步:实战——requestAnimationFrame 做动画
动画的本质是「清屏 → 重画 → 下一帧」。
let x = 0;
function loop() {
ctx.clearRect(0, 0, 600, 400); // 必须先清屏
ctx.beginPath();
ctx.arc(x, 200, 30, 0, Math.PI * 2);
ctx.fillStyle = '#2f6fed';
ctx.fill();
x = (x + 3) % 600; // 越界回到起点
requestAnimationFrame(loop);
}
loop();
想要不同刷新率(60Hz / 120Hz)下速度一致,把位移写成基于时间戳的:
let last = 0;
function loop(t) {
const dt = t - last; last = t;
x += 0.15 * dt; // 每秒移动 150 像素
// ...绘制...
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
注意:漏掉 clearRect 会出现「拖影」;用 setInterval 代替 requestAnimationFrame 则会在后台标签页里白烧 CPU。
第五步:实战——图片像素处理
先把图画进画布,再取出像素数组改,最后放回去:
const img = new Image();
img.crossOrigin = 'anonymous'; // 跨域图片必须加
img.src = 'photo.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0, 600, 400);
const d = ctx.getImageData(0, 0, 600, 400);
for (let i = 0; i < d.data.length; i += 4) {
const g = d.data[i] * 0.3 + d.data[i + 1] * 0.59 + d.data[i + 2] * 0.11;
d.data[i] = d.data[i + 1] = d.data[i + 2] = g; // 灰阶
}
ctx.putImageData(d, 0, 0);
const url = cv.toDataURL('image/png'); // 导出成图片
};
数组每 4 个值是一组 R、G、B、A,改颜色只动前三个,透明度是第四个。
注意:跨域图片没配 CORS 会让画布被「污染」,调用 getImageData 或 toDataURL 直接抛安全错误。像素循环是逐点操作,2000×2000 以上先缩小再处理,否则页面会卡住几百毫秒。
第六步:高清屏适配
在 Retina 屏上按物理像素放大缓冲区,再用 scale 把坐标系还原:
const dpr = window.devicePixelRatio || 1;
cv.width = 600 * dpr;
cv.height = 400 * dpr;
cv.style.width = '600px';
cv.style.height = '400px';
ctx.scale(dpr, dpr);
这四行加完,后面所有代码仍然按 600×400 写,不用改。
小结
- 上下文只有 2D:
getContext('2d'),所有绘制都靠它。
- 原点在左上、y 轴向下,画图表时柱高要反着算。
- 路径绘制记住
beginPath → 画 → stroke/fill。
- 动画用
requestAnimationFrame,每帧先 clearRect,位移按时间差算。
- 像素处理靠
getImageData/putImageData,数据每 4 位一组。
- 跨域图片要给 CORS 头并设
crossOrigin,否则画布被污染。
- 高清屏用
devicePixelRatio + ctx.scale 适配,代码逻辑不用改。