有一天老王突然给何发发来了一个优化需求:现在给图片加文字的 canvas 库没有自动换行的功能,一旦文字超出了最大宽度,会导致文字压缩,你有空搞搞看。

是的吼,Canvas 对文字排版的支持很弱,当我需要绘制一串文字需要自动换行时却没有 api 辅助快速实现。

canvas 绘制实心文字就一个 api:

1
CanvasRenderingContext2D.fillText(text, x, y [, maxWidth]);

参数说明:

参数 含义 默认值
text 要绘制的文字
x 绘制起始点的横坐标
y 绘制起始点的纵坐标
maxWidth 容纳文字的最大宽度,若超了,则会压缩文字排版

CanvasRenderingContext2D 接口提供的 2D 渲染背景用来绘制 <canvas> 元素,为了获得这个接口的对象,需要在 <canvas> 上调用 getContext() 方法,并提供一个 '2d' 的参数:

1
2
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

以上这段代码想必大家都很熟悉了。

Canvas 逐行绘制文字

上网 google 了一下,发现 CanvasRenderingContext2D 提供了一个方法 measureText, 用以计算将要绘制的文字长度,如下:

1
CanvasRenderingContext2D.measureText(text);

该 api 返回一个 TextMetrics 对象,包含一个 double 精度的 width,来指示当前 text 的实际长度。

故解决方案:

  1. 从空字符串开始逐个添加要绘制的字符,并在每一个循环中进行 measureText 并比较其结果与 maxWidth 的大小;
    • 如果超过了,则绘制先前的字符串,并清空保存字符的变量,当前字符保留到下一个循环中,且下一次绘制起始点的纵坐标需要移到下一行;
    • 如果不超过,则继续下一次循环;
  2. 最后一次循环结束后,肯定还要绘制一次。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
*
* @param {Object} context : 绘制上下文
* @param {String} text : 将要绘制的文字
* @param {Number} x : 绘制起始点的横坐标
* @param {Number} y : 绘制起始点的纵坐标
* @param {Number} maxWidth : 绘制文字最大行宽
* @param {Number} lineHeight : 文字行高
*/
function fillTextAutoWrap (context, text, x, y, maxWidth, lineHeight) {
let texts = text.split('');
let textsLen = texts.length;
let line = '';
let testLine = '';
let testWidth = 0;

for (let i=0; i<textsLen; i++) {
testLine = line + texts[i];
testWidth = context.measureText(testLine).width;

if (testWidth > maxWidth && i > 0) {
context.fillText(line, x, y);
y += lineHeight;
line = texts[i];
} else {
line = testLine;
}
}
context.fillText(line, x, y);
}

以上代码没有带参数做校验和拦截,但是核心实现就是那么简单。

改进:主动控制绘制文字换行

如果又来了一个需求:不仅想要文字能够自动换行,而且想要能够让开发人员主动换行,如何实现?

其实很容易想到,无非就是设定一个主动换行的字符,多设置一层循环,让程序自动去匹配检测换行字符,检测到了就换行绘制。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
*
* @param {Object} context : 绘制上下文
* @param {String} text : 将要绘制的文字
* @param {Number} x : 绘制起始点的横坐标
* @param {Number} y : 绘制起始点的纵坐标
* @param {Number} maxWidth : 绘制文字最大行宽
* @param {Number} lineHeight : 文字行高
*/
function fillTextAutoWrap (context, text, x, y, maxWidth, lineHeight) {
const WRAP_SEPARATOR = '\n';
let texts = text.split('');
let textsLen = 0;
let line = '';
let testLine = '';
let testWidth = 0;

const arrTexts = text.split(WRAP_SEPARATOR);

for (let j = 0; j < arrTexts.length; j++) {
let texts = arrTexts[j].split('');
line = '';
textsLen = texts.length;

for (let i=0; i<textsLen; i++) {
testLine = line + texts[i];
testWidth = context.measureText(testLine).width;

if (testWidth > maxWidth && i > 0) {
context.fillText(line, x, y);
y += lineHeight;
line = texts[i];
} else {
line = testLine;
}
}
context.fillText(line, x, y);
}
}

后记

嗯,何发发圆满完成了老王交代的需求,还做了一下功能拓展,心满意足地回家喝糖水了。

参考链接