博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
canvas文本绘制自动换行、字间距、竖排等实现
阅读量:4116 次
发布时间:2019-05-25

本文共 9732 字,大约阅读时间需要 32 分钟。

640?wx_fmt=jpeg

作者 | 张鑫旭

来源 | http://www.zhangxinxu.com/wordpress/?p=7362

一、canvas对文字排版的支持很弱

和CSS相比,SVG以及canvas对文字排版的支持很弱。
在CSS中天然支持的文本自动换行,其他
letter-sapcing 字间距,
writing-mode 竖排等都是一个CSS属性就可以实现。但是在canvas中,全部都不支持。
canvas绘制文本API为:
 
  • text

  • text是需要绘制的文本。

  • x

  • x是文本绘制的水平参考点坐标。随着CanvasRenderingContext2D.textAlign的设置不同,x的坐标位置也不同。可以表示这段文字内容左侧坐标,或水平中心坐标,或右侧坐标。

    y

    y是文本绘制的垂直参考点坐标。随着CanvasRenderingContext2D.textBaseline的设置不同,y的坐标位置也不同。支持多种基线类型(CSS中也有对应概念),MDN上有一张图可以很好地表示文本基线和文本垂直位置的关系。

    640?wx_fmt=png

    maxWidth

    maxWidth表示文本内容占据的最大宽度。这里的maxWidth概念和CSS中的max-width差别很大,其最终的文本表现是:当文本占据宽度超过maxWidth的后,所有的文本自动变窄以适应这个最大宽度限制。表现类似这样:

    640?wx_fmt=png

    您可以狠狠地点击这里:maxWidth参数让文字变窄demo

    相关测试代码如下:

  • var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	context.font = '32px sans-serif';	context.fillText('我是一段被maxWidth限制的文本', 0, 50, 200

如果没有maxWidth限制,则文本会一行走到底,直到超出画布尺寸,有点类似CSS中设置容器white-space:nowrap + overflow:hidden的表现。

二、如何让canvas支持自动换行?

如何让canvas支持自动换行?
首先有一点可以肯定,就是到目前为止,canvas中并没有任何可以让文本自动换行的现成的API。
因此注定这个看上去简单的事情实践起来并没有那么容易。
通常比较好的实现方法有下面两种:

1. canvas计算与逐行绘制

实现原理的核心是
CanvasRenderingContext2D.measureText(text) 这个API,可以返回一个TextMetrics对象,其中包含了当前上下文环境下
text  
double 精度的占据宽度,于是我们就可以通过每个字符宽度的不断累加,精确计算哪个位置应该可以换行。
下面就是我扩展的文本自动换行方法JS代码:
CanvasRenderingContext2D.prototype.wrapText = function (text, x, y, maxWidth, lineHeight) {	    if (typeof text != 'string' || typeof x != 'number' || typeof y != 'number') {	        return;	    }	    	    var context = this;	    var canvas = context.canvas;	    	    if (typeof maxWidth == 'undefined') {	        maxWidth = (canvas && canvas.width) || 300;	    }	    if (typeof lineHeight == 'undefined') {	        lineHeight = (canvas && parseInt(window.getComputedStyle(canvas).lineHeight)) || parseInt(window.getComputedStyle(document.body).lineHeight);	    }	    	    // 字符分隔为数组	    var arrText = text.split('');	    var line = '';	    	    for (var n = 0; n < arrText.length; n++) {	        var testLine = line + arrText[n];	        var metrics = context.measureText(testLine);	        var testWidth = metrics.width;	        if (testWidth > maxWidth && n > 0) {	            context.fillText(line, x, y);	            line = arrText[n];	            y += lineHeight;	        } else {	            line = testLine;	        }	    }	    context.fillText(line, x, y);	};
API如下:
CanvasRenderingContext2D.wrapText(text, x, y, maxWidth, lineHeight)
其中
text
x
y  3个参数和
fillText() 方法中的这3个参数含义是一样的,不赘述。
maxWidth 表示的含义可就不一样了,表示最大需要换行的宽度,此参数可缺省,默认会使用canvas画布的
width 宽度作为
maxWidth
lineHeight 表示行高,同样可缺省,默认会使用
<canvas> 元素在DOM中继承的
line-height 作为行高。
使用示例如下:
var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	context.font = '16px sans-serif';	context.textBaseline = 'top';	context.wrapText('我是一段会换行的文字啦啦啦', 0, 0);
用法很简单,使用
wrapText 代替原生的
fillText 即可!
您可以狠狠的点击这里:自动换行扩展API wrapText演示demo
下面截图就是demo页面绘制效果(截自IE9浏览器):
640?wx_fmt=png
可以看到上方绘制的文字在核实位置自动换行了,您可以修改
<textarea> 中的文字内容,点击“绘制”按钮体验下其他文本内容的自动换行绘制效果。

2. 借助SVG <foreignObject>直接把CSS效果绘制上去

关于SVG 
<foreignObject> 让HTML转换成canvas图片的原理和细节可以参见我之前写的“SVG <foreignObject<简介与截图等应用”这篇文章。
基本上,本文后面会介绍到的字符间距,文字竖排等实现都可以使用这个方法实现,因此,为了避免不必要的啰嗦,仅本效果会具体演示代码细节,后面效果大家自行拷贝改改就好了。
我们先看实例,您可以狠狠地点击这里:canvas借助SVG foreignObject实现文本自动换行demo
结果Chrome浏览器下:
640?wx_fmt=png
JS实现如下:
var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	context.font = '16px sans-serif';	var width = canvas.width;	var height = canvas.height;		var tempImg = new Image();	tempImg.width = width;	tempImg.height = height;	tempImg.onload = function () {	    // 把img绘制在canvas画布上	    context.drawImage(this, 0, 0, width, height);	};	tempImg.src = 'data:image/svg+xml;charset=utf-8,
我是一段需要换行的文字啦啦啦
';
此方法优点在于足够简单,只要一段带
style 样式的HTML代码即可!
唯一不足在于兼容性,IE浏览器不支持
<foreignObject> ,最新的Firefox浏览器虽然支持
<foreignObject> ,但是只能以
<img< 形式呈现,无法绘制到canvas画布上(若谁知道原因欢迎不吝赐教)。
不过好的是移动端Safari浏览器以及微信浏览器都是支持的,因此,此方法理论上是可以在移动端使用的。例如我手机Safari的效果截图:
640?wx_fmt=png

三、如何让canvas支持字符间距?

1. 如果只需要兼容Chrome,直接letter-spacing控制

对于Chrome浏览器,无论是字符间距还是单词间距,都可以自动继承于
<canvas> 元素,这个特性让人非常感动。
也就是:
canvas { letter-pacing: 5px; }
绘制的文字字符间距自动就是
5px
如此欣喜的特性有必要亲眼见证一下,您可以狠狠地点击这里:canavs文本间距使用CSS letter-spacing实现demo
效果如下GIF示意:
640?wx_fmt=gif
完整测试JS代码如下:
var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	var range = document.querySelector('input[type=range]');	// 绘制方法	var draw = function () {	    // 清除之前的绘制	    context.clearRect(0, 0, canvas.width, canvas.height);	    // 字符间距设置	    canvas.style.letterSpacing = range.value + 'px';	    // 并绘制文本,font属性值设置一定要在这里	    context.font = '32px sans-serif';	    context.fillText('我是一段文本', 0, 50);	};	// 改变字符间距后重绘	range.addEventListener('change', draw);	// 一进来根据默认值绘制	draw();
根据我的观察,貌似Chrome浏览器在设置
font 属性值的时候,把
letter-spacing 等信息一起算作上下文中了。所以,虽然看上去
context.font = '32px sans-serif' 一直都没变,但却不能放在
draw() 方法之外,否则,还是按照老的
letter-spacing 渲染而看不到字符间距变化。
此方法最简单最容易理解,只可惜,根据我的测试,目前仅Chrome浏览器支持。Firefox以及Safari全都不行。

2. canvas计算与逐字绘制

原理为,每一个字符单独作为一个绘制单元,然后根据字符宽度+letterSpacing间距动态绘制,同样,离不开使用
CanvasRenderingContext2D.measureText(text) 这个API。
以下就是自己直接在原型上扩展的字符间距绘制方法letterSpacingText,大家可以直接拷贝过去使用,MIT协议,保留原出处即可。
/**	* @author zhangxinxu(.com)	* @licence MIT	* @description http://www.zhangxinxu.com/wordpress/?p=7362	*/	CanvasRenderingContext2D.prototype.letterSpacingText = function (text, x, y, letterSpacing) {	    var context = this;	    var canvas = context.canvas;	    	    if (!letterSpacing && canvas) {	        letterSpacing = parseFloat(window.getComputedStyle(canvas).letterSpacing);	    }	    if (!letterSpacing) {	        return this.fillText(text, x, y);	    }	    	    var arrText = text.split('');	    var align = context.textAlign || 'left';	    	    // 这里仅考虑水平排列	    var originWidth = context.measureText(text).width;	    // 应用letterSpacing占据宽度	    var actualWidth = originWidth + letterSpacing * (arrText.length - 1);	    // 根据水平对齐方式确定第一个字符的坐标	    if (align == 'center') {	        x = x - actualWidth / 2;	    } else if (align == 'right') {	        x = x - actualWidth;	    }	    	    // 临时修改为文本左对齐	    context.textAlign = 'left';	    // 开始逐字绘制	    arrText.forEach(function (letter) {	        var letterWidth = context.measureText(letter).width;	        context.fillText(letter, x, y);	        // 确定下一个字符的横坐标	        x = x + letterWidth + letterSpacing;	    });	    // 对齐方式还原	    context.textAlign = align;	};
API详细:
CanvasRenderingContext2D.letterSpacingText(text, x, y, letterSpacing);
其中
text
x
y  3个参数和
fillText() 方法中的这3个参数含义是一样的,不赘述。
letterSpacing 表示字符间距大小,数值。可缺省,默认会拿
<canvas> 元素在DOM环境下的
letter-spacing 大小作为计算值。
使用示意:
var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	context.font = '32px sans-serif';	context.textAlign = 'center';	// 字符间隙5px	context.letterSpacingText('我是一段文本', canvas.width / 2, 50, 5);
您可以狠狠地点击这里:canavs字符间距JS逐字计算demo
效果如下GIF示意(居中对齐,截自IE Edge):
640?wx_fmt=gif
此方法兼容性非常好,IE9+浏览器都支持,PC和Mobile通吃。

3. 借助SVG <foreignObject>直接把CSS效果绘制上去

和自动换行实现类似,详细略。

四、如何让canvas支持竖直排列?

文字竖直排列,对于玩英文的老外,可以使用
context.rotate() 旋转
90deg 实现,但是对于中文等中亚文字,却是完全不适合的。因为两种语言的竖直排版规则是不一样的。
中文等东亚文字上,例如一些古诗词文字还是正的,仅仅是阅读方向是从上往下,但是,英文(以及阿拉伯数字)由于本身的字符特性,直接就是旋转排列的。
640?wx_fmt=png
所以单纯旋转策略对于大中国并不实用。
在CSS中,我们可以使用
writing-mode 改变文档流的方向,从而实现文字竖排,相关文章可以参见我之前的文章:“改变CSS世界纵横规则的writing-mode属性”,或者购买我的《CSS世界》,其中有详细介绍。
那在canvas中又该如何实现呢?

1. JS混合计算逐字排列

混合计算规则如下:全角字符竖排,英文数字等半角字符旋转排列。
下面是我扩展的竖排方法,同样MIT协议,可随意使用,保留上面一段作者和出 处说明即可。
/* @author zhangxinxu(.com)@licence MIT@description http://www.zhangxinxu.com/wordpress/?p=7362*/CanvasRenderingContext2D.prototype.fillTextVertical = function (text, x, y) {    var context = this;    var canvas = context.canvas;        var arrText = text.split('');    var arrWidth = arrText.map(function (letter) {        return context.measureText(letter).width;    });    var align = context.textAlign;    var baseline = context.textBaseline;        if (align == 'left') {        x = x + Math.max.apply(null, arrWidth) / 2;    } else if (align == 'right') {        x = x - Math.max.apply(null, arrWidth) / 2;    }    if (baseline == 'bottom' || baseline == 'alphabetic' || baseline == 'ideographic') {        y = y - arrWidth[0] / 2;    } else if (baseline == 'top' || baseline == 'hanging') {        y = y + arrWidth[0] / 2;    }        context.textAlign = 'center';    context.textBaseline = 'middle';        // 开始逐字绘制    arrText.forEach(function (letter, index) {        // 确定下一个字符的纵坐标位置        var letterWidth = arrWidth[index];        // 是否需要旋转判断        var code = letter.charCodeAt(0);        if (code <= 256) {            context.translate(x, y);            // 英文字符,旋转90°            context.rotate(90 * Math.PI / 180);            context.translate(-x, -y);        } else if (index > 0 && text.charCodeAt(index - 1) < 256) {            // y修正            y = y + arrWidth[index - 1] / 2;        }        context.fillText(letter, x, y);        // 旋转坐标系还原成初始态        context.setTransform(1, 0, 0, 1, 0, 0);        // 确定下一个字符的纵坐标位置        var letterWidth = arrWidth[index];        y = y + letterWidth;    });    // 水平垂直对齐方式还原    context.textAlign = align;    context.textBaseline = baseline;};
API名称是
fillTextVertical ,语法如下:
CanvasRenderingContext2D.fillTextVertical(text, x, y)
其中
text
x
y  3个参数和
fillText() 方法中的这3个参数含义是一样的,不赘述。
实现的效果是:英文数字等旋转,中文垂直排列。支持
textAlign
textBaseline 等基本设置。
您可以狠狠地点击这里:canavs文本竖排JS逐字计算实现demo
使用JS如下:
var canvas = document.querySelector('canvas');	var context = canvas.getContext('2d');	context.font = '24px STKaiti, sans-serif';	context.textAlign = 'center';	context.textBaseline = 'top';	context.fillTextVertical('anglebaby和黄晓明', canvas.width / 2, 0);
结果如下图所示:
640?wx_fmt=png

2. 借助SVG <foreignObject>直接把CSS效果绘制上去

和自动换行实现类似,详细略。

五、结束语

当年CSS之所以一统天下就是在文本展现文字排版这一块非常方便。
看看SVG的文本展现,在看看canvas的文本呈现,难用的很。全靠友军衬托啊!
问题来了,CSS文本呈现这里厉害,那还需要canvas干什么?
因为canvas可以方便把文字转换成图片,例如一些广告工具等等,需要前端合成的,就需要canvas大放异彩了。
本文扩展的这些方法并未实际项目大规模验证,有疏漏之处在所难免,欢迎指正!
感谢阅读!
640?wx_fmt=jpeg
640?wx_fmt=png

转载地址:http://jyfpi.baihongyu.com/

你可能感兴趣的文章
基于“分形”编写的交互应用
查看>>
《融入动画技术的交互应用》主题博文推荐
查看>>
链睿和家乐福合作推出下一代零售业隐私保护技术
查看>>
Unifrax宣布新建SiFAB™生产线
查看>>
艾默生纪念谷轮™在空调和制冷领域的百年创新成就
查看>>
NEXO代币持有者获得20,428,359.89美元股息
查看>>
Piper Sandler为EverArc收购Perimeter Solutions提供咨询服务
查看>>
RMRK筹集600万美元,用于在Polkadot上建立先进的NFT系统标准
查看>>
JavaSE_day12 集合
查看>>
JavaSE_day14 集合中的Map集合_键值映射关系
查看>>
Day_15JavaSE 异常
查看>>
异常 Java学习Day_15
查看>>
JavaSE_day_03 方法
查看>>
day-03JavaSE_循环
查看>>
Mysql初始化的命令
查看>>
day_21_0817_Mysql
查看>>
day-22 mysql_SQL 结构化查询语言
查看>>
MySQL关键字的些许问题
查看>>
浅谈HTML
查看>>
css基础
查看>>