一、canvas自定义文本排列方法
其中还添加了自定义文字描边、文字阴影、文字下划线等逻辑功能。
1、竖排
注意点:竖排里的英文和数字的摆放方式是倒立的,跟中文的正立摆放是有区别的。
1.1 自动换行
需要根据传入的文字区域的宽高自动识别文本的长度进行换行
const ARR = [] // 存储每行的宽高
const WORDS = {} // 存储每个字的宽高
let LINEARR = [] // 片段数组// console.error('自动换行')_maxWidth = item.width * devicePixelRatio
_maxHeight = item.height * devicePixelRatiolet _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 = 0let _h = 0for (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':breakcase 'middle':y += ((item.height * devicePixelRatio - ARR[i][1]) / 2)breakcase '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.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize / 120) * devicePixelRatioitem.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.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize / 120) * devicePixelRatioitem.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[LINEARR[i][j]][0]) / 2 + s * item.shadow.x, y + s * item.shadow.y);context.restore()}// 主体文字item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x + (ARR[i][0] - WORDS[LINEARR[i][j]][0]) / 2, y);context.fillText(LINEARR[i][j], x + (ARR[i][0] - WORDS[LINEARR[i][j]][0]) / 2, y);// context.beginPath()// context.strokeRect(x+(ARR[i][0]-WORDS[LINEARR[i][j]][0])/2, y, WORDS[LINEARR[i][j]][0], WORDS[LINEARR[i][j]][1])// context.closePath()// 下划线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]][1]);context.closePath()context.stroke();context.restore();}}}}
2、横排
2.1 自动换行
_maxWidth = item.width * devicePixelRatio
_maxHeight = item.height * devicePixelRatiolet _w = 0
let _h = 0
let _p = ''// 计算所有字的宽度和高度
for(let i = 0; i < item.content.length; i++) {// 换行符if(item.content[i] === '\n') {ARR.push([_w, lineHeight || _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(_w + textWidth > item.width * devicePixelRatio) {ARR.push([_w, lineHeight || _h])LINEARR.push(_p)_w = textWidth_h = textHeight_p = item.content[i]} else {_w += textWidth_h = Math.max(textHeight, _h)_p += item.content[i]}
}
2.2 换行符换行
// 根据换行符分段
LINEARR = item.content.split('\n');// 计算所有段落的宽度和高度
for(let i = 0; i < LINEARR.length; i++) {let _w = 0let _h = 0for(let j = 0; j < LINEARR[i].length; j++) {/*let metrics = nulllet textWidth = 0let textHeight = 0const key = LINEARR[i] + item.italic + item.weight + item.fontSize + devicePixelRatio + (item?.webFontFamily || item?.fontFamily)if(useCommon().words[key]) {metrics = useCommon().words[key]textWidth = metrics.widthtextHeight = metrics.height} else {// 计算文字宽高metrics = context.measureText(LINEARR[i][j]);// 计算文本宽度// const textWidth = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;textWidth = metrics.width// 计算文本高度// 所有字在这个字体下的高度textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent// 当前文本字符串在这个字体下用的实际高度// const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; useCommon().$patch((state) => {state.words[key] = {width: textWidth,height: textHeight}})} */// 计算文字宽高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]}_w += textWidth_h = Math.max(textHeight, _h)}_maxWidth = Math.max(_maxWidth, _w)_maxHeight += (lineHeight || _h)ARR.push([_w, lineHeight || _h])
}
2.3 绘制逻辑
// 计算坐标并绘制文字
let x = item.left * devicePixelRatio
let y = item.top * devicePixelRatiofor(let i = 0; i < LINEARR.length; i++) {x = item.left * devicePixelRatio// 判断对齐方式switch(item.align) {case 'left':breakcase 'center':x += ((item.width * devicePixelRatio - ARR[i][0])/2)breakcase 'right':x += (item.width * devicePixelRatio - ARR[i][0])break}if(i > 0) {y += ARR[i-1][1]}for(let j = 0; j < LINEARR[i].length; j++) { if(j > 0) {x += WORDS[LINEARR[i][j-1]][0]}// 自定义阴影if(item?.shadow) {context.save()context.filter = `blur(${item.shadow.blur}px)`context.fillStyle = item.shadow.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize/120) * devicePixelRatioitem.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x+s*item.shadow.x, y+s*item.shadow.y);context.fillText(LINEARR[i][j], x+s*item.shadow.x, y+s*item.shadow.y);context.restore()}// 主体文字item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x, y);context.fillText(LINEARR[i][j], x, y);// context.beginPath()// context.strokeRect(x, y, WORDS[LINEARR[i][j]][0], WORDS[LINEARR[i][j]][1])// context.closePath()// 下划线if(item?.underline) {context.save()context.strokeStyle = item.fontColor; // 描边颜色context.lineWidth = Math.max(item.fontSize * devicePixelRatio/12, 1)context.beginPath()context.moveTo(x, y + ARR[i][1]);context.lineTo(x + WORDS[LINEARR[i][j]][0], y+ ARR[i][1]);context.closePath()context.stroke();context.restore();}}}
3、整体基本逻辑代码
import { isCstr } from './index'
// import { useCommon } from '@/stores/common.ts';(function(global) {global.OffscreenCanvasRenderingContext2D.prototype.wrapText = global.CanvasRenderingContext2D.prototype.wrapText = function(item, e) {// console.error(item, e)// 默认参数const context = this;// const canvas = context.canvas;// 文本对齐方式context.textAlign = 'left'// 文本基线context.textBaseline = 'top'// 可配置参数let noDraw = e?.noDraw || '0' // 不绘制,主要用于获取实际宽高const devicePixelRatio = noDraw === '1' ? 1 : (e?.devicePixelRatio || 1) // 控制缩放倍数let wrapLine = e?.wrapLine || 0 // 自动换行let lineHeightType = e?.lineHeightType || 1 // 行高类型let lineHeight = 0 // 使用行高参数if(typeof lineHeightType === 'number') {lineHeight = item.fontSize * devicePixelRatio * lineHeightType} else if(lineHeightType === 'normal') {// 计算文字宽高const metrics = context.measureText('字');// 计算文本宽度// const textWidth = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;// const textWidth = metrics.width// 计算文本高度// 所有字在这个字体下的高度const textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent// 当前文本字符串在这个字体下用的实际高度// const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; lineHeight = textHeight}// 返回的参数let _maxWidth = 0let _maxHeight = 0// 计算参数 const ARR = [] // 存储每行的宽高const WORDS = {} // 存储每个字的宽高let LINEARR = [] // 片段数组// 竖排if(item.direction === "vertical") {// console.error('竖排')// 自动换行if(wrapLine) {// console.error('自动换行')_maxWidth = item.width * devicePixelRatio_maxHeight = item.height * devicePixelRatiolet _w = 0let _h = 0let _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]}}}} else {// 根据换行符分段LINEARR = item.content.split('\n');// 计算所有段落的宽度和高度for(let i = 0; i < LINEARR.length; i++) {let _w = 0let _h = 0for(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])}}if(noDraw === '0') {// 计算坐标并绘制文字let x = (item.left + item.width) * devicePixelRatio let y = item.top * devicePixelRatiofor(let i = 0; i < LINEARR.length; i++) {x -= ARR[i][0]y = item.top * devicePixelRatio// 判断对齐方式switch(item.align) {case 'top':breakcase 'middle':y += ((item.height * devicePixelRatio - ARR[i][1])/2)breakcase '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.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize/120)* devicePixelRatioitem.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.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize/120)* devicePixelRatioitem.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[LINEARR[i][j]][0])/2+s*item.shadow.x, y+s*item.shadow.y);context.restore()}// 主体文字item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x+(ARR[i][0]-WORDS[LINEARR[i][j]][0])/2, y);context.fillText(LINEARR[i][j], x+(ARR[i][0]-WORDS[LINEARR[i][j]][0])/2, y);// context.beginPath()// context.strokeRect(x+(ARR[i][0]-WORDS[LINEARR[i][j]][0])/2, y, WORDS[LINEARR[i][j]][0], WORDS[LINEARR[i][j]][1])// context.closePath()// 下划线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]][1]);context.closePath()context.stroke();context.restore();}}}}} } // 横排else {// console.error('横排')// 自动换行if(wrapLine) {// console.error('自动换行')_maxWidth = item.width * devicePixelRatio_maxHeight = item.height * devicePixelRatiolet _w = 0let _h = 0let _p = ''// 计算所有字的宽度和高度for(let i = 0; i < item.content.length; i++) {// 换行符if(item.content[i] === '\n') {ARR.push([_w, lineHeight || _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(_w + textWidth > item.width * devicePixelRatio) {ARR.push([_w, lineHeight || _h])LINEARR.push(_p)_w = textWidth_h = textHeight_p = item.content[i]} else {_w += textWidth_h = Math.max(textHeight, _h)_p += item.content[i]}}} else {// 根据换行符分段LINEARR = item.content.split('\n');// 计算所有段落的宽度和高度for(let i = 0; i < LINEARR.length; i++) {let _w = 0let _h = 0for(let j = 0; j < LINEARR[i].length; j++) {/*let metrics = nulllet textWidth = 0let textHeight = 0const key = LINEARR[i] + item.italic + item.weight + item.fontSize + devicePixelRatio + (item?.webFontFamily || item?.fontFamily)if(useCommon().words[key]) {metrics = useCommon().words[key]textWidth = metrics.widthtextHeight = metrics.height} else {// 计算文字宽高metrics = context.measureText(LINEARR[i][j]);// 计算文本宽度// const textWidth = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;textWidth = metrics.width// 计算文本高度// 所有字在这个字体下的高度textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent// 当前文本字符串在这个字体下用的实际高度// const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; useCommon().$patch((state) => {state.words[key] = {width: textWidth,height: textHeight}})} */// 计算文字宽高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]}_w += textWidth_h = Math.max(textHeight, _h)}_maxWidth = Math.max(_maxWidth, _w)_maxHeight += (lineHeight || _h)ARR.push([_w, lineHeight || _h])}}if(noDraw === '0') {// 计算坐标并绘制文字let x = item.left * devicePixelRatiolet y = item.top * devicePixelRatiofor(let i = 0; i < LINEARR.length; i++) {x = item.left * devicePixelRatio// 判断对齐方式switch(item.align) {case 'left':breakcase 'center':x += ((item.width * devicePixelRatio - ARR[i][0])/2)breakcase 'right':x += (item.width * devicePixelRatio - ARR[i][0])break}if(i > 0) {y += ARR[i-1][1]}for(let j = 0; j < LINEARR[i].length; j++) { if(j > 0) {x += WORDS[LINEARR[i][j-1]][0]}// 自定义阴影if(item?.shadow) {context.save()context.filter = `blur(${item.shadow.blur}px)`context.fillStyle = item.shadow.colorcontext.strokeStyle = item.shadow.colorconst s = (item.fontSize/120) * devicePixelRatioitem.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x+s*item.shadow.x, y+s*item.shadow.y);context.fillText(LINEARR[i][j], x+s*item.shadow.x, y+s*item.shadow.y);context.restore()}// 主体文字item.stroke && item.stroke.width > 0 && context.strokeText(LINEARR[i][j], x, y);context.fillText(LINEARR[i][j], x, y);// context.beginPath()// context.strokeRect(x, y, WORDS[LINEARR[i][j]][0], WORDS[LINEARR[i][j]][1])// context.closePath()// 下划线if(item?.underline) {context.save()context.strokeStyle = item.fontColor; // 描边颜色context.lineWidth = Math.max(item.fontSize * devicePixelRatio/12, 1)context.beginPath()context.moveTo(x, y + ARR[i][1]);context.lineTo(x + WORDS[LINEARR[i][j]][0], y+ ARR[i][1]);context.closePath()context.stroke();context.restore();}}}}}return {line: ARR.length,maxWidth: _maxWidth,maxHeight: _maxHeight}}})(window);
// 判断中英文字符
export function isCstr(val) {const _Cstr = "【〗〈){」】《〉〔}『〖》(〕「』‹»||›…¦—‥、«–﹒。′~ˉ•ˋ‵ˆ―ˇ´_, L'【"// 英文if(escape(val).indexOf("%u")<0) {return false} else {if(_Cstr.indexOf(val) != -1) {return false}return true}
}
二、自定义花字应用案例
/*** 绘制文本* @param {*} item * @param {*} ctx * @param {*} e * @returns */
export function drawText(item, ctx, e) {let res = nullctx.save()const noDraw = e?.noDraw || '0'const devicePixelRatio = e?.devicePixelRatio || 1ctx.lineJoin = 'round' // 圆角// 字体样式 normal oblique// 字体粗细 normal bold lighter// 是否斜体let _italic = item.italic ? 'oblique ' : 'normal '// 字重let _weight = item.weight ? item.weight + ' ' : ''ctx.font = _italic + _weight + (item.fontSize * devicePixelRatio) + 'px ' + (item?.webFontFamily || item?.fontFamily)// 下划线// let underline = item.underline// 是否竖排let vertical = item.direction === 'vertical'// 文本对齐方式ctx.textAlign = vertical ? 'left' : item.align// 文本基线ctx.textBaseline = 'middle'// 绘制文字背景色ctx.fillStyle = item.fontBgColorctx.fillRect(item.left * devicePixelRatio, item.top * devicePixelRatio, item.width * devicePixelRatio, item.height * devicePixelRatio)// 阴影// if (item.shadow && item.shadow.blur) {// ctx.shadowColor = item.shadow.color// ctx.shadowBlur = item.shadow.blur * devicePixelRatio // 模糊级别// ctx.shadowOffsetX = item.shadow.x * devicePixelRatio // 阴影与形状的水平距离// ctx.shadowOffsetY = item.shadow.y * devicePixelRatio // 阴影与形状的垂直距离// // if(item.rotate == 0) {// // ctx.shadowOffsetX = item.shadow.x * devicePixelRatio // 阴影与形状的水平距离// // ctx.shadowOffsetY = item.shadow.y * devicePixelRatio // 阴影与形状的垂直距离// // }// // else if(item.rotate > 0) {// // ctx.shadowOffsetX = -item.shadow.x * devicePixelRatio // 阴影与形状的水平距离// // ctx.shadowOffsetY = item.shadow.y * devicePixelRatio // 阴影与形状的垂直距离// // } // // else if(item.rotate < 0) {// // ctx.shadowOffsetX = item.shadow.x * devicePixelRatio // 阴影与形状的水平距离// // ctx.shadowOffsetY = -item.shadow.y * devicePixelRatio // 阴影与形状的垂直距离// // }// }// 绘制描边颜色// 渐变if (item.stroke && item.stroke.width > 0) {if(Object.prototype.toString.call(item.stroke.color) === '[object Array]') {let gradient = '#00000000'if(noDraw === '0') {gradient = workWithGradient(item.stroke?.colorGradientType, ctx, [...item.stroke.color], [item.left* devicePixelRatio, item.top* devicePixelRatio, item.left* devicePixelRatio, (item.top+item.height)* devicePixelRatio])}ctx.strokeStyle = gradient // 描边颜色} else {ctx.strokeStyle = item.stroke.color // 描边颜色}ctx.lineWidth = (item.fontSize/120)*item.stroke.width * devicePixelRatio // 描边大小}// 绘制文字颜色// 渐变if(Object.prototype.toString.call(item.fontColor) === '[object Array]') {let gradient = '#00000000'if(noDraw === '0') {gradient = workWithGradient(item?.colorGradientType, ctx, [...item.fontColor], [item.left* devicePixelRatio, item.top* devicePixelRatio, item.left* devicePixelRatio, (item.top+item.height)* devicePixelRatio])}ctx.fillStyle = gradient // 文字颜色} else {ctx.fillStyle = item.fontColor // 文字颜色}const _content = item.type == 'zm' ? '智能字幕' : item.content?.replace(/(\[[\=\#]([^\[\]]*)\])/g, '')item.content = _content// 绘制花字if(item?.floweredFont && noDraw === '0') {res = drawFlowerWord(item, ctx, {...e})} else {res = ctx.wrapText(item, e)}ctx.restore()return res
}
/*** 绘制花字* @param {*} _item * @param {*} ctx * @param {*} e */
export function drawFlowerWord(item, ctx, e) {// console.error('绘制花字:', item, e)// ctx.canvas.style.fontSmoothing = 'antialiased';const devicePixelRatio = e?.devicePixelRatio || 1let res = null// 绘制发光阴影if(item.floweredFont?.glow?.size) {ctx.save()let _item = _.cloneDeep(item)// 绘制发光阴影_item.shadow = {x: 0, // 阴影距离文字的x轴距离y: 0, // 阴影距离文字的y轴距离color: item.floweredFont.glow.color, // 阴影颜色width: 0, // 阴影宽度blur: item.floweredFont.glow.size // 模糊程度}delete _item.strokeres = ctx.wrapText(_item, e)ctx.restore()}// 绘制外大描边if(item.floweredFont?.outerStroke?.size) {ctx.save()let _item = _.cloneDeep(item)const s = (_item.fontSize/120) * devicePixelRatio_item.top = _item.top + s * _item.floweredFont.outerStroke.y_item.left = _item.left + s * _item.floweredFont.outerStroke.x_item.stroke = {color: _item.floweredFont.outerStroke.color,width: _item.floweredFont.outerStroke.size}ctx.strokeStyle = _item.stroke.color // 描边颜色ctx.lineWidth = (_item.fontSize/120) * _item.stroke.width * devicePixelRatio // 描边大小ctx.fillStyle = _item.floweredFont.outerStroke.colordelete _item.shadowres = ctx.wrapText(_item, e)ctx.restore()}ctx.save()res = ctx.wrapText(item, e)ctx.restore()return res}