canvas自定义文本排列方法 + 自定义花字应用案例
一、canvas自定义文本排列方法
其中还添加了自定义文字描边、文字阴影、文字下划线等逻辑功能。
1、竖排
注意点:竖排里的英文和数字的摆放方式是倒立的,跟中文的正立摆放是有区别的。
1.1 自动换行
需要根据传入的文字区域的宽高自动识别文本的长度进行换行
const ARR = [] // 存储每行的宽高
const WORDS = {} // 存储每个字的宽高
let LINEARR = [] // 片段数组
// console.error('自动换行')
_maxWidth = item.width * devicePixelRatio
_maxHeight = item.height * devicePixelRatio
let _w = 0
let _h = 0
let _p = ''
// 计算所有字的宽度和高度
for (let i = 0; i < item.content.length; i++) {
// 换行符
if (item.content[i] === '\n') {
ARR.push([lineHeight || _w, _h])
LINEARR.push(_p)
_w = 0
_h = 0
_p = ''
continue
}
// 计算文字宽高
const metrics = context.measureText(item.content[i]);
// 计算文本宽度
// const textWidth = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;
const textWidth = metrics.width
// 计算文本高度
// 所有字在这个字体下的高度
const textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
// 当前文本字符串在这个字体下用的实际高度
// const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
if (!WORDS[item.content[i]]) {
WORDS[item.content[i]] = [textWidth, textHeight]
}
// 英文&数字
if (!isCstr(item.content[i])) {
// 超过编辑框高度
if (_h + textWidth > item.height * devicePixelRatio) {
ARR.push([lineHeight || _w, _h])
LINEARR.push(_p)
_w = textHeight
_h = textWidth
_p = item.content[i]
} else {
_w = Math.max(textHeight, _w)
_h += textWidth
_p += item.content[i]
}
} else {
// 超过编辑框高度
if (_h + textHeight > item.height * devicePixelRatio) {
ARR.push([lineHeight || _w, _h])
LINEARR.push(_p)
_w = textWidth
_h = textHeight
_p = item.content[i]
} else {
_w = Math.max(textWidth, _w)
_h += textHeight
_p += item.content[i]
}
}
}
1.2 换行符换行
const ARR = [] // 存储每行的宽高
const WORDS = {} // 存储每个字的宽高
let LINEARR = [] // 片段数组
// 根据换行符分段
LINEARR = item.content.split('\n');
// 计算所有段落的宽度和高度
for (let i = 0; i < LINEARR.length; i++) {
let _w = 0
let _h = 0
for (let j = 0; j < LINEARR[i].length; j++) {
// 计算文字宽高
const metrics = context.measureText(LINEARR[i][j]);
// 计算文本宽度
// const textWidth = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;
const textWidth = metrics.width
// 计算文本高度
// 所有字在这个字体下的高度
const textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
// 当前文本字符串在这个字体下用的实际高度
// const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
if (!WORDS[LINEARR[i][j]]) {
WORDS[LINEARR[i][j]] = [textWidth, textHeight]
}
// 英文&数字
if (!isCstr(LINEARR[i][j])) {
_w = Math.max(textHeight, _w)
_h += textWidth
} else {
_w = Math.max(textWidth, _w)
_h += textHeight
}
}
_maxWidth += (lineHeight || _w)
_maxHeight = Math.max(_maxHeight, _h)
ARR.push([lineHeight || _w, _h])
}
1.3 绘制逻辑
// 计算坐标并绘制文字
let x = (item.left + item.width) * devicePixelRatio
let y = item.top * devicePixelRatio
for (let i = 0; i < LINEARR.length; i++) {
x -= ARR[i][0]
y = item.top * devicePixelRatio
// 判断对齐方式
switch (item.align) {
case 'top':
break
case 'middle':
y += ((item.height * devicePixelRatio - ARR[i][1]) / 2)
break
case 'bottom':
y += (item.height * devicePixelRatio - ARR[i][1])
break
}
for (let j = 0; j < LINEARR[i].length; j++) {
// 英文&数字
if (!isCstr(LINEARR[i][j])) {
context.save()
if (j > 0) {
// 英文&数字
if (!isCstr(LINEARR[i][j - 1])) {
y += WORDS[LINEARR[i][j - 1]][0]
} else {
y += WORDS[LINEARR[i][j - 1]][1]
}
}
context.translate(x + ARR[i][0] / 2, y + WORDS[LINEARR[i][j]][0] / 2)
context.rotate(Math.PI / 2)
context.translate(-(x + ARR[i][0] / 2), -(y + WORDS[LINEARR[i][j]][0] / 2))
// 自定义阴影
if (item?.shadow) {
context.save()
context.filter = `blur(${item.shadow.blur})px`
context.fillStyle = item.shadow.color
context.strokeStyle = item.shadow.color
const s = (item.fontSize / 120) * devicePixelRatio
item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x + (ARR[i][0] / 2 - WORDS[LINEARR[i][j]][0] / 2) + s * item.shadow.x, y + (WORDS[LINEARR[i][j]][0] / 2 - WORDS[LINEARR[i][j]][1] / 2) + s * item.shadow.y);
context.fillText(LINEARR[i][j], x + (ARR[i][0] / 2 - WORDS[LINEARR[i][j]][0] / 2) + s * item.shadow.x, y + (WORDS[LINEARR[i][j]][0] / 2 - WORDS[LINEARR[i][j]][1] / 2) + s * item.shadow.y);
context.restore()
}
// 主体文字
item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x + ARR[i][0] / 2 - WORDS[LINEARR[i][j]][0] / 2, y + WORDS[LINEARR[i][j]][0] / 2 - WORDS[LINEARR[i][j]][1] / 2);
context.fillText(LINEARR[i][j], x + ARR[i][0] / 2 - WORDS[LINEARR[i][j]][0] / 2, y + WORDS[LINEARR[i][j]][0] / 2 - WORDS[LINEARR[i][j]][1] / 2);
// context.beginPath()
// context.strokeRect(x+ARR[i][0]/2-WORDS[LINEARR[i][j]][0]/2, y+WORDS[LINEARR[i][j]][0]/2-WORDS[LINEARR[i][j]][1]/2, WORDS[LINEARR[i][j]][0], WORDS[LINEARR[i][j]][1])
// context.closePath()
context.restore()
// 下划线
if (item?.underline) {
context.save()
context.strokeStyle = item.fontColor; // 描边颜色
context.lineWidth = Math.max(item.fontSize * devicePixelRatio / 12, 1)
context.beginPath()
context.moveTo(x, y);
context.lineTo(x, y + WORDS[LINEARR[i][j]][0]);
context.closePath()
context.stroke();
context.restore();
}
} else {
if (j > 0) {
// 英文&数字
if (!isCstr(LINEARR[i][j - 1])) {
y += WORDS[LINEARR[i][j - 1]][0]
} else {
y += WORDS[LINEARR[i][j - 1]][1]
}
}
// 自定义阴影
if (item?.shadow) {
context.save()
context.filter = `blur(${item.shadow.blur})px`
context.fillStyle = item.shadow.color
context.strokeStyle = item.shadow.color
const s = (item.fontSize / 120) * devicePixelRatio
item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x + (ARR[i][0] - WORDS[LINEARR[i][j]][0]) / 2 + s * item.shadow.x, y + s * item.shadow.y);
context.fillText(LINEARR[i][j], x + (ARR[i][0] - WORDS[L