const QR = require('./qrcode.js'); const GD = require('./gradient.js'); require('./string-polyfill.js'); export const penCache = { // 用于存储带 id 的 view 的 rect 信息 viewRect: {}, textLines: {}, }; export const clearPenCache = id => { if (id) { penCache.viewRect[id] = null; penCache.textLines[id] = null; } else { penCache.viewRect = {}; penCache.textLines = {}; } }; export default class Painter { constructor(ctx, data) { this.ctx = ctx; this.data = data; } paint(callback) { this.style = { width: this.data.width.toPx(), height: this.data.height.toPx(), }; this._background(); for (const view of this.data.views) { this._drawAbsolute(view); } this.ctx.draw(false, () => { callback && callback(); }); } _background() { this.ctx.save(); const { width, height } = this.style; const bg = this.data.background; this.ctx.translate(width / 2, height / 2); this._doClip(this.data.borderRadius, width, height); if (!bg) { // 如果未设置背景,则默认使用透明色 this.ctx.fillStyle = 'transparent'; this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') { // 背景填充颜色 this.ctx.fillStyle = bg; this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else if (GD.api.isGradient(bg)) { GD.api.doGradient(bg, width, height, this.ctx); this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else { // 背景填充图片 this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height); } this.ctx.restore(); } _drawAbsolute(view) { if (!(view && view.type)) { // 过滤无效 view return; } // 证明 css 为数组形式,需要合并 if (view.css && view.css.length) { /* eslint-disable no-param-reassign */ view.css = Object.assign(...view.css); } switch (view.type) { case 'image': this._drawAbsImage(view); break; case 'text': this._fillAbsText(view); break; case 'inlineText': this._fillAbsInlineText(view); break; case 'rect': this._drawAbsRect(view); break; case 'qrcode': this._drawQRCode(view); break; default: break; } } _border({ borderRadius = 0, width, height, borderWidth = 0, borderStyle = 'solid' }) { let r1 = 0, r2 = 0, r3 = 0, r4 = 0; const minSize = Math.min(width, height); if (borderRadius) { const border = borderRadius.split(/\s+/); if (border.length === 4) { r1 = Math.min(border[0].toPx(false, minSize), width / 2, height / 2); r2 = Math.min(border[1].toPx(false, minSize), width / 2, height / 2); r3 = Math.min(border[2].toPx(false, minSize), width / 2, height / 2); r4 = Math.min(border[3].toPx(false, minSize), width / 2, height / 2); } else { r1 = r2 = r3 = r4 = Math.min(borderRadius && borderRadius.toPx(false, minSize), width / 2, height / 2); } } const lineWidth = borderWidth && borderWidth.toPx(false, minSize); this.ctx.lineWidth = lineWidth; if (borderStyle === 'dashed') { this.ctx.setLineDash([(lineWidth * 4) / 3, (lineWidth * 4) / 3]); // this.ctx.lineDashOffset = 2 * lineWidth } else if (borderStyle === 'dotted') { this.ctx.setLineDash([lineWidth, lineWidth]); } const notSolid = borderStyle !== 'solid'; this.ctx.beginPath(); notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则 r1 !== 0 && this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1 + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI); //左上角圆弧 this.ctx.lineTo( r2 === 0 ? (notSolid ? width / 2 : width / 2 + lineWidth / 2) : width / 2 - r2, -height / 2 - lineWidth / 2, ); // 顶边线 notSolid && r2 === 0 && this.ctx.moveTo(width / 2 + lineWidth / 2, -height / 2 - lineWidth); // 右边虚线规避重叠规则 r2 !== 0 && this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2 + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI); // 右上角圆弧 this.ctx.lineTo( width / 2 + lineWidth / 2, r3 === 0 ? (notSolid ? height / 2 : height / 2 + lineWidth / 2) : height / 2 - r3, ); // 右边线 notSolid && r3 === 0 && this.ctx.moveTo(width / 2 + lineWidth, height / 2 + lineWidth / 2); // 底边虚线规避重叠规则 r3 !== 0 && this.ctx.arc(width / 2 - r3, height / 2 - r3, r3 + lineWidth / 2, 0, 0.5 * Math.PI); // 右下角圆弧 this.ctx.lineTo( r4 === 0 ? (notSolid ? -width / 2 : -width / 2 - lineWidth / 2) : -width / 2 + r4, height / 2 + lineWidth / 2, ); // 底边线 notSolid && r4 === 0 && this.ctx.moveTo(-width / 2 - lineWidth / 2, height / 2 + lineWidth); // 左边虚线规避重叠规则 r4 !== 0 && this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4 + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI); // 左下角圆弧 this.ctx.lineTo( -width / 2 - lineWidth / 2, r1 === 0 ? (notSolid ? -height / 2 : -height / 2 - lineWidth / 2) : -height / 2 + r1, ); // 左边线 notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则 if (!notSolid) { this.ctx.closePath(); } } /** * 根据 borderRadius 进行裁减 */ _doClip(borderRadius, width, height, borderStyle) { if (borderRadius && width && height) { // 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会 // globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white,相对默认的 black 要好点 this.ctx.globalAlpha = 0; this.ctx.fillStyle = 'white'; this._border({ borderRadius, width, height, borderStyle, }); this.ctx.fill(); // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性 if (!(getApp().systemInfo && getApp().systemInfo.version <= '6.6.6' && getApp().systemInfo.platform === 'ios')) { this.ctx.clip(); } this.ctx.globalAlpha = 1; } } /** * 画边框 */ _doBorder(view, width, height) { if (!view.css) { return; } const { borderRadius, borderWidth, borderColor, borderStyle } = view.css; if (!borderWidth) { return; } this.ctx.save(); this._preProcess(view, true); this.ctx.strokeStyle = borderColor || 'black'; this._border({ borderRadius, width, height, borderWidth, borderStyle, }); this.ctx.stroke(); this.ctx.restore(); } _preProcess(view, notClip) { let width = 0; let height; let extra; const paddings = this._doPaddings(view); switch (view.type) { case 'inlineText': { { // 计算行数 let lines = 0; // 文字总长度 let textLength = 0; // 行高 let lineHeight = 0; const textList = view.textList || []; for (let i = 0; i < textList.length; i++) { let subView = textList[i]; const fontWeight = subView.css.fontWeight || '400'; const textStyle = subView.css.textStyle || 'normal'; if (!subView.css.fontSize) { subView.css.fontSize = '20rpx'; } this.ctx.font = `${textStyle} ${fontWeight} ${subView.css.fontSize.toPx()}px "${subView.css.fontFamily || 'sans-serif'}"`; textLength += this.ctx.measureText(subView.text).width; let tempLineHeight = subView.css.lineHeight ? subView.css.lineHeight.toPx() : subView.css.fontSize.toPx(); lineHeight = Math.max(lineHeight, tempLineHeight); } width = view.css.width ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3] : textLength;; const calLines = Math.ceil(textLength / width); lines += calLines; // lines = view.css.maxLines < lines ? view.css.maxLines : lines; height = lineHeight * lines; extra = { lines: lines, lineHeight: lineHeight, // textArray: textArray, // linesArray: linesArray, }; } break; } case 'text': { const textArray = String(view.text).split('\n'); // 处理多个连续的'\n' for (let i = 0; i < textArray.length; ++i) { if (textArray[i] === '') { textArray[i] = ' '; } } const fontWeight = view.css.fontWeight || '400'; const textStyle = view.css.textStyle || 'normal'; if (!view.css.fontSize) { view.css.fontSize = '20rpx'; } this.ctx.font = `${textStyle} ${fontWeight} ${view.css.fontSize.toPx()}px "${ view.css.fontFamily || 'sans-serif' }"`; // 计算行数 let lines = 0; const linesArray = []; for (let i = 0; i < textArray.length; ++i) { const textLength = this.ctx.measureText(textArray[i]).width; const minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3]; let partWidth = view.css.width ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3] : textLength; if (partWidth < minWidth) { partWidth = minWidth; } const calLines = Math.ceil(textLength / partWidth); // 取最长的作为 width width = partWidth > width ? partWidth : width; lines += calLines; linesArray[i] = calLines; } lines = view.css.maxLines < lines ? view.css.maxLines : lines; const lineHeight = view.css.lineHeight ? view.css.lineHeight.toPx() : view.css.fontSize.toPx(); height = lineHeight * lines; extra = { lines: lines, lineHeight: lineHeight, textArray: textArray, linesArray: linesArray, }; break; } case 'image': { // image的长宽设置成auto的逻辑处理 const ratio = getApp().systemInfo.pixelRatio ? getApp().systemInfo.pixelRatio : 2; // 有css却未设置width或height,则默认为auto if (view.css) { if (!view.css.width) { view.css.width = 'auto'; } if (!view.css.height) { view.css.height = 'auto'; } } if (!view.css || (view.css.width === 'auto' && view.css.height === 'auto')) { width = Math.round(view.sWidth / ratio); height = Math.round(view.sHeight / ratio); } else if (view.css.width === 'auto') { height = view.css.height.toPx(false, this.style.height); width = (view.sWidth / view.sHeight) * height; } else if (view.css.height === 'auto') { width = view.css.width.toPx(false, this.style.width); height = (view.sHeight / view.sWidth) * width; } else { width = view.css.width.toPx(false, this.style.width); height = view.css.height.toPx(false, this.style.height); } break; } default: if (!(view.css.width && view.css.height)) { console.error('You should set width and height'); return; } width = view.css.width.toPx(false, this.style.width); height = view.css.height.toPx(false, this.style.height); break; } let x; if (view.css && view.css.right) { if (typeof view.css.right === 'string') { x = this.style.width - view.css.right.toPx(true, this.style.width); } else { // 可以用数组方式,把文字长度计算进去 // [right, 文字id, 乘数(默认 1)] const rights = view.css.right; x = this.style.width - rights[0].toPx(true, this.style.width) - penCache.viewRect[rights[1]].width * (rights[2] || 1); } } else if (view.css && view.css.left) { if (typeof view.css.left === 'string') { x = view.css.left.toPx(true, this.style.width); } else { const lefts = view.css.left; x = lefts[0].toPx(true, this.style.width) + penCache.viewRect[lefts[1]].width * (lefts[2] || 1); } } else { x = 0; } //const y = view.css && view.css.bottom ? this.style.height - height - view.css.bottom.toPx(true) : (view.css && view.css.top ? view.css.top.toPx(true) : 0); let y; if (view.css && view.css.bottom) { y = this.style.height - height - view.css.bottom.toPx(true, this.style.height); } else { if (view.css && view.css.top) { if (typeof view.css.top === 'string') { y = view.css.top.toPx(true, this.style.height); } else { const tops = view.css.top; y = tops[0].toPx(true, this.style.height) + penCache.viewRect[tops[1]].height * (tops[2] || 1); } } else { y = 0; } } const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0; // 当设置了 right 时,默认 align 用 right,反之用 left const align = view.css && view.css.align ? view.css.align : view.css && view.css.right ? 'right' : 'left'; const verticalAlign = view.css && view.css.verticalAlign ? view.css.verticalAlign : 'top'; // 记录绘制时的画布 let xa = 0; switch (align) { case 'center': xa = x; break; case 'right': xa = x - width / 2; break; default: xa = x + width / 2; break; } let ya = 0; switch (verticalAlign) { case 'center': ya = y; break; case 'bottom': ya = y - height / 2; break; default: ya = y + height / 2; break; } this.ctx.translate(xa, ya); // 记录该 view 的有效点击区域 // TODO ,旋转和裁剪的判断 // 记录在真实画布上的左侧 let left = x; if (align === 'center') { left = x - width / 2; } else if (align === 'right') { left = x - width; } var top = y; if (verticalAlign === 'center') { top = y - height / 2; } else if (verticalAlign === 'bottom') { top = y - height; } if (view.rect) { view.rect.left = left; view.rect.top = top; view.rect.right = left + width; view.rect.bottom = top + height; view.rect.x = view.css && view.css.right ? x - width : x; view.rect.y = y; } else { view.rect = { left: left, top: top, right: left + width, bottom: top + height, x: view.css && view.css.right ? x - width : x, y: y, }; } view.rect.left = view.rect.left - paddings[3]; view.rect.top = view.rect.top - paddings[0]; view.rect.right = view.rect.right + paddings[1]; view.rect.bottom = view.rect.bottom + paddings[2]; if (view.type === 'text') { view.rect.minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3]; } this.ctx.rotate(angle); if (!notClip && view.css && view.css.borderRadius && view.type !== 'rect') { this._doClip(view.css.borderRadius, width, height, view.css.borderStyle); } this._doShadow(view); if (view.id) { penCache.viewRect[view.id] = { width, height, left: view.rect.left, top: view.rect.top, right: view.rect.right, bottom: view.rect.bottom, }; } return { width: width, height: height, x: x, y: y, extra: extra, }; } _doPaddings(view) { const { padding } = view.css ? view.css : {}; let pd = [0, 0, 0, 0]; if (padding) { const pdg = padding.split(/\s+/); if (pdg.length === 1) { const x = pdg[0].toPx(); pd = [x, x, x, x]; } if (pdg.length === 2) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); pd = [x, y, x, y]; } if (pdg.length === 3) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); const z = pdg[2].toPx(); pd = [x, y, z, y]; } if (pdg.length === 4) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); const z = pdg[2].toPx(); const a = pdg[3].toPx(); pd = [x, y, z, a]; } } return pd; } // 画文字的背景图片 _doBackground(view) { this.ctx.save(); const { width: rawWidth, height: rawHeight } = this._preProcess(view, true); const { background } = view.css; let pd = this._doPaddings(view); const width = rawWidth + pd[1] + pd[3]; const height = rawHeight + pd[0] + pd[2]; this._doClip(view.css.borderRadius, width, height, view.css.borderStyle); if (GD.api.isGradient(background)) { GD.api.doGradient(background, width, height, this.ctx); } else { this.ctx.fillStyle = background; } this.ctx.fillRect(-(width / 2), -(height / 2), width, height); this.ctx.restore(); } _drawQRCode(view) { this.ctx.save(); const { width, height } = this._preProcess(view); QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color); this.ctx.restore(); this._doBorder(view, width, height); } _drawAbsImage(view) { if (!view.url) { return; } this.ctx.save(); const { width, height } = this._preProcess(view); // 获得缩放到图片大小级别的裁减框 let rWidth = view.sWidth; let rHeight = view.sHeight; let startX = 0; let startY = 0; // 绘画区域比例 const cp = width / height; // 原图比例 const op = view.sWidth / view.sHeight; if (cp >= op) { rHeight = rWidth / cp; startY = Math.round((view.sHeight - rHeight) / 2); } else { rWidth = rHeight * cp; startX = Math.round((view.sWidth - rWidth) / 2); } if (view.css && view.css.mode === 'scaleToFill') { this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height); } else { this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height); view.rect.startX = startX / view.sWidth; view.rect.startY = startY / view.sHeight; view.rect.endX = (startX + rWidth) / view.sWidth; view.rect.endY = (startY + rHeight) / view.sHeight; } this.ctx.restore(); this._doBorder(view, width, height); } /** * * @param {*} view * @description 一行内文字多样式的方法 * * 暂不支持配置 text-align,默认left * 暂不支持配置 maxLines */ _fillAbsInlineText(view) { if (!view.textList) { return; } if (view.css.background) { // 生成背景 this._doBackground(view); } this.ctx.save(); const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius); const { lines, lineHeight } = extra; let staticX = -(width / 2); let lineIndex = 0; // 第几行 let x = staticX; // 开始x位置 let leftWidth = width; // 当前行剩余多少宽度可以使用 let getStyle = css => { const fontWeight = css.fontWeight || '400'; const textStyle = css.textStyle || 'normal'; if (!css.fontSize) { css.fontSize = '20rpx'; } return `${textStyle} ${fontWeight} ${css.fontSize.toPx()}px "${css.fontFamily || 'sans-serif'}"`; } // 遍历行内的文字数组 for (let j = 0; j < view.textList.length; j++) { const subView = view.textList[j]; // 某个文字开始位置 let start = 0; // 文字已使用的数量 let alreadyCount = 0; // 文字总长度 let textLength = subView.text.length; // 文字总宽度 let textWidth = this.ctx.measureText(subView.text).width; // 每个文字的平均宽度 let preWidth = Math.ceil(textWidth / textLength); // 循环写文字 while (alreadyCount < textLength) { // alreadyCount - start + 1 -> 当前摘取出来的文字 // 比较可用宽度,寻找最大可写文字长度 while ((alreadyCount - start + 1) * preWidth < leftWidth && alreadyCount < textLength) { alreadyCount++; } // 取出文字 let text = subView.text.substr(start, alreadyCount - start); const y = -(height / 2) + subView.css.fontSize.toPx() + lineIndex * lineHeight; // 设置文字样式 this.ctx.font = getStyle(subView.css); this.ctx.fillStyle = subView.css.color || 'black'; this.ctx.textAlign = 'left'; // 执行画布操作 if (subView.css.textStyle === 'stroke') { this.ctx.strokeText(text, x, y); } else { this.ctx.fillText(text, x, y); } // 当次已使用宽度 let currentUsedWidth = this.ctx.measureText(text).width; const fontSize = subView.css.fontSize.toPx(); // 画 textDecoration let textDecoration; if (subView.css.textDecoration) { this.ctx.lineWidth = fontSize / 13; this.ctx.beginPath(); if (/\bunderline\b/.test(subView.css.textDecoration)) { this.ctx.moveTo(x, y); this.ctx.lineTo(x + currentUsedWidth, y); textDecoration = { moveTo: [x, y], lineTo: [x + currentUsedWidth, y], }; } if (/\boverline\b/.test(subView.css.textDecoration)) { this.ctx.moveTo(x, y - fontSize); this.ctx.lineTo(x + currentUsedWidth, y - fontSize); textDecoration = { moveTo: [x, y - fontSize], lineTo: [x + currentUsedWidth, y - fontSize], }; } if (/\bline-through\b/.test(subView.css.textDecoration)) { this.ctx.moveTo(x, y - fontSize / 3); this.ctx.lineTo(x + currentUsedWidth, y - fontSize / 3); textDecoration = { moveTo: [x, y - fontSize / 3], lineTo: [x + currentUsedWidth, y - fontSize / 3], }; } this.ctx.closePath(); this.ctx.strokeStyle = subView.css.color; this.ctx.stroke(); } // 重置数据 start = alreadyCount; leftWidth -= currentUsedWidth; x += currentUsedWidth; // 如果剩余宽度 小于等于0 或者小于一个字的平均宽度,换行 if (leftWidth <= 0 || leftWidth < preWidth) { leftWidth = width; x = staticX; lineIndex++; } } } this.ctx.restore(); this._doBorder(view, width, height); } _fillAbsText(view) { if (!view.text) { return; } if (view.css.background) { // 生成背景 this._doBackground(view); } this.ctx.save(); const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius); this.ctx.fillStyle = view.css.color || 'black'; if (view.id && penCache.textLines[view.id]) { this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left'; for (const i of penCache.textLines[view.id]) { const { measuredWith, text, x, y, textDecoration } = i; if (view.css.textStyle === 'stroke') { this.ctx.strokeText(text, x, y, measuredWith); } else { this.ctx.fillText(text, x, y, measuredWith); } if (textDecoration) { const fontSize = view.css.fontSize.toPx(); this.ctx.lineWidth = fontSize / 13; this.ctx.beginPath(); this.ctx.moveTo(...textDecoration.moveTo); this.ctx.lineTo(...textDecoration.lineTo); this.ctx.closePath(); this.ctx.strokeStyle = view.css.color; this.ctx.stroke(); } } } else { const { lines, lineHeight, textArray, linesArray } = extra; // 如果设置了id,则保留 text 的长度 if (view.id) { let textWidth = 0; for (let i = 0; i < textArray.length; ++i) { const _w = this.ctx.measureText(textArray[i]).width; textWidth = _w > textWidth ? _w : textWidth; } penCache.viewRect[view.id].width = width ? (textWidth < width ? textWidth : width) : textWidth; } let lineIndex = 0; for (let j = 0; j < textArray.length; ++j) { const preLineLength = Math.ceil(textArray[j].length / linesArray[j]); let start = 0; let alreadyCount = 0; for (let i = 0; i < linesArray[j]; ++i) { // 绘制行数大于最大行数,则直接跳出循环 if (lineIndex >= lines) { break; } alreadyCount = preLineLength; let text = textArray[j].substr(start, alreadyCount); let measuredWith = this.ctx.measureText(text).width; // 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除 // 如果已经到文本末尾,也不要进行该循环 while ( start + alreadyCount <= textArray[j].length && (width - measuredWith > view.css.fontSize.toPx() || measuredWith - width > view.css.fontSize.toPx()) ) { if (measuredWith < width) { text = textArray[j].substr(start, ++alreadyCount); } else { if (text.length <= 1) { // 如果只有一个字符时,直接跳出循环 break; } text = textArray[j].substr(start, --alreadyCount); // break; } measuredWith = this.ctx.measureText(text).width; } start += text.length; // 如果是最后一行了,发现还有未绘制完的内容,则加... if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) { while (this.ctx.measureText(`${text}...`).width > width) { if (text.length <= 1) { // 如果只有一个字符时,直接跳出循环 break; } text = text.substring(0, text.length - 1); } text += '...'; measuredWith = this.ctx.measureText(text).width; } this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left'; let x; let lineX; switch (view.css.textAlign) { case 'center': x = 0; lineX = x - measuredWith / 2; break; case 'right': x = width / 2; lineX = x - measuredWith; break; default: x = -(width / 2); lineX = x; break; } const y = -(height / 2) + (lineIndex === 0 ? view.css.fontSize.toPx() : view.css.fontSize.toPx() + lineIndex * lineHeight); lineIndex++; if (view.css.textStyle === 'stroke') { this.ctx.strokeText(text, x, y, measuredWith); } else { this.ctx.fillText(text, x, y, measuredWith); } const fontSize = view.css.fontSize.toPx(); let textDecoration; if (view.css.textDecoration) { this.ctx.lineWidth = fontSize / 13; this.ctx.beginPath(); if (/\bunderline\b/.test(view.css.textDecoration)) { this.ctx.moveTo(lineX, y); this.ctx.lineTo(lineX + measuredWith, y); textDecoration = { moveTo: [lineX, y], lineTo: [lineX + measuredWith, y], }; } if (/\boverline\b/.test(view.css.textDecoration)) { this.ctx.moveTo(lineX, y - fontSize); this.ctx.lineTo(lineX + measuredWith, y - fontSize); textDecoration = { moveTo: [lineX, y - fontSize], lineTo: [lineX + measuredWith, y - fontSize], }; } if (/\bline-through\b/.test(view.css.textDecoration)) { this.ctx.moveTo(lineX, y - fontSize / 3); this.ctx.lineTo(lineX + measuredWith, y - fontSize / 3); textDecoration = { moveTo: [lineX, y - fontSize / 3], lineTo: [lineX + measuredWith, y - fontSize / 3], }; } this.ctx.closePath(); this.ctx.strokeStyle = view.css.color; this.ctx.stroke(); } if (view.id) { penCache.textLines[view.id] ? penCache.textLines[view.id].push({ text, x, y, measuredWith, textDecoration, }) : (penCache.textLines[view.id] = [ { text, x, y, measuredWith, textDecoration, }, ]); } } } } this.ctx.restore(); this._doBorder(view, width, height); } _drawAbsRect(view) { this.ctx.save(); const { width, height } = this._preProcess(view); if (GD.api.isGradient(view.css.color)) { GD.api.doGradient(view.css.color, width, height, this.ctx); } else { this.ctx.fillStyle = view.css.color; } const { borderRadius, borderStyle, borderWidth } = view.css; this._border({ borderRadius, width, height, borderWidth, borderStyle, }); this.ctx.fill(); this.ctx.restore(); this._doBorder(view, width, height); } // shadow 支持 (x, y, blur, color), 不支持 spread // shadow:0px 0px 10px rgba(0,0,0,0.1); _doShadow(view) { if (!view.css || !view.css.shadow) { return; } const box = view.css.shadow.replace(/,\s+/g, ',').split(/\s+/); if (box.length > 4) { console.error("shadow don't spread option"); return; } this.ctx.shadowOffsetX = parseInt(box[0], 10); this.ctx.shadowOffsetY = parseInt(box[1], 10); this.ctx.shadowBlur = parseInt(box[2], 10); this.ctx.shadowColor = box[3]; } _getAngle(angle) { return (Number(angle) * Math.PI) / 180; } }