1. 菜鸟到老司机,先搞懂Canvas到底是个什么"东西"
先花三分钟把这层窗户纸捅破:Canvas不是绘图软件,也不是图形库,它只是一块由你完全掌控的像素画布。你可以用几行JavaScript告诉它"在这个坐标画一条线""在那个区域填一块颜色""每16毫秒把这帧画面重新画一遍"。市面上那些炫酷的图表库、流程图编辑器、小游戏、在线白板,底层全是这一套东西。
所以我一直跟身边人说,别一上来就急着抱各种库的大腿。很多场景你需要的可能只是原生Canvas那几百行代码,你抱着库反而被人牵着走,出现了一个诡异Bug,你连排查的入口都找不到。回到标题说的"100个实战示例",我是按"能跑、能改、能扩展"的标准来拆这批例子的,不是让你背100个孤立Demo,而是让你掌握几类核心套路,然后在套路上做组合。Canvas这东西,但凡你能随手写出基础图元、动画循环、事件坐标换算这三板斧,你已经能覆盖绝大多数日常需求了。
画布的核心就三样:坐标系、画笔状态、绘制命令。坐标系和数学课上的坐标系不一样,它的原点(0, 0)在画布左上角,x轴往右,y轴往下。新手最容易在这里犯迷糊,总觉得y轴应该往上走,结果画布上的图形全是"倒立"的。习惯就好,你可以把它理解成电子产品的屏幕坐标,从上往下数行,从左往右数列,天然适合画界面元素。
画笔状态则是Canvas很特别的设计。你设置了填充色、描边色、线条宽度,接下来的绘制命令都会用这套状态去画,直到你把它改掉。就好比你换了一支圆珠笔,后面写出来的字都是这个颜色,直到你再换一支。理解了这个"状态机"模型,很多代码就好读了:为什么前面明明设置了红色,后面却画出了蓝线?因为中间某个地方又设置了一次颜色。
再说绘制命令。2D上下文(通常写成ctx)提供了几十个API,但刨去高级玩法,基础的就那么几个:fillRect画方块、strokeRect画方块边框、beginPath开始一条路径、moveTo移动画笔、lineTo画直线、arc画弧线、fill填充、stroke描边。把这几个命令过成一串流程,你就能画出几乎所有你能想到的平面图形。
这里要特别强调一个概念:ctx.beginPath()太容易被漏掉。它表示"我要开始记录一条新路径了"。如果你不调用它,之前所有的线条点都会残留,你再stroke的时候,会连着一堆奇怪的线一起描出来。我见过很多新手在画多个图形的时候,发现图形间莫名其妙多出几条斜线,然后怀疑自己代码写错了,其实只是忘了调用beginPath。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三类基础图元吃透,画布就能玩出八成花样
2.1 线条:从moveTo到lineTo,把线段锚点理解透
画线本身很简单,但"线段锚点工具"这个热搜词暴露了一个真实需求:在线编辑图形的时候,用户看到的那些小方块就是锚点,拖动锚点就能改变形状。做这种工具的第一步,是先把一条线段的基本绘制流程走明白。
javascript复制const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// 画一条从(50, 100)到(300, 100)的直线
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.lineTo(300, 100);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();
moveTo相当于把笔抬起来移动到某一点,lineTo是落笔拉一条线到目标点。中间你随时可以穿插moveTo来断线,画多条互不相干的线段,这就是"锚点"的雏形。一个形状的锚点,本质就是一组坐标点数组,绘制的时候逐个连接,交互的时候监听鼠标点击,判断用户是否点中了锚点附近区域。
我做锚点拖动的时候习惯用一个hitRadius常量,比如12像素。鼠标坐标和锚点坐标的距离小于这个值,就认为用户选中了这个锚点。判断距离用勾股定理,没有更高级的写法:
javascript复制function distance(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}
锚点数量多起来之后,简单的遍历也完全够用。如果锚点上千个,可以提前做空间分割,把点按区域分桶,但日常的工具类应用完全没有性能压力,别过度设计。
2.2 矩形与圆形:Canvas里最基础的"块"与"球"
矩形在Canvas里有独立的API,但我更推荐用rect()方法加fill或stroke,因为rect()是路径命令,可以和其他路径组合成一个整体,而fillRect()是立即绘制的简化版,组合能力弱很多。
javascript复制ctx.beginPath();
ctx.rect(20, 20, 150, 100);
ctx.rect(200, 20, 150, 100); // 两个矩形在一个路径里
ctx.fillStyle = '#4A90D9';
ctx.fill();
圆形同样有两个选择:arc(x, y, radius, startAngle, endAngle)。注意角度单位是弧度,不是角度。一个常见的换算公式是度 * Math.PI / 180,你也可以直接记住:画一个完整的圆,角度范围是0到2 * Math.PI。
javascript复制ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = '#27AE60';
ctx.fill();
绘制圆形的时候,fill()和stroke()可以先后都调用,这样既填充又描边。但很多人会忽略一个顺序问题:如果你先fill()再改strokeStyle,也会生效,因为填充和描边是两条独立的绘制管线,各用各的状态。
圆的进阶玩法是画"饼图"。算法很简单:把百分比换算成弧度,从0度开始挨个扇区画弧线,然后闭合到圆心。这个技能能让你在没有任何图表库的情况下画出一个可交互的饼图,很多统计页面的核心就是这么来的。
2.3 路径:折线、曲线和"一笔画"的底层逻辑
路径是Canvas绘制中最灵活也是最重要的概念。前面讲moveTo和lineTo只能画直线,但实际项目中箭头、虚线、折线图、自由笔迹、贝塞尔曲线都靠路径。
所谓"一笔画",就是从一个起点开始,中间不断添加点和曲线段,最后统一填充或描边。这个特性让Canvas非常适合做涂鸦功能:鼠标按下时beginPath,鼠标移动时不断lineTo新坐标,鼠标抬起时收尾。
贝塞尔曲线是另一个高频考点。Canvas提供了quadraticCurveTo(二次贝塞尔)和bezierCurveTo(三次贝塞尔)。二次贝塞尔适合画平滑的弧形,比如聊天气泡的圆角尾巴;三次贝塞尔适合形状更复杂的曲线,比如流线型图标。
javascript复制ctx.beginPath();
ctx.moveTo(30, 200);
ctx.quadraticCurveTo(200, 20, 370, 200);
ctx.strokeStyle = '#E67E22';
ctx.lineWidth = 3;
ctx.stroke();
这段代码从(30, 200)出发,以(200, 20)为控制点,画一条终点在(370, 200)的平滑曲线。你可以把控制点想象成一根"拉线",它并不在曲线上,却决定了曲线往哪个方向弯、弯多深。多做几个控制点的调整实验,你的曲线手感会直线上升。
3. 让画面动起来:canvas动画与线段锚点工具的核心写法
3.1 requestAnimationFrame:动画循环的唯一标准答案
很多人第一次写动画,第一反应是setInterval。但Canvas动画的标准答案只有一个:requestAnimationFrame。原因有两条:一是它能自动匹配屏幕刷新率,每秒执行次数和显示器同步(通常是60次),不会因为定时器偏差导致画面闪烁或者卡顿;二是当页面切到后台时,浏览器会自动暂停动画循环,不再浪费CPU。
基本套路是这样:
javascript复制let x = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, 100, 30, 0, Math.PI * 2);
ctx.fillStyle = '#E74C3C';
ctx.fill();
x += 2;
if (x > canvas.width + 30) x = -30;
requestAnimationFrame(animate);
}
animate();
这套模板能覆盖90%的动画场景。注意三个关键点:每次动画开始要清除整块画布,否则上一帧的残影会留在原地;每次更新状态量(这里的x);在函数末尾递归调用requestAnimationFrame。
如果画面中有多个物体,建议维护一个对象数组,循环体里遍历数组更新和绘制。永远不要为每个物体单独开一个动画循环,否则性能会迅速崩掉,而且多个循环之间的时序也不好控制。
3.2 用状态机管理交互:点击、拖拽、松手
线段锚点工具的核心不止是绘制,更是交互。把拖拽流程拆开看,就是一个简单的状态机:空闲状态 → 鼠标按下后进入"拖动中"状态 → 鼠标抬起后回到空闲状态。虽然听起来简单,但很多初学者写交互就是一团乱麻,就是因为没有先定义"状态"。
我推荐一个极小状态机的写法:
javascript复制const state = {
dragging: false,
currentAnchorIndex: -1,
anchors: [
{ x: 100, y: 100 },
{ x: 200, y: 100 },
{ x: 200, y: 200 },
{ x: 100, y: 200 }
]
};
canvas.addEventListener('mousedown', (e) => {
const { x, y } = getCanvasCoords(e);
for (let i = 0; i < state.anchors.length; i++) {
if (distance(x, y, state.anchors[i].x, state.anchors[i].y) < 12) {
state.dragging = true;
state.currentAnchorIndex = i;
break;
}
}
});
canvas.addEventListener('mousemove', (e) => {
if (!state.dragging) return;
const { x, y } = getCanvasCoords(e);
state.anchors[state.currentAnchorIndex].x = x;
state.anchors[state.currentAnchorIndex].y = y;
});
canvas.addEventListener('mouseup', () => {
state.dragging = false;
state.currentAnchorIndex = -1;
});
这样结构一拆,后面每加一个新功能都往对应的事件函数里塞,代码不会烂。很多编辑器就是这么长出来的,先有一个画板,再有点击、拖拽、缩放、撤销,一点一点迭代成复杂工具。不要一上来就想做一个完整的图形编辑器,先把"画一个可拖拽矩形"跑通,这个地基打牢了,后面都是一层一层往上盖的事情。
3.3 事件坐标换算:canvas.getBoundingClientRect
写交互的时候有一个坑,十个新手九个踩:鼠标事件的clientX和clientY是相对于浏览器窗口的坐标,而Canvas的绘制坐标是相对于画布自身的。直接拿clientX当画布横坐标用,一旦页面有滚动、画布不是贴左上角,锚点就会漂移。
正确做法是拿getBoundingClientRect()把画布在视口里的位置算出来:
javascript复制function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
如果你的画布CSS尺寸和像素尺寸不一样(比如做了响应式缩放),那还需要再乘一个放大系数,这个我在避坑章节会专门讲。
4. 图像复制、压缩与离屏绘制:不只会画,还要会"搬运"
4.1 js获取canvas内容赋值给另一个canvas
"js获取canvas内容赋值给另一个canvas"这个热搜词,我在实际项目里遇到过好多次。需求通常是这样的:你已经在一个Canvas上画了内容,现在需要复制一份到另一个Canvas上展示,或者把A画布当成水印层、B画布当成内容层,最后合并成一张图。
最常见的做法是用drawImage。Canvas本身实现了CanvasImageSource接口,可以直接把另一个Canvas当作图片源画到目标画布上:
javascript复制const sourceCanvas = document.getElementById('source');
const targetCanvas = document.getElementById('target');
const targetCtx = targetCanvas.getContext('2d');
// 把sourceCanvas的内容整体绘制到targetCanvas的(0,0)位置
targetCtx.drawImage(sourceCanvas, 0, 0);
// 也可以先缩放再绘制
targetCtx.drawImage(sourceCanvas, 0, 0, 400, 300);
这个方法最大的好处是速度快,因为它走的是浏览器图形加速通道,不是逐像素搬运。你甚至可以把一个Canvas缩放后画到另一个Canvas上,实现"缩略图"功能,一行代码解决。
另一种方案是canvas.toDataURL()导出成图片字符串,再用new Image()加载,最后画到目标画布。这个方案多绕了几个弯,而且toDataURL()是同步操作,大尺寸画布下会阻塞主线程,体验并不好。但它的优势是可以直接生成可下载的图片,或者塞到表单里提交到服务器。
如果你需要做像素级修改,比如滤镜、抠色、翻转,那就要走getImageData / putImageData的组合。这个属于重操作,性能开销很大,后面单独说。
4.2 像素级操作与压缩:为什么不要迷信toDataURL
getImageData能拿到画布某一块区域的RGBA像素数组,每个像素对应四个元素:红、绿、蓝、透明度,取值都是0到255。这让你可以逐像素处理图片,做灰度、反色、锐化、模糊,全都可以实现。
但像素操作是Canvas性能杀手,对大图逐像素遍历很容易卡顿。一张1920x1080的图就有200多万个像素点,每次遍历都是几百万次计算。所以实际操作中,做像素处理之前一定要把图片缩小到你能接受的最小尺寸,处理完再按需放大。比如做图片压缩,更推荐的顺序是:先把大图缩小,再换成JPEG格式导出,而不是直接对原尺寸逐个像素处理。
再聊到"压缩"这个话题,我在图片上传功能里经常用Canvas做客户端压缩,核心就是三步:读取原始图片 → 按比例缩小绘制到画布 → toDataURL('image/jpeg', 0.8)导出。第二个参数是质量,范围0到1,0.8通常是在体积和清晰度之间比较平衡的选择。
javascript复制// 生成一个缩小后的图片
function compressImage(img, maxWidth, maxHeight, quality = 0.8) {
const canvas = document.createElement('canvas');
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
canvas.width = Math.round(img.width * ratio);
canvas.height = Math.round(img.height * ratio);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg', quality);
}
这个函数把任意大图等比缩放到不超过maxWidth和maxHeight的范围内,然后按JPEG格式压缩输出。为什么用JPEG而不是PNG?JPEG对照片类图片压缩率高很多,PNG更适合带透明背景的图形。实际业务里建议根据图片类型动态选择格式,一揽子处理不可取。
4.3 离屏Canvas:性能优化和"图层"思想
离屏Canvas是性能优化里非常推荐的一招。思路很简单:不直接把内容画到用户看得到的画布上,而是先画到一个看不到的临时画布上,然后把整个临时画布一次性画到主画布上。
举个具体场景:你要画一个非常复杂的背景,里面包含几百条网格线、渐变、多个文字。如果每帧都重画这些内容,性能会很难看。正确做法是:初始化时把背景画到离屏Canvas上,动画循环里每次先把离屏Canvas绘制到主画布,再在上面画动态物体。这样复杂背景只需要绘制一次,动画帧里最多一次大图拷贝。
javascript复制const offscreen = document.createElement('canvas');
offscreen.width = 800;
offscreen.height = 600;
const offCtx = offscreen.getContext('2d');
// 在离屏画布上画复杂背景
for (let i = 0; i < 800; i += 20) {
offCtx.beginPath();
offCtx.moveTo(i, 0);
offCtx.lineTo(i, 600);
offCtx.strokeStyle = '#eee';
offCtx.stroke();
}
// 主循环里直接drawImage
function frame() {
ctx.clearRect(0, 0, 800, 600);
ctx.drawImage(offscreen, 0, 0);
// 再画动态物体...
requestAnimationFrame(frame);
}
离屏Canvas还能当"图层"用。把静态层、交互层、选区层分开,交互层需要频繁重绘,静态层不用,这比一个画布上反复整体清空重画要高效得多。做白板、图形编辑器、地图标注这类功能尤其管用。
5. 鸿蒙小程序与课堂白板:真实业务里Canvas的适配与妥协
5.1 跨端Canvas的API差异与统一思路
网页端写Canvas,用的都是标准的getContext('2d')。但到了微信小程序、鸿蒙应用这些环境,情况就变了,API各有各的封装方式。最典型的是微信小程序的Canvas,早期版本是用wx.createCanvasContext来拿一个"异步上下文",很多方法调用后并不会立即生效,而是排队执行。新版小程序推荐用canvas.getContext('2d'),这跟Web端更一致,但要先通过wx.createSelectorQuery().select('#canvas').fields({ node: true })来获取节点对象,绕了一圈才拿到真正的Canvas节点。
鸿蒙侧适配又是另一套。我在跨端项目里的经验是:把Canvas相关操作抽成一个独立模块,上层业务只调用你自己封装的接口,不要直接依赖某个平台的Canvas API。比如你封装一个createCanvas(id)、drawRect(options)、toDataURL(),内部再根据运行环境走不同分支。这样就算平台API再怎么改,底层适配只需要改一处,不会牵连整个业务代码。
5.2 鸿蒙系统使用微信小程序Canvas压缩图片的真实流程
"鸿蒙系统使用微信小程序canvas压缩"这个场景,我拆开来讲。在小程序里,如果你用Canvas压缩一张图片,流程通常是:wx.chooseMedia选图 → 创建Canvas并设置尺寸 → 用ctx.drawImage把图片绘制到Canvas → wx.canvasToTempFilePath导出压缩后的临时文件。核心步骤跟Web端类似,但有几个平台特性要留意。
第一,小程序的Canvas初始化是异步的,必须在ctx.drawImage之前确保Canvas节点已经渲染好,否则拿不到画布上下文。第二,从相册选出来的图片有真实的宽高,但也要考虑方向信息,部分机型拍出来的照片是带EXIF旋转的,画之前要处理旋转,不然压缩出来的图是横着的。第三,wx.canvasToTempFilePath导出的图片质量参数是quality,只对JPEG格式有效,PNG不支持,这个坑我也踩过。
鸿蒙系统上跑小程序Canvas,本质是运行时的差异化支持。你不能假设所有API行为和微信在iOS/Android上完全一致。在真机上多验证几遍是绕不开的。我的做法是写一个Canvas适配层,把"创建画布""绘制图片""导出文件"这三个步骤封装好,遇到多端差异就在这一层打补丁。业务代码里不出现平台API,后面改起来会舒服很多。
5.3 课堂白板类工具:Canvas的"轻量UI"实践
"课堂小助手"和"课堂白板"这类应用是Canvas的高频使用场景。白板最核心的功能就是手写笔迹和基础图形绘制。手写笔迹在技术层面就是:监听touch或pointer事件,在pointerdown时beginPath,在pointermove时不断lineTo并stroke。
但直接这么做有个致命问题:重绘太快会导致笔迹断裂或过于平滑度不够。常规优化是记录上次坐标,在两次pointermove之间补一条线段,而不是只画一个点。另一个优化是使用ctx.lineCap = 'round'和ctx.lineJoin = 'round',这样线条两端的接缝处是圆角过渡,看起来像一笔连贯写下来的。
白板里如果要画直线、矩形、圆形等基础图形,套路是:pointerdown时记录起点,pointermove时动态绘制"预览图形"(每次都清空重画),pointerup时把最终形状写入"已固定图形列表"。这种临时预览加最终固定的模式,是所有图形编辑器的统一形态,跟桌面端Visio、Figma的思路一致。
Canvas还能用来做UI,比如自定义进度环、签名板、图形验证码。这类功能用DOM也能做,但Canvas更适合需要极致控制像素效果的场景。签名板就是典型的例子:用Canvas获取用户触摸轨迹,最后转成图片上传,比CSS方案要稳定得多。
6. Canvas避坑清单:我踩过的坑,你今晚就能绕开
6.1 画布模糊:把devicePixelRatio算进去
同一个Canvas,在普通笔记本和Retina屏上显示效果可能完全不一样:文字模糊、线条发虚。原因是CSS像素和设备物理像素之间存在一个比例系数,叫devicePixelRatio。如果你只用CSS把画布撑成300x150,实际画布像素也是300x150,但在高清屏上,浏览器会用600x300物理像素去显示这300x150的尺寸,等于每个逻辑像素被复制成2x2,看起来就糊了。
解决办法是让画布物理尺寸乘以devicePixelRatio,同时用CSS尺寸保持渲染大小不变:
javascript复制const dpr = window.devicePixelRatio || 1;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
ctx.scale(dpr, dpr);
这段代码做完之后,你在逻辑坐标里的所有绘制(比如ctx.arc(100, 100, 50))依然以CSS像素为单位,但底层像素数足够多,画面就清晰锐利了。这几乎是Canvas应用必备的一步,谁不做,谁家的画布就要被吐槽"糊"。
6.2 clearRect清不干净:锚点残影与半透明叠加
动画循环里清空画布,写的是ctx.clearRect(0, 0, canvas.width, canvas.height),理论上应该清干净。但你是不是遇到过这样的问题:清不干净,总有淡淡的残影?
原因通常是状态问题。如果当前globalCompositeOperation不是source-over,而是multiply或lighter这类混合模式,clearRect的行为会变得不可控。另一个常见原因是:你的绘制顺序是先画一个半透明覆盖层再画内容,每帧叠加下来就会越来越深。
排查方法很直接:检查动画循环开头是否真的调用了clearRect,以及是否在clearRect后重置了所有可能被污染的状态(globalAlpha、globalCompositeOperation、transform)。如果你在循环里调用了ctx.save(),那也要检查restore()是否成对出现,否则状态会越积越多。
6.3 图形模糊/位移:坐标没有取整
Canvas绘制时,如果要画的坐标是整数,线条落在像素点上,看起来很清晰;但如果坐标带小数,比如x = 100.5,浏览器会做抗锯齿处理,线条边缘就会发虚。绘制大量图形的时候,这些小数坐标累积起来,视觉上就是整个画面"蒙了一层雾"。
解决方案是在绘制之前对所有坐标做取整。最方便的做法是在坐标上加上0.5:ctx.moveTo(Math.round(x) + 0.5, y + 0.5)。为什么加0.5?因为1像素宽的线条是画在坐标对应像素的中心,整数坐标会让线条横跨两个像素各占一半,加上0.5后正好落在单个像素中心,线条更实更锐利。
不过这也不是绝对真理。如果你画的是2像素宽的线条,取整规则又不一样。我的习惯是:在大量绘制之前统一对关键坐标取整,然后观察效果对比,而不是盲目到处加0.5。取整影响的是"边缘锐度",对"内容正确性"没有影响,所以不用纠结在逻辑上,慢慢调。
6.4 文字位置不对:baseline的坑
用Canvas画文字,最容易被忽略的是文字基线(baseline)。DOM里的文字天然是垂直方向居中对齐的,但Canvas的默认textBaseline是alphabetic,也就是说文字的底部会贴着y坐标,而不是整个文字的中线贴着y坐标。这会导致你写"上下左右居中"的代码,画出来的文字位置偏上或偏下,尤其在不同字体、不同字号下偏移还不一样。
如果你想要的是文字在某个矩形内居中对齐,推荐这样设置:
javascript复制ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('你好', x, y);
这三个配合,文字会以(x, y)为中心点水平垂直居中,绝大多数场景下这就是你要的效果。做图形标注、饼图中心数字、按钮上的文案,都建议用这套配置。
6.5 内存泄漏与性能陷阱:大图、离屏Canvas、事件监听
Canvas的内存和安全我把它放在避坑清单的最后,因为这块的问题通常不是一闪而过的Bug,而是页面越来越卡、内存越占越大的慢性病。
第一,不要在循环里反复createElement('canvas')。每创建一张画布就会分配一块内存,不用的画布要及时把引用置空,让垃圾回收能回收它。尤其是做图像批量处理时,很多人的代码会在循环里为每张图创建一个新Canvas,处理完又没释放,很快就卡了。复用那一个离屏Canvas,每次drawImage进去处理完导出,效率能翻好几倍。
第二,getImageData后一定要显式putImageData或把ImageData对象置空。大尺寸像素数组占用的内存很可观,停留在作用域里不释放,内存就会持续爬高。
第三,事件监听器要记得移除。尤其在组件化开发里,一个组件实例销毁了,但它的mousedown、mousemove监听还挂在全局对象上,等于每次交互都在调用一个已经销毁的实例方法。轻则内存泄漏,重则控制台刷一堆异常。在组件卸载时统一removeEventListener,是最省心的习惯。
第四,drawImage如果每次都从零加载一张大图片,会有解码开销。建议把图片对象缓存起来,只解码一次,后续复用。尤其做帧动画贴图时,这个优化可以显著降低卡顿概率。
6.6 字体未加载就绘制:文字"跳动"的秘密
最后分享一个比较隐蔽的坑:Canvas在页面刚加载时就绘制文字,但字体文件还没加载完成,浏览器会先用默认字体渲染,等字体加载完你再重绘,文字就突然变了样。这种"跳动"在Canvas动画里特别明显。
解决思路是在字体加载完成后触发重绘。浏览器提供了document.fonts.ready这个Promise,可以等它resolve后再绘制:
javascript复制async function drawWithFont() {
await document.fonts.ready;
// 这里再执行Canvas文字绘制
}
如果你的字体是自定义图标库或特殊字体,也可以直接用document.fonts.load('16px FontName')来主动触发加载。这个细节不解决,你做的统计图表的标题、大屏上的数字,每次刷新都会先闪出默认字体再跳变到目标字体,体验很扣分。
Canvas这套技术,说简单也简单,说深也深。简单到几行代码就能画一个圆,深到你可以用像素级操作写出一个图片处理引擎。我对它的态度一直是:先把手上的场景做好,遇到瓶颈再往底层钻。别为了炫技过度设计,也别因为怕性能不敢用。它毕竟只是一块画布,画什么、怎么画,最终都是你说了算。把这篇文章里的几个核心套路跑通,你已经是个能独立解决实际问题的Canvas开发者了。
