我正在参与「码上挑战赛」概况请看:码上挑战赛来了!
在一些前端开发场景中,或许会遇到运用 canvas 来烘托文本,例如 web 表格应用,便是用 canvas 来烘托文本,假如咱们去检查飞书、谷歌、石墨、腾讯表格能够发现它们都是用 canvas 来完成的。
这篇文章就来解说如安在 canvas 中烘托和排版富文本。在介绍之前能够先点击下面链接,体会下终究的效果。
主动换行
在平时根据 DOM 的文本开发时,咱们并不关心文本的主动换行,由于浏览器现已主动帮咱们自己处理了文本主动换行,如下图所示。
在 canvas 中只要两个 API fillText
和 strokeText
来制作文本,它们并不能处理文本主动换行,烘托出来的文本都在一行,类似于 white-space: nowrap
相同的效果。
在 canvas 中假如想让文本主动换行,需求手动丈量每一个字符的巨细,假如累计的字符的宽度超越容器的宽度,则换一行持续烘托。
canvas 中的 measureText
API 能够用来丈量文本的信息,它返回一个 TextMetrics 对象,签名如下所示。
interface TextMetrics {
// x-direction
readonly attribute double width; // advance width
readonly attribute double actualBoundingBoxLeft;
readonly attribute double actualBoundingBoxRight;
// y-direction
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute double actualBoundingBoxAscent;
readonly attribute double actualBoundingBoxDescent;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double hangingBaseline;
readonly attribute double alphabeticBaseline;
readonly attribute double ideographicBaseline;
};
TextMetrics 中的 width
表示当时丈量字符的宽度,fontBoundingBoxAscent
加 fontBoundingBoxDescent
能够知道这一行的高度。
const text = 'abcdefg'
let maxWidth = 100
let lineWidth = 0
let w = 0
let line = ''
for (let c of text) {
w = ctx.measureText(c).width
if (totalWidth + w > maxWidth) {
console.log(line)
line = c
lineWidth = w
} else {
line += c
lineWidth += w
}
}
上面代码中丈量每个字符的巨细,假如超越 maxWidth
则换一行持续丈量,这样就简略的完成了文本主动换行。
但是,还没完,假如上面这样处理睬英文单词被折断的问题,如下图所示。
上图中的 figure、exist、viewed 等单词都被从中心折断了,这样会导致用户不方便阅览,或许发生歧义。
正确的换行方式应该如下图所示。
假如剩余空间存放不下一个单词的长度则进行换行。
所以在判别的时分还需求区别当时字符是不是属于当时单词的字符。要做到按单词维度来换行,首要要区别当时字符是不是一个断词字符。咱们能够以为 unicode 小于 0x2E80
的都为拉丁字符(echart 中是小于等于 0x017F
),在这个范围内咱们还需求排除一些字符,比方空格、问号等字符。
浏览器判别是否是断词字符是十分杂乱的,还会和当时字符的上下文来判别,比方单个 [
不是,假如前面加上 ]
便是了,但是咱们这里没有必要做的这么杂乱。只需求判别字符是否大于 0x2E80
,或许是空格、问号等字符,就以为字符是断词字符,咱们能够很轻松的写下如下判别函数。
const breakCharSet = new Set(['?', '-', ' ', ',', '.'])
function isWordBreakChar(ch) {
if (ch.charCodeAt(0) < 0x2e80) return breakCharSet.has(ch)
return true
}
接下来完善下主动换行的代码,如下所示。
const lines = []
let line = ''
let word = ''
let lineWidth = 0
let wordWidth = 0
for (let c of text) {
const w = ctx.measureText(c)
const inWord = !isWordBreakChar(c)
if (lineWidth + wordWidth + w > maxWidth) { // 假如超长
if (lineWidth) {
lines.push(line)
line = ''
lineWidth = 0
if (wordWidth + w > maxWidth) {
if (wordWidth) {
lines.push(word)
word = c
wordWidth = w
}
if (w > maxWidth) {
lines.push(c)
word = ''
wordWidth = 0
}
} else if (!inWord) {
line += (word + c)
lineWidth += (wordWidth + w)
word = ''
wordWidth = 0
} else {
word += ch
wordWidth += w
}
} else if (wordWidth) {
lines.push(word)
word = c
wordWidth = w
} else { // 假如容器宽度小于一个字符
lines.push(c)
}
} else if (inWord) { // 假如属于一个单词
word += ch
wordWidth += w
} else { // 假如不是一个单词
line += (word + c)
lineWidth += (wordWidth + w)
word = ''
wordWidth = 0
}
}
能够发现相比之前的简略换行,按单词换行杂乱多了,由于咱们需求判别很多鸿沟情况,例如要一个单词换行,但是当容器宽度小于一个单词长度时,又要强行中止,在或许容器宽度小于一个字符时,需求一个字符一行。
富文本
了解了文本的主动换行,接下来再来看看怎么完成 canvas 富文本烘托。在烘托之前咱们首要界说好富文本的数据组织,如下所示。
interface Rich {
start: number; // 开始字符(包括)
end: number; // 结束字符(不包括)
fontFamily?: string; // 字体
fontSize?: number; // 字体巨细
bold?: boolean; // 是否加粗
italic?: boolean; // 是否歪斜
color?: string; // 色彩
underline?: boolean; // 下划线
lineThough?: boolean; // 删去线
}
Rich
接口界说了原文本 start
到 end
范围内的款式,这里总共界说了 7 种富文本款式,前 4 个能够用 canvas 中的 font
来完成,色彩能够用 fillStyle
,而下划线和删去线则需求咱们自己来完成,在特定方位画一条横线。
接下来再来界说下一个文本的数据结构,如下所示。
interface TextData {
width: number; // 容器宽度
text: string; // 要烘托的文本
rich?: Rich[] // 当时文本的富文本款式
}
富文本的主动换行会比上面介绍的主动换行还要杂乱一点,由于一行文字中或许存在某个字符字体巨细十分大,把其他字符挤下去,而且它还会影响行高,每行的行高也或许是不一致的。
咱们 measureText
也需求做些改变才干精确丈量出字符宽高,代码如下所示。
function getFont(r) {
return `${r.italic ? 'italic' : ''} ${r.bold ? 'bold' : ''} ${r.fontSize || 16}px ${r.fontFamily || 'sans-serif'}`.trim()
}
function measureText(str, font) {
ctx.font = font
return ctx.measureText(str)
}
丈量字体时先设置字体的 font
再来丈量,由于影响字符宽高的只要 font
特点。
接下来咱们还需求规划 3 个类来协助咱们了解,分别是 TextCell
、TextLine
和 TextToken
。
TextCell
是文本容器,它拥有多个 TextLine
,TextLine
是一个行文本,它包括多个 TextToken
,TextToken
是是个文本片段,这一个文本片段的款式要是相同的(属于同一个 Rich)。
接下来咱们需求将整个文本打散,变成上面咱们提到的文本 token,代码如下所示。
let prevEnd = 0
for (let i = 0, r; i < richLen; ++i) {
r = rich[i] // 富文本配置
if (prevEnd < r.start) {
// 纯文本
flush(parseText(text.slice(prevEnd, r.start), x, maxWidth))
}
// 富文本
flush(parseText(text.slice(r.start, r.end), x, maxWidth, r))
prevEnd = r.end
}
其间的 parseText
是上一章节中介绍的主动换行,它会返回一个个 TextToken
,篇幅有限,这里就只贴相关代码,详细代码请检查码上。
flush
是创建 TextLine
假如当时文本长度超了的话,别的它还会修正 TextToken
的高度,比方先解析字体比较小的 TextToken
,假如后边又遇到这一行中字号更大的 TextToken
则需求手动修正之前 TextToken
的高度。
相关代码如下所示。
let prevEnd = 0
let x = 0
let j = 0
let len = 0
let line = []
let lineHeights = []
const lines = []
const flushLine = () => {
lines.push(new TextLine(line, Math.max.apply(null, lineHeights))) // 修正行高
}
const flush = (info) => {
j = 0
while (info.tokens[j]?.x) j++
len = info.tokens.length
if (j < len) {
if (line.length) { // 说明当时 TextToken 超了一行
line.push(...info.tokens.slice(0, j))
if (j) lineHeights.push(info.lineHeight)
flushLine() // 完成一行
line = []
lineHeights = []
}
if ((len - j - 1) > 0) {
for (let l = len - 1; j < l; ++j) { // 每一个 TextToken 便是一行
lines.push(new TextLine([info.tokens[j]], info.lineHeight))
}
}
line.push(info.tokens[len - 1]) // 保留最后一个
} else {
line.push(...info.tokens)
}
lineHeights.push(info.lineHeight)
x = info.x // 一下个代解析片段的开始 x
}
上面代码中是判别解析好的 TextToken
,假如长度超了一行,则修正之前这一行 TextToken
的高度为最大高度。
别的还需保存最新一行已解析的宽度,便是上面代码中的 x
。由于接下来解析新的文本是需求从 x
宽度之后来计算的。
烘托
有了上面计算好的信息,要将文本烘托出来就十分简略直接,代码如下所示。
function render(cellData) {
const cell = new TextCell(cellData)
ctx.save();
ctx.strokeRect(0, 0, cell.width, cell.height);
ctx.beginPath();
ctx.rect(0, 0, cell.width, cell.height);
ctx.clip();
let dx = 4 // padding
let dy = 0
cell.lines.forEach(l => {
l.tokens.forEach(t => {
ctx.font = t.style.font
ctx.strokeStyle = ctx.fillStyle = t.style.color || '#000'
ctx.fillText(t.text, t.x + dx, l.y + dy) // 烘托文字
if (t.style.underline) { // 烘托下划线
ctx.beginPath();
ctx.moveTo(t.x + dx, l.y+3 + dy);
ctx.lineTo(t.x + t.width + dx, l.y+3 + dy);
ctx.stroke();
}
if (t.style.lineThough) { // 烘托删去线
ctx.beginPath();
ctx.moveTo(t.x + dx, l.y - t.actualHeight / 2 + dy);
ctx.lineTo(t.x + t.width + dx, l.y - t.actualHeight / 2 + dy);
ctx.stroke();
}
})
})
ctx.restore();
}
上面代码遍历每一个 TextToken
,设置款式并烘托文字,假如有下划线或删去线,则再画一根线即可。
总结
这篇文章主要解说了怎么运用 canvas 来烘托富文本和富文本的主动换行,原理是运用 measureText
API 来丈量每个字符的宽高,并且判别当时字符是不是属于同一个单词,假如超越长度则进行换行,对与富文本咱们还需求判别每个 TextToken
的高度,丈量完一行后还需求修正这一行中每个 TextToken
的高度,计算好各种信息后,最后只用读取这些信息进行烘托即可。
这篇文章的中的计算代码都是没有经过功能优化的,假如烘托很多的数据或许功能很慢,下篇文章将解说怎么进行高功能的 canvas 烘托。
在线体会: