你搜“Canvas字体大小怎么调”,大概率会看到一堆让你改DBeaver界面字号、给Word批注调字号的教程。它们都很热,但不是你要的答案。这里的“Canvas”,指的是HTML5页面上用来绘制图形的那个<canvas>元素,以及它背后的Canvas 2D上下文——在游戏画面里显示得分、在数据大屏上画动态标签、在图片合成工具里写宣传语,都得靠它把文字画出来。文字画出来本身不复杂,真正让无数人踩坑的,是“字号怎么设置才有效”“怎么让文字自动适应宽度”“高分屏下为什么总是发虚”,以及更扎心的——“明明设置了字体大小,输出却和预期完全不一样”。
在进入实操之前,先把范围框定一下:这篇要解决的问题,是开发者向的Canvas字体大小调整,包括font属性的正确写法、字号动态计算的原理、高清屏适配方案,以及与此相关的常见排查。如果你是Web前端、做Canvas小游戏、搞数据可视化大屏,或者写图片合成类页面,直接参照后面几节就行。其它像Canva设计工具、Canvas学习平台这类同名产品不在讨论范围内,它们的差异我放到第三节专门说明。
1. 先搞清楚:Canvas的字体大小到底存在哪里
1.1 默认字号是10px,不是你以为的16px
很多第一次接触Canvas的人都会产生一个错觉:既然页面里的文字默认是16px,那fillText画出来的文字也应该是16px。事实并非如此。
Canvas 2D上下文的默认字体值,规范里写得清清楚楚:10px sans-serif。也就是说,你什么都不设置,直接调用ctx.fillText('Hello', 0, 20),画出来的是10px的细长小字,和浏览器页面的默认字号没有任何继承关系。页面里CSS定义的font-size、font-family,不会自动传导到Canvas上。
这个“10px默认值”是很多问题的根源。有人写了半天代码,发现文字小到看不清,第一反应是“Canvas对字体大小有限制”,实际上只是因为没有显式设置ctx.font。记住一条原则:在Canvas里,每一帧、每一次绘制,你对字体样式的要求都要自己声明,浏览器不会帮你“记住”页面里已有的样式。
1.2 ctx.font是一整串CSS简写,不是数字
Canvas没有ctx.fontSize这个属性。网上不少旧代码会写ctx.fontSize = 30,这行代码在浏览器里不会报错,但也绝对不会生效,因为你等于给一个不存在的属性赋了值,JavaScript引擎只是默默创建了一个新属性然后丢掉。
正确的写法是设置ctx.font,而且它的值是一个完整的CSS字体简写字符串。最基础的形式是“字号+空格+字体族”:
ctx.font = '30px Arial'; ctx.fillText('Hello World', 20, 60);这里30px是字号,Arial是字体族。也可以加上字重、字体风格:
ctx.font = 'bold 30px Arial'; // 加粗 ctx.font = 'italic 30px "Courier New"'; // 斜体,注意字体名带空格要加引号 ctx.font = 'bold italic 20px/1.5 "Microsoft YaHei"'; // 字重+风格+字号/行高+字体族CSS的font简写语法中包含font-style、font-variant、font-weight、font-size、line-height、font-family,Canvas里的ctx.font基本沿用了这套规则。但要注意,line-height对fillText没有实际意义,fillText只关心基线和坐标,不参与行盒布局。
为什么明明可以直接用一个数字属性,Canvas偏要用一整串字符串?原因是Canvas设计时选择了和CSS尽量对齐,这样前端开发者可以把CSS里调好的字体样式直接平移到Canvas上,比如用canvasContext.font = window.getComputedStyle(domElement).font把某个DOM元素的完整字体样式复制过来。当然,这也带来了新的坑,请看下一节。
1.3 格式错了不报错,只会默默不生效
如果你在CSS里写错了font属性,浏览器会在控制台给你报错,或者直接拒绝应用。但在Canvas里,ctx.font = '任意字符串'基本不会抛异常。当字符串无法被解析成合法的CSS字体简写时,Canvas会忽略这次赋值,继续沿用上一次的字体设置。
看几个典型的无效写法:
ctx.font = 'bold'; // 没有字号和字体族,无效 ctx.font = '30px'; // 没有字体族,无效 ctx.font = 'Arial 30px'; // 顺序反了,字号要在字体族前面,无效 ctx.font = 30; // 类型不对,必须字符串 ctx.font = '30'; // 缺单位、缺字体族,无效这些代码都不会报错,问题就是你的文字大小完全没变化。最迷惑人的是ctx.font = 'Arial 30px'这种,看着好像没问题,字体名在前、字号在后,但你拿它去浏览器里跑,文字永远是上一次的样式。因为CSS font简写的语法要求必须是“字号在前、字体族在后”,顺序错了整个声明就是非法的。
排查这类问题,最快的办法是在赋值后马上读一下ctx.font,看浏览器实际保存了什么值。比如你写了错误写法,再执行console.log(ctx.font),返回的往往还是'10px sans-serif',说明赋值被无视了。
2. 三种靠谱的Canvas字体大小设置方法
2.1 直接设置ctx.font:最基础也最常用
第一种方法,也是绝大多数场景够用的方法,就是直接给ctx.font赋值:
const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // 先设置字体,再绘制文本 ctx.font = '28px Arial, sans-serif'; ctx.fillStyle = '#333'; ctx.fillText('得分: 100', 10, 30);两点经验:
一是ctx.font设置的是一个全局状态,和fillStyle、strokeStyle一样。如果你在循环里反复绘制不同大小的文字,要注意当前set的值会一直保留,直到下一次被覆盖。比如绘制完大标题,后面画小注释文字时忘了重新设置ctx.font,注释会继续用大标题的字号,推荐的做法是在每次绘制文本前都显式设置字体,或者把字体字符串封装成函数:
function setFont(ctx, px) { ctx.font = `${px}px Arial, sans-serif`; }二是如果有大量文字需要重复绘制,可以把字体字符串预先拼好,避免每次循环都做字符串拼接。虽然现代JavaScript引擎对字符串性能优化得不错,但Canvas游戏一帧可能要绘制几十段文本,堆在一起仍然有可感知的开销。
2.2 用measureText动态计算字号:文字自动适配宽高
直接指定固定字号很简单,但真实需求往往更刁钻:文字要适配宽度、高度、或者既不能超出某个矩形区域,又要尽量大。这时候就要靠measureText方法了。
ctx.measureText(text)返回一个TextMetrics对象,其中width属性表示在当前字体下,这段文字渲染出来的实际宽度(单位是像素)。这个宽度是所有字形宽度的总和,不是简单的“字符数x字号”。同样是10个字符,'iiiiiiiiii'和'WWWWWWWWWW'的宽度差了快十倍。
利用measureText,就能实现“文字自适应容器宽度”的核心逻辑:
function fitTextToWidth(ctx, text, maxWidth, fontFamily = 'Arial') { let fontSize = 20; // 初始尝试的字号 ctx.font = `${fontSize}px ${fontFamily}`; let width = ctx.measureText(text).width; // 如果初始字号已经超宽,就等比例缩小 if (width > maxWidth) { fontSize = Math.floor(fontSize * (maxWidth / width)); ctx.font = `${fontSize}px ${fontFamily}`; width = ctx.measureText(text).width; // 由于字体宽度不是完全线性的,缩完之后再微调一次 while (width > maxWidth && fontSize > 1) { fontSize--; ctx.font = `${fontSize}px ${fontFamily}`; width = ctx.measureText(text).width; } } return fontSize; }这段代码的思路很简单:先量出当前字号的宽度,按“目标宽度/实际宽度”的比例算出新字号。因为同一字体下,字符宽度基本和字号成正比,所以一次缩放基本能到位。后面那个while循环是兜底,处理个别字体的非线性情况。
如果是高度也要受限,比如文字必须放在一个高100px的区域内,那就不能只用measureText的宽度了。高度要结合字号估算,一般一行文本的视觉高度大约是字号的1.2到1.4倍。更严格的做法是用二分查找,在最小字号和最大字号之间不断尝试,找到满足条件的最大字号:
function findBestFitFont(ctx, text, maxWidth, maxHeight, minFontSize = 8, maxFontSize = 100, fontFamily = 'Arial') { let low = minFontSize; let high = maxFontSize; let best = minFontSize; while (low <= high) { const mid = Math.floor((low + high) / 2); ctx.font = `${mid}px ${fontFamily}`; const width = ctx.measureText(text).width; const lineHeight = mid * 1.2; // 近似行高 if (width <= maxWidth && lineHeight <= maxHeight) { best = mid; low = mid + 1; } else { high = mid - 1; } } ctx.font = `${best}px ${fontFamily}`; return best; }二分查找的次数是log2(100-8),大约7次,性能上完全没问题,而且结果比等比例缩放更稳。实际开发中,图表库给轴标签、形状内文字做自适应时,用的基本都是这个思路。
2.3 高清屏适配:canvas.width和ctx.scale怎么配合
在普通分辨率屏幕上,文字显示是清晰的;一换到高分辨率屏幕(Retina、4K屏),Canvas画出来的字就发虚、有锯齿。这不是字体设置的问题,而是Canvas内部明明有高分辨率能力,你却没用起来。
原理是这样的:Canvas画布的位图尺寸(canvas.width和canvas.height)决定了它有多少个物理像素。而CSS样式里的宽度(canvas.style.width)决定了它在页面上占据多大尺寸。如果两者一样,在DPR为2的高分屏上,浏览器就要用1个位图像素去模拟2个物理像素,结果就是发虚。
解决办法,是让画布的物理像素等于“CSS像素 x devicePixelRatio”,然后用ctx.scale把坐标系缩放回CSS像素尺度:
const dpr = window.devicePixelRatio || 1; const cssWidth = 300; const cssHeight = 150; canvas.width = cssWidth * dpr; canvas.height = cssHeight * dpr; canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; ctx.scale(dpr, dpr); ctx.font = '24px Arial'; // 按照CSS像素的24px来写 ctx.fillText('高清的文字', 10, 40);关键在于:当你把canvas.width设成600(300 CSS像素 x DPR 2)后,坐标系里的1个单位就代表1个物理像素。如果没有ctx.scale(2, 2),你写font = '24px Arial',画出来的字实际只有12个逻辑像素那么大,文字反而变小了;加上ctx.scale(2, 2)后,坐标系的单位重新映射回CSS像素,你写的24px才是页面上看到的24px,而这24px实际上由48个物理像素去渲染,自然而然就清晰了。
高清屏适配是一个容易“一改就乱”的环节。我自己的经验是:把DPR适配逻辑封装成一个单独的函数,每次动态调整大小时统一调用,不要散落在各个绘制分支里。
3. 那些让你怀疑人生的“同名Canvas”问题
3.1 你搜的可能是Canva、Canvas LMS,而不是HTML Canvas
“Canvas字体大小怎么调”这个话题下,搜索结果的混乱程度远超想象。因为“Canvas”这个拼写,同时对应了至少三个完全不相干的东西:
一是HTML5的<canvas>元素,也就是本文讨论的核心对象。
二是Canva,那个在线平面设计工具。如果你不是程序员,只是想在Canva里放大一段文字,你搜“Canva字体大小调整”,出来的教程会教你选中文本框、在顶部位号下拉框里选择字号。这个操作和HTML Canvas没有任何关系,代码环境里根本不存在一个叫“Canva”的API。
三是Canvas LMS,一个学习教育平台。老师和学生经常在Canvas LMS里编辑课程页面、发布讨论帖时想调整字体大小,他们搜索“Canvas字体大小”时,看到的其实是Canvas LMS编辑器里的字号按钮。
如果你的搜索意图落在后两种,用“HTML5 Canvas”“canvas 2d”“canvas fillText”这些关键词去搜,结果会精准得多。反过来说,如果你自己写教程、发帖子,也最好在标题里加“HTML5”或“前端”字样,避免被误归到设计工具类教程里。
3.2 Word批注字体大小改不动的原因和处理思路
为什么“Word批注改不了字体大小”会和“Canvas字体大小”出现在同一组热搜词里?因为很多人直接搜“Canvas字体”,Word的结果就混进来了。平心而论,Word批注字体设置和HTML Canvas完全不搭界,但它确实拦住了无数普通用户的路,值得一提。
Word批注里的文字,默认情况下并不直接响应你选中后按“增大字号”按钮。原因是批注内容使用的样式与正文样式是两条线,批注文本默认套用的是“批注文本”样式,这个样式的字号不是你通过正文上的字号工具能改的。
处理思路有两步:一是打开“开始”菜单右边的样式侧边栏,找到“批注文本”样式,右键选择“修改”,在“格式-字体”里重新设置字号;二是在修改完样式后,确认勾选“基于该模板的新文档”,否则只有当前文档生效,下次新建文档又打回原形。
3.3 DBeaver、JMeter的“字体大小设置”为什么搜进来
DBeaver(数据库客户端)和JMeter(压测工具)这两个软件,内部都有大量表格、编辑器、树形面板,使用者经常嫌默认字体小,于是搜索“DBeaver字体大小怎么调”“JMeter界面字体大小怎么调”。因为“Canvas”这个词在它们的搜索结果里经常出现——DBeaver查询结果表、JMeter图表组件都涉及canvas绘图——于是“Canvas字体大小”这一长尾搜索词就把这些工具教程带出来了。
这两类设置和HTML Canvas毫无关系。DBeaver在“窗口→首选项→用户界面→字体和颜色”里可以调整编辑器、表格、数据库导航器的字体;JMeter在“选项→外观→字体”里调整界面字体,如果你要调整的是GUI上某个具体表格的字体,可能还要到JMeter的bin/jmeter.properties里配置jmeter.hidpi.scale和jsyntaxtextarea.font.size等属性。这些都属于软件UI定制的范畴,和前端开发里的Canvas绘图是两个赛道。
3.4 QML里的HeaderView字体大小,跟HTML Canvas无关
还有一个搜索结果里频繁出现的问题是“qml更改horizontalheaderview字体大小”。这是Qt/QML开发里的控件样式问题。HorizontalHeaderView是Qt Quick Controls中的一个表头控件,要调整它的字体大小,通常是在QML里给styleData字段或者委托组件设置font.pixelSize或font.pointSize,例如:
HorizontalHeaderView { delegate: Label { text: styleData.value font.pixelSize: 14 } }这完全是另一套技术栈。如果你在用Qt开发桌面应用,看到这个搜索词是正常的;但千万不要把它和HTML Canvas混为一谈,两者除了名字里都有“canva”这几个字符,没有任何共同点。类似的还有“Canvas绘图引擎”“Canvas游戏”这类词,它们背后的技术基础还是HTML5 Canvas,只是被包装在了各类游戏引擎和绘图框架里,搜索引擎把它们抓进来,反而让本来具体的问题变得模糊。
4. Canvas文本渲染高频问题与排查技巧
4.1 设置了ctx.font,但文字大小纹丝不动
这是出现频率最高的一类问题。原因在第一节已经铺垫过,大多数情况是字体字符串格式非法,浏览器静默忽略了赋值。除了格式问题,还有一个特别容易踩的坑是:自定义字体还没加载完成,你就急着设置ctx.font并绘制文字。
这种情况在使用Web字体时尤其常见。页面加载时字体文件是异步加载的,如果你在window.onload里立即开始Canvas绘制,字体文件很大可能还没加载完。Canvas拿到一个未加载的字体名,不会报错,而是默默用默认字体替换。结果就是:你查控制台,字体名没错、字号也没错,但画出来的就是默认字体。
解决方案是等字体加载完再绘制,目前推荐用document.fonts.ready:
async function drawWithCustomFont(canvas) { await document.fonts.ready; const ctx = canvas.getContext('2d'); ctx.font = '24px "MyCustomFont", sans-serif'; ctx.fillText('字体加载完再绘制', 10, 40); }如果想等某一个具体字体,可以这样:
const fontFace = new FontFace('MyCustomFont', 'url(./font.woff2)'); await fontFace.load(); document.fonts.add(fontFace); ctx.font = '24px MyCustomFont';4.2 中文字体不生效,问题多半出在“名字匹配”
Canvas里设置中文字体,最常见的问题是字体名没写对。Windows上的“微软雅黑”,字体族的实际名称是Microsoft YaHei,不是微软雅黑,也不是yahei;macOS上的苹方,字体名是PingFang SC,不是pingfang。写错了名字,Canvas不会报错,直接用sans-serif替你渲染。
另外,如果字体名里有空格,记住加引号:
ctx.font = '30px "Microsoft YaHei", sans-serif'; ctx.font = '30px "PingFang SC", "Microsoft YaHei", sans-serif';带引号的字体族名和前面的30px之间要有逗号或空格,否则解析会出问题。一个稳妥的做法是给font属性设置一整个字体栈,和CSS一样,第一个字体找不到,浏览器会自动尝试下一个,最后回退到sans-serif。
4.3 fillText坐标越画越偏:baseline和textAlign在捣乱
字号调对了,文字大小也正常了,但很多人会遇到另一个问题:文字的位置怎么调都不对,要么偏高,要么偏低,要么左右对不齐。这是因为默认情况下,fillText(x, y)里的y指的是文字基线的位置,不是文字顶部的y坐标,也不是文字垂直中心的y坐标。
ctx.textBaseline的默认值是alphabetic,也就是字母基线。欧文字母的基线是很多字母底部的那条线,但这条线并不是文字的下边界,还有一部分降部会探到基线下面。所以当你把y设成100,文字占据的大致范围是85到105之间,靠下一点。
解决这个问题,最简单的方式是明确设置textBaseline:
ctx.textBaseline = 'top'; // y位置是文字顶端 ctx.textBaseline = 'middle'; // y位置是文字垂直中心 ctx.textBaseline = 'bottom'; // y位置是文字底部textAlign同理。默认值是left,可以改成center或right。做大屏数据标签、按钮文字的时候,textAlign='center'加textBaseline='middle'的组合,能省掉大量手工偏移计算。
4.4 动态文本的常见隐患:canvas尺寸变化导致字体被重置
这个问题非常隐蔽。如果你在代码里动态修改了canvas.width或canvas.height,浏览器会做两件事:一是把画布内容全部清空,二是重置Canvas上下文的所有状态,包括ctx.font、fillStyle、strokeStyle、textAlign等。也就是说,画布内容被清了,你之前设置的24px字号也没了,又变回10px默认值。
典型场景是做自适应布局:窗口变化后,你觉得需要重新计算Canvas画布尺寸,于是执行了canvas.width = newWidth。然后继续绘制文字,结果发现所有文本突然变小、变清晰且同时消失了之前设置的样式。其实不是文字变小,而是ctx.font被重置了。
解决办法是,在每次改变canvas.width或canvas.height之后,重新设置一次字体和其他绘制状态。如果绘制逻辑比较复杂,最好把字体、颜色等状态设置集中到一个setupContext函数里,改变画布尺寸后统一调用一次。同样,ctx.scale(dpr, dpr)也会被重置,在第5章的实战组件里可以看到完整的处理方式。
4.5 问题排查速查表:一眼定位问题
| 症状 | 常见原因 | 排查方向 |
|---|---|---|
| 设置了font但字号没变化 | 字体字符串缺字号单位、缺字体族、顺序错误 | 赋值后读取ctx.font打印出来,检查是否保留了目标值 |
| 字号变了但字体不对 | 字体名未加载完成 / 字体名拼写错误 | 用document.fonts.ready等待加载,检查字体名是否含空格是否加引号 |
| 文字画出来发虚、有锯齿 | Canvas物理像素小于视觉尺寸,未适配devicePixelRatio | 按DPR乘上canvas物理尺寸并恢复坐标缩放 |
| 文字位置明显偏上或偏下 | textBaseline设置不符合预期 | 根据需求显式设置top/middle/bottom/alphabetic |
| 文字左右对不齐 | textAlign未设置或设置错误 | 使用ctx.textAlign='center'等对齐方式 |
| 动画运行时文字变小或样式丢失 | 动态改变了canvas的width/height | 尺寸变更后重新设置所有ctx状态 |
| 同一画布中不同文字字号互相干扰 | 没有显式重置ctx.font | 每次绘制文本前都显式设置字体,不要复用旧值 |
| 用包围盒测高度不准确 | Canvas没有直接提供字号到像素高度的API | 先用实际渲染高度公式估算,或结合measureText+实际渲染验证 |
4.6 代码实践中的经验:先测量,再绘制,最后微调
这是我自己做Canvas文本渲染时反复验证过的一个流程,适用于所有“文字必须恰好放在某个区域内”的需求。流程分三步:
第一步,先设置目标字体和字号,用measureText量出文字的实际宽度。第二步,根据已知宽度推算出合适的对齐位置和字号。第三步,绘制后肉眼检查,因为字体基线、字形边距在不同平台上有细微差异,尤其是中英混排的时候,纯计算很难完全符合视觉预期。
举个实际例子,我做数据大屏时要把“今日销售额: ¥12,345.00”这串文字放在一个卡片中央,卡片宽260px,高80px。我需要让这串文字尽量大,又不能超出卡片。我的处理是:
const text = '今日销售额: ¥12,345.00'; const maxWidth = 260; const maxHeight = 80; let fontSize = 30; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; // 第一次尝试 ctx.font = `${fontSize}px "PingFang SC", "Microsoft YaHei", sans-serif`; let metrics = ctx.measureText(text); if (metrics.width > maxWidth) { // 等比例缩放一次 fontSize = Math.floor(fontSize * (maxWidth / metrics.width)); ctx.font = `${fontSize}px "PingFang SC", "Microsoft YaHei", sans-serif`; metrics = ctx.measureText(text); } // 高度校验 let textHeight = fontSize * 1.3; // 实际经验值:中文和数字混排时1.3比较稳 if (textHeight > maxHeight) { fontSize = Math.floor(fontSize * (maxHeight / textHeight)); } ctx.fillText(text, maxWidth / 2, maxHeight / 2);这种“先测量、再缩放、再微调”的思路,几乎可以套用到所有需要固定容器内自适应文字的Canvas场景。
5. 实战:封装一个自动适配宽高且高清不糊的Canvas文字组件
5.1 需求拆解
需求来自一个在线图片合成项目:页面上有一个Canvas画布,尺寸会随窗口变化,用户输入一段文字,文字要始终居中显示,并且字号要自动适配画布宽度,不能溢出。另外,这个项目在笔记本高分屏和普通外接显示器上都要显示清晰,所以必须处理DPR高清适配。
拆解一下,核心功能有三个:一是画布尺寸变化后,所有状态(包括字体)不会失效;二是文字宽度不超过画布可用宽度的90%,垂直方向也不超过画布高度的80%;三是无论DPR是1还是2,文字边缘都保持清晰。
5.2 核心实现
function drawAutoFitText(canvas, text, options = {}) { const { ratio = 0.9, fontFamily = '"PingFang SC", "Microsoft YaHei", sans-serif', color = '#333', minFontSize = 10, } = options; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const cssWidth = canvas.clientWidth; const cssHeight = canvas.clientHeight; // 第一步:重置物理尺寸并恢复坐标缩放 canvas.width = Math.round(cssWidth * dpr); canvas.height = Math.round(cssHeight * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 第二步:清空画布 ctx.clearRect(0, 0, cssWidth, cssHeight); // 第三步:动态计算最佳字号 const maxWidth = cssWidth * ratio; const maxHeight = cssHeight * 0.8; let low = minFontSize; let high = Math.max(minFontSize, Math.floor(cssHeight * 1.2)); let best = minFontSize; // 二分查找字号的边界条件 while (low <= high) { const mid = Math.floor((low + high) / 2); ctx.font = `${mid}px ${fontFamily}`; const w = ctx.measureText(text).width; const h = mid * 1.3; if (w <= maxWidth && h <= maxHeight) { best = mid; low = mid + 1; } else { high = mid - 1; } } // 第四步:用计算出的字号绘制文本 ctx.font = `${best}px ${fontFamily}`; ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, cssWidth / 2, cssHeight / 2); }5.3 调用示例
const canvas = document.getElementById('bannerCanvas'); // 初始绘制 drawAutoFitText(canvas, '欢迎参加年度技术沙龙', { color: '#1d4ed8' }); // 窗口尺寸变化后重绘 window.addEventListener('resize', () => { drawAutoFitText(canvas, '欢迎参加年度技术沙龙', { color: '#1d4ed8' }); });5.4 三个值得记录的细节
第一个细节:ctx.setTransform(dpr, 0, 0, dpr, 0, 0)这个调用是在设置canvas.width之后执行的。因为改canvas.width会把坐标变换也重置掉,如果不重新设置坐标变换,后面的绘制全部按物理像素计算,你在resize事件里看到的文字要么偏小,要么位置错乱。
第二个细节:为什么使用“二分查找”而不是简单的等比例缩放?因为等比例缩放依赖一个前提——字体宽度和字号严格线性。这个前提在绝大多数场景下成立,但遇到某些有特殊字距调整(kerning)的字体、或者中文和数字混排时,会有微小的偏差。二分查找不依赖这个前提,在指定范围内反复测试,虽然多循环几次,但结论更稳。对Canvas来说,7次左右的measureText调用性能损耗可以忽略。
第三个细节:文字高度预估系数取的是1.3。这个系数不是规范值,是我在多个字体和平台下测试出来的经验值。如果你画的是纯英文字体,1.2更合适;中文字体由于字形较高、行内间距大,1.3更能避免文字被判为“偏高”。如果你需要精确定制高度,还可以用ctx.measureText(text).actualBoundingBoxAscent + ctx.measureText(text).actualBoundingBoxDescent来获取精确高度,不过这个方法兼容性稍弱,我在实际项目中习惯先用估算值,再针对特殊字体加例外处理。
这个组件的思路可以继续扩展,比如支持多行文本、支持动态输入、支持加阴影、支持限制在某个子区域内等。万变不离其宗,核心始终是三个点:字体的正确设置、测量驱动的字号计算、以及对Canvas状态重置的觉悟。把这三条拿捏住,Canvas字体大小这关就过了大半。