前端 Canvas/WebGL 入门
Canvas 2D 和 WebGL 都是浏览器原生的图形绘制接口,选择标准只有一条:**画 2D 图形、元素数量在几千以内,用 Canvas 2D;要画 3D、或者单帧图元数量上万、需要 GPU 并行计算,用 WebGL**。两者的 JS 调用方式完全不同,Canvas 2D 是「命令式画图」,WebGL 是「配好管线上传数据」,新手最常见的错误是用 WebGL 的思维写 Canvas 2D,或者反过来。
Canvas 2D 和 WebGL 到底有什么区别?
结论:Canvas 2D 是 CPU 逐条执行绘图命令的立即模式接口,WebGL 是把顶点数据交给 GPU 的渲染管线接口。具体差异有四点:
- 坐标系统:Canvas 2D 原点在左上角,x 向右、y 向下,单位是 CSS 像素;WebGL 的裁剪空间是 x/y/z 都在 -1 到 1 之间,原点在画布中心。
- 编程模型:Canvas 2D 调用
fillRect、arc、drawImage这类方法,画完即生效;WebGL 必须先编译着色器(shader,跑在 GPU 上的小程序)、链接成 program,再通过attribute(逐顶点数据)、uniform(全局常量)、varying(顶点到片元之间插值的变量)三个通道传数据。 - 语言:Canvas 2D 只用写 JS;WebGL 必须写 GLSL(一种类 C 的着色器语言)。
- 生态:Canvas 2D 做图表、H5 小游戏、图片裁剪、签名的场景,用原生 API 就够;WebGL 实际项目基本都套库,2D 用 Pixi.js,3D 用 Three.js。
最小的 Canvas 2D 代码怎么写?
结论:三行就能画出东西。<canvas> 元素的默认尺寸是 300×150 像素,不设置宽高就直接按这个尺寸渲染。
<canvas id="c" width="400" height="300"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
ctx.fillStyle = '#09f';
ctx.fillRect(10, 10, 100, 80);
</script>
注意 width/height 是 canvas 的位图尺寸,和 CSS 里的 style.width 是两回事。只改 CSS 尺寸会让画面被拉伸模糊。
动画必须用 requestAnimationFrame 而不是 setInterval:前者跟随显示器刷新率(60Hz 屏幕约 16.7ms 一帧,120Hz 约 8.3ms),并且页面切到后台会自动暂停。Canvas 2D 是立即模式,每帧都要先 ctx.clearRect(0, 0, w, h) 清空再重画,否则图形会叠加。
为什么我的 Canvas 在高清屏上很模糊?
结论:因为没有按 devicePixelRatio(设备像素比,Retina 屏通常是 2 或 3)放大位图尺寸。CSS 写 400px 宽,实际需要 800 或 1200 个物理像素来填。
标准做法:
const dpr = window.devicePixelRatio || 1;
const w = 400, h = 300;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.scale(dpr, dpr); // 之后按 CSS 像素坐标画即可
这样设置后,你继续用 fillRect(10, 10, 100, 80) 这种逻辑坐标写代码,渲染自动变清晰。同样的坑在 WebGL 里也存在,gl.viewport(0, 0, canvas.width, canvas.height) 要传物理像素值。
WebGL 的第一个三角形要写多少代码?
结论:原生的第一个三角形大概 40 行,其中一半是样板代码。核心流程是固定的五步:
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')——先要 WebGL 2(等价于 OpenGL ES 3.0,Chrome 56+、Firefox 51+、Safari 15+ 支持),拿不到再退回 WebGL 1(等价于 OpenGL ES 2.0)。- 写两个着色器源码字符串。WebGL 1 的顶点着色器用
attribute vec2 a_pos;接收顶点,片元着色器用gl_FragColor输出颜色;WebGL 2 要加#version 300 es,改用in/out,gl_FragColor已移除,需要自己声明out vec4 fragColor;。 gl.createShader→gl.shaderSource→gl.compileShader,两个着色器各来一遍,务必检查gl.getShaderParameter(shader, gl.COMPILE_STATUS),编译失败时gl.getShaderInfoLog才有错误行号。- 链接成 program:
gl.createProgram→gl.attachShader×2 →gl.linkProgram→gl.useProgram。 - 传数据并绘制:把三角形三个顶点
[-0.5,-0.5, 0.5,-0.5, 0,0.5]放进Float32Array,绑定ARRAY_BUFFER,gl.vertexAttribPointer告诉 GPU 怎么读,最后gl.drawArrays(gl.TRIANGLES, 0, 3)。
每帧要 gl.clear(gl.COLOR_BUFFER_BIT),画布尺寸变化时要重新调用 gl.viewport。
该学原生 WebGL 还是直接用库?
结论:目标是交付项目就直接用库,目标是理解渲染原理才手写原生。两者不冲突,建议先手写一遍三角形,再切到库里。
- 2D 场景:Pixi.js 用 WebGL 加速,同屏渲染上万个精灵(sprite)依然能跑满 60fps,API 比原生 Canvas 2D 还简单。
- 3D 场景:Three.js 覆盖相机、光照、材质、模型加载,原生写同样的效果代码量是它的 10 倍以上。
- 图表场景:ECharts、Chart.js 默认走 Canvas 2D,数据量大时开
renderer: 'canvas'之外的 SVG 反而更慢,别乱切。 - 新方向:WebGPU 已在 Chrome 113(2023 年)默认开启,是 WebGL 的继任标准,但 Safari 和 Firefox 的完整支持仍在推进中,2024 年后新项目可以关注,暂时不作为主力。
性能优化必须知道的三个数字
结论:Canvas 2D 单帧 drawImage 调用超过约 1000 次、WebGL 单帧 draw call 超过约 100-300 次,就要考虑优化。
- 减少状态切换:Canvas 2D 里
fillStyle、strokeStyle的赋值有开销,把同色图形合并批量绘制。 - 离屏缓存:不变的内容(背景、静态文字)用
document.createElement('canvas')或OffscreenCanvas预渲染一次,之后每帧只drawImage一次。 - WebGL 侧:不要在渲染循环里
new Float32Array或创建 program,gl.bufferData分配好的 buffer 复用;图元多时用drawElements配索引缓冲,能省掉重复顶点。 - 逐帧检测:用 Chrome DevTools 的 Performance 面板录制 3-5 秒,看是 JS 执行超时还是 GPU 绘制超时,前者优化逻辑,后者减少绘制量。
总结一下:Canvas 2D 是画 2D 的首选,关键是处理 devicePixelRatio 和用 requestAnimationFrame 驱动;WebGL 的核心是着色器 + program + attribute 数据这条固定管线,原生写一遍再交给 Three.js 或 Pixi.js;性能上,Canvas 2D 控制单帧绘制次数,WebGL 控制 draw call 和每帧对象分配。选型的唯一判断依据是维度(2D/3D)和图元数量,不是哪个「更高级」。
原文链接:https://www.gj0.com/thread-617.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。