904 lines
30 KiB
JavaScript
904 lines
30 KiB
JavaScript
|
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;
|
|||
|
}
|
|||
|
}
|