|
@@ -0,0 +1,941 @@
|
|
|
+import { toPx, CHAR_WIDTH_SCALE_MAP, isNumber, getImageInfo } from './utils'
|
|
|
+import { GD } from './gradient'
|
|
|
+import QR from './qrcode'
|
|
|
+let id = 0
|
|
|
+
|
|
|
+export class Draw {
|
|
|
+ constructor(context, canvas, use2dCanvas = false, isH5PathToBase64 = false, boundary) {
|
|
|
+ this.ctx = context
|
|
|
+ this.canvas = canvas || null
|
|
|
+ this.root = boundary
|
|
|
+ this.use2dCanvas = use2dCanvas
|
|
|
+ this.isH5PathToBase64 = isH5PathToBase64
|
|
|
+ }
|
|
|
+ roundRect(x, y, w, h, r, fill = false, stroke = false, ) {
|
|
|
+ if (r < 0) return
|
|
|
+ const {ctx} = this
|
|
|
+ ctx.beginPath()
|
|
|
+ if(!r) {
|
|
|
+ ctx.rect(x, y, w, h)
|
|
|
+ } else if(typeof r === 'number' && [0,1,-1].includes(w - r * 2) && [0, 1, -1].includes(h - r * 2)) {
|
|
|
+ ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 2)
|
|
|
+ } else {
|
|
|
+ let {
|
|
|
+ borderTopLeftRadius: tl = r || 0,
|
|
|
+ borderTopRightRadius: tr = r || 0,
|
|
|
+ borderBottomRightRadius: br = r || 0,
|
|
|
+ borderBottomLeftRadius: bl = r || 0
|
|
|
+ } = r || {r,r,r,r}
|
|
|
+ ctx.beginPath()
|
|
|
+ // 右下角
|
|
|
+ ctx.arc(x + w - br, y + h - br, br, 0, Math.PI * 0.5)
|
|
|
+ ctx.lineTo(x + bl, y + h)
|
|
|
+ // 左下角
|
|
|
+ ctx.arc(x + bl, y + h - bl, bl, Math.PI * 0.5, Math.PI)
|
|
|
+ ctx.lineTo(x, y + tl)
|
|
|
+ // 左上角
|
|
|
+ ctx.arc(x + tl, y + tl, tl, Math.PI, Math.PI * 1.5)
|
|
|
+ ctx.lineTo(x + w - tr, y)
|
|
|
+ // 右上角
|
|
|
+ ctx.arc(x + w - tr, y + tr, tr, Math.PI * 1.5, Math.PI * 2)
|
|
|
+ ctx.lineTo(x + w, y + h - br)
|
|
|
+ }
|
|
|
+ ctx.closePath()
|
|
|
+ if (stroke) ctx.stroke()
|
|
|
+ if (fill) ctx.fill()
|
|
|
+ }
|
|
|
+ measureText(text, fontSize) {
|
|
|
+ const { ctx } = this
|
|
|
+ // #ifndef APP-PLUS
|
|
|
+ return ctx.measureText(text).width
|
|
|
+ // #endif
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ // app measureText为0需要累加计算0
|
|
|
+ return text.split("").reduce((widthScaleSum, char) => {
|
|
|
+ let code = char.charCodeAt(0);
|
|
|
+ let widthScale = CHAR_WIDTH_SCALE_MAP[code - 0x20] || 1;
|
|
|
+ return widthScaleSum + widthScale;
|
|
|
+ }, 0) * fontSize;
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ setFont({fontFamily: ff = 'sans-serif', fontSize: fs = 14, fontWeight: fw = 'normal' , textStyle: ts = 'normal'}) {
|
|
|
+ let ctx = this.ctx
|
|
|
+ // 设置属性
|
|
|
+ // #ifndef MP-TOUTIAO
|
|
|
+ // fw = fw == 'bold' ? 'bold' : 'normal'
|
|
|
+ // ts = ts == 'italic' ? 'italic' : 'normal'
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP-TOUTIAO
|
|
|
+ fw = fw == 'bold' ? 'bold' : ''
|
|
|
+ ts = ts == 'italic' ? 'italic' : ''
|
|
|
+ // #endif
|
|
|
+ // fs = toPx(fs)
|
|
|
+ ctx.font = `${ts} ${fw} ${fs}px ${ff}`;
|
|
|
+ }
|
|
|
+ setTransform(box, {transform}) {
|
|
|
+ const {ctx} = this
|
|
|
+ const {
|
|
|
+ scaleX = 1,
|
|
|
+ scaleY = 1,
|
|
|
+ translateX = 0,
|
|
|
+ translateY = 0,
|
|
|
+ rotate = 0,
|
|
|
+ skewX = 0,
|
|
|
+ skewY = 0
|
|
|
+ } = transform || {}
|
|
|
+ let {
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h
|
|
|
+ } = box
|
|
|
+
|
|
|
+ ctx.scale(scaleX, scaleY)
|
|
|
+ ctx.translate(
|
|
|
+ w * (scaleX > 0 ? 1 : -1) / 2 + (x + translateX) / scaleX,
|
|
|
+ h * (scaleY > 0 ? 1 : -1) / 2 + (y + translateY) / scaleY)
|
|
|
+
|
|
|
+ if(rotate) {
|
|
|
+ ctx.rotate(rotate * Math.PI / 180)
|
|
|
+ }
|
|
|
+ if(skewX || skewY) {
|
|
|
+ ctx.transform(1, Math.tan(skewY * Math.PI/180), Math.tan(skewX * Math.PI/180), 1 , 0, 0)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setBackground(bd, w, h) {
|
|
|
+ const {ctx} = this
|
|
|
+ if (!bd) {
|
|
|
+ // #ifndef MP-TOUTIAO || MP-BAIDU
|
|
|
+ ctx.setFillStyle('transparent')
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP-TOUTIAO || MP-BAIDU
|
|
|
+ ctx.setFillStyle('rgba(0,0,0,0)')
|
|
|
+ // #endif
|
|
|
+ } else if(GD.isGradient(bd)) {
|
|
|
+ GD.doGradient(bd, w, h, ctx);
|
|
|
+ } else {
|
|
|
+ ctx.setFillStyle(bd)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setShadow({boxShadow: bs = []}) {
|
|
|
+ // #ifndef APP-NVUE
|
|
|
+ const {ctx} = this
|
|
|
+ if (bs.length) {
|
|
|
+ const [x, y, b, c] = bs
|
|
|
+ ctx.setShadow(x, y, b, c)
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ setBorder(box, style) {
|
|
|
+ const {ctx} = this
|
|
|
+ let {
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h
|
|
|
+ } = box
|
|
|
+ const {border, borderBottom, borderTop, borderRight, borderLeft, borderRadius: r, opacity = 1} = style;
|
|
|
+ const {
|
|
|
+ borderWidth : bw = 0,
|
|
|
+ borderStyle : bs,
|
|
|
+ borderColor : bc,
|
|
|
+ } = border || {}
|
|
|
+ const {
|
|
|
+ borderBottomWidth : bbw = bw,
|
|
|
+ borderBottomStyle : bbs = bs,
|
|
|
+ borderBottomColor : bbc= bc,
|
|
|
+ } = borderBottom || {}
|
|
|
+ const {
|
|
|
+ borderTopWidth: btw = bw,
|
|
|
+ borderTopStyle: bts = bs,
|
|
|
+ borderTopColor: btc = bc,
|
|
|
+ } = borderTop || {}
|
|
|
+ const {
|
|
|
+ borderRightWidth: brw = bw,
|
|
|
+ borderRightStyle: brs = bs,
|
|
|
+ borderRightColor: brc = bc,
|
|
|
+ } = borderRight || {}
|
|
|
+ const {
|
|
|
+ borderLeftWidth: blw = bw,
|
|
|
+ borderLeftStyle: bls = bs,
|
|
|
+ borderLeftColor: blc = bc,
|
|
|
+ } = borderLeft || {}
|
|
|
+
|
|
|
+ let {
|
|
|
+ borderTopLeftRadius: tl = r || 0,
|
|
|
+ borderTopRightRadius: tr = r || 0,
|
|
|
+ borderBottomRightRadius: br = r || 0,
|
|
|
+ borderBottomLeftRadius: bl = r || 0
|
|
|
+ } = r || {r,r,r,r}
|
|
|
+ if(!borderBottom && !borderLeft && !borderTop && !borderRight && !border) return;
|
|
|
+ const _borderType = (w, s, c) => {
|
|
|
+ // #ifndef APP-NVUE
|
|
|
+ if (s == 'dashed') {
|
|
|
+ // #ifdef MP
|
|
|
+ ctx.setLineDash([Math.ceil(w * 4 / 3), Math.ceil(w * 4 / 3)])
|
|
|
+ // #endif
|
|
|
+ // #ifndef MP
|
|
|
+ ctx.setLineDash([Math.ceil(w * 6), Math.ceil(w * 6)])
|
|
|
+ // #endif
|
|
|
+ } else if (s == 'dotted') {
|
|
|
+ ctx.setLineDash([w, w])
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ ctx.setStrokeStyle(c)
|
|
|
+ }
|
|
|
+ const _setBorder = (x1, y1, x2, y2, x3, y3, r1, r2, p1, p2, p3, bw, bs, bc) => {
|
|
|
+ ctx.save()
|
|
|
+ this.setOpacity(style)
|
|
|
+ this.setTransform(box, style)
|
|
|
+ ctx.setLineWidth(bw)
|
|
|
+ _borderType(bw, bs, bc)
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.arc(x1, y1, r1, Math.PI * p1, Math.PI * p2)
|
|
|
+ ctx.lineTo(x2, y2)
|
|
|
+ ctx.arc(x3, y3, r2, Math.PI * p2, Math.PI * p3)
|
|
|
+ ctx.stroke()
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+
|
|
|
+ if(border) {
|
|
|
+ ctx.save()
|
|
|
+ this.setOpacity(style)
|
|
|
+ this.setTransform(box, style)
|
|
|
+ ctx.setLineWidth(bw)
|
|
|
+ _borderType(bw, bs, bc)
|
|
|
+ this.roundRect(-w/2, -h/2, w, h, r, false, bc ? true : false)
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+ x = -w/2
|
|
|
+ y = -h/2
|
|
|
+ if(borderBottom) {
|
|
|
+ _setBorder(x + w - br, y + h - br, x + bl, y + h, x + bl, y + h - bl, br, bl, 0.25, 0.5, 0.75, bbw, bbs, bbc)
|
|
|
+ }
|
|
|
+ if(borderLeft) {
|
|
|
+ // 左下角
|
|
|
+ _setBorder(x + bl, y + h - bl, x, y + tl, x + tl, y + tl, bl, tl, 0.75, 1, 1.25, blw, bls, blc)
|
|
|
+ }
|
|
|
+ if(borderTop) {
|
|
|
+ // 左上角
|
|
|
+ _setBorder(x + tl, y + tl, x + w - tr, y, x + w - tr, y + tr, tl, tr, 1.25, 1.5, 1.75, btw, bts, btc)
|
|
|
+ }
|
|
|
+ if(borderRight) {
|
|
|
+ // 右上角
|
|
|
+ _setBorder(x + w - tr, y + tr, x + w, y + h - br, x + w - br, y + h - br, tr, br, 1.75, 2, 0.25, btw, bts, btc)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setOpacity({opacity = 1}) {
|
|
|
+ this.ctx.setGlobalAlpha(opacity)
|
|
|
+ }
|
|
|
+ drawView(box, style) {
|
|
|
+ const {ctx} = this
|
|
|
+ const {
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h
|
|
|
+ } = box
|
|
|
+ let {
|
|
|
+ borderRadius = 0,
|
|
|
+ border,
|
|
|
+ borderTop,
|
|
|
+ borderBottom,
|
|
|
+ borderLeft,
|
|
|
+ borderRight,
|
|
|
+ color = '#000000',
|
|
|
+ backgroundColor: bg,
|
|
|
+ rotate,
|
|
|
+ shadow
|
|
|
+ } = style || {}
|
|
|
+ ctx.save()
|
|
|
+ this.setOpacity(style)
|
|
|
+ this.setTransform(box, style)
|
|
|
+ this.setShadow(style)
|
|
|
+ this.setBackground(bg, w, h)
|
|
|
+ this.roundRect(-w/2, -h/2, w, h, borderRadius, true, false)
|
|
|
+ ctx.restore()
|
|
|
+ this.setBorder(box, style)
|
|
|
+ }
|
|
|
+ async drawImage(img, box = {}, style = {}, custom = true) {
|
|
|
+ await new Promise(async (resolve, reject) => {
|
|
|
+ const {ctx} = this
|
|
|
+ const canvas = this.canvas
|
|
|
+ const {
|
|
|
+ borderRadius = 0,
|
|
|
+ mode,
|
|
|
+ backgroundColor: bg,
|
|
|
+ } = style
|
|
|
+ let {
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h
|
|
|
+ } = box
|
|
|
+ ctx.save()
|
|
|
+ if(!custom) {
|
|
|
+ this.setOpacity(style)
|
|
|
+ this.setTransform(box, style)
|
|
|
+ this.setBackground(bg, w, h)
|
|
|
+ this.setShadow(style)
|
|
|
+ x = -w/2
|
|
|
+ y = -h/2
|
|
|
+ this.roundRect(x, y, w, h, borderRadius, true, false)
|
|
|
+ }
|
|
|
+ ctx.clip()
|
|
|
+ const _modeImage = (img) => {
|
|
|
+ // 获得图片原始大小
|
|
|
+ let rWidth = img.width
|
|
|
+ let rHeight = img.height
|
|
|
+ let startX = 0
|
|
|
+ let startY = 0
|
|
|
+ // 绘画区域比例
|
|
|
+ const cp = w / h
|
|
|
+ // 原图比例
|
|
|
+ const op = rWidth / rHeight
|
|
|
+ if (cp >= op) {
|
|
|
+ rHeight = rWidth / cp;
|
|
|
+ // startY = Math.round((h - rHeight) / 2)
|
|
|
+ } else {
|
|
|
+ rWidth = rHeight * cp;
|
|
|
+ startX = Math.round(((img.width || w) - rWidth) / 2)
|
|
|
+ }
|
|
|
+ if (mode === 'scaleToFill' || !img.width) {
|
|
|
+ ctx.drawImage(img.src, x, y, w, h);
|
|
|
+ } else {
|
|
|
+ // 百度小程序 开发工具 顺序有问题 暂不知晓真机
|
|
|
+ // #ifdef MP-BAIDU
|
|
|
+ ctx.drawImage(img.src, x, y, w, h, startX, startY, rWidth, rHeight)
|
|
|
+ // #endif
|
|
|
+ // #ifndef MP-BAIDU
|
|
|
+ ctx.drawImage(img.src, startX, startY, rWidth, rHeight, x, y, w, h)
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const _drawImage = (img) => {
|
|
|
+ if (this.use2dCanvas) {
|
|
|
+ const Image = canvas.createImage()
|
|
|
+ Image.onload = () => {
|
|
|
+ img.src = Image
|
|
|
+ _modeImage(img)
|
|
|
+ _restore()
|
|
|
+ }
|
|
|
+ Image.onerror = () => {
|
|
|
+ console.error(`createImage fail: ${img}`)
|
|
|
+ reject(new Error(`createImage fail: ${img}`))
|
|
|
+ }
|
|
|
+ Image.src = img.src
|
|
|
+ } else {
|
|
|
+ _modeImage(img)
|
|
|
+ _restore()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const _restore = () => {
|
|
|
+ ctx.restore()
|
|
|
+ this.setBorder(box, style)
|
|
|
+ setTimeout(() => {
|
|
|
+ resolve(true)
|
|
|
+ }, this.root.sleep)
|
|
|
+ }
|
|
|
+ if(typeof img === 'string') {
|
|
|
+ const {path: src, width, height} = await getImageInfo(img, this.isH5PathToBase64)
|
|
|
+ _drawImage({src, width, height})
|
|
|
+ } else {
|
|
|
+ _drawImage(img)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ drawText(text, box, style, rules) {
|
|
|
+ const {ctx} = this
|
|
|
+ let {
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ offsetLeft: ol = 0
|
|
|
+ } = box
|
|
|
+ let {
|
|
|
+ color = '#000000',
|
|
|
+ lineHeight = '1.4em',
|
|
|
+ fontSize = 14,
|
|
|
+ fontWeight,
|
|
|
+ fontFamily,
|
|
|
+ textStyle,
|
|
|
+ textAlign = 'left',
|
|
|
+ verticalAlign: va = 'top',
|
|
|
+ backgroundColor: bg,
|
|
|
+ maxLines,
|
|
|
+ textDecoration: td
|
|
|
+ } = style
|
|
|
+ lineHeight = toPx(lineHeight, fontSize)
|
|
|
+
|
|
|
+ if (!text) return
|
|
|
+ ctx.save()
|
|
|
+
|
|
|
+ this.setOpacity(style)
|
|
|
+ this.setTransform(box, style)
|
|
|
+ x = -w/2
|
|
|
+ y = -h/2
|
|
|
+ ctx.setTextBaseline(va)
|
|
|
+ this.setFont({fontFamily, fontSize, fontWeight, textStyle})
|
|
|
+ ctx.setTextAlign(textAlign)
|
|
|
+
|
|
|
+ if(bg) {
|
|
|
+ this.setBackground(bg, w, h)
|
|
|
+ this.roundRect(x, y, w, h, 1, bg)
|
|
|
+ }
|
|
|
+ this.setShadow(style)
|
|
|
+ ctx.setFillStyle(color)
|
|
|
+ let rulesObj = {};
|
|
|
+ if(rules) {
|
|
|
+ if (rules.word.length > 0) {
|
|
|
+ for (let i = 0; i < rules.word.length; i++) {
|
|
|
+ let startIndex = 0,
|
|
|
+ index;
|
|
|
+ while ((index = text.indexOf(rules.word[i], startIndex)) > -1) {
|
|
|
+ rulesObj[index] = {
|
|
|
+ reset: true
|
|
|
+ };
|
|
|
+ for (let j = 0; j < rules.word[i].length; j++) {
|
|
|
+ rulesObj[index + j] = {
|
|
|
+ reset: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+ startIndex = index + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 水平布局
|
|
|
+ switch (textAlign) {
|
|
|
+ case 'left':
|
|
|
+ break
|
|
|
+ case 'center':
|
|
|
+ x += 0.5 * w
|
|
|
+ break
|
|
|
+ case 'right':
|
|
|
+ x += w
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+ const textWidth = this.measureText(text, fontSize)
|
|
|
+ const actualHeight = Math.ceil(textWidth / w) * lineHeight
|
|
|
+ let paddingTop = Math.ceil((h - actualHeight) / 2)
|
|
|
+ if (paddingTop < 0) paddingTop = 0
|
|
|
+ // 垂直布局
|
|
|
+ switch (va) {
|
|
|
+ case 'top':
|
|
|
+ break
|
|
|
+ case 'middle':
|
|
|
+ y += fontSize / 2
|
|
|
+ break
|
|
|
+ case 'bottom':
|
|
|
+ y += fontSize
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘线
|
|
|
+ const _drawLine = (x, y, textWidth) => {
|
|
|
+ const { system } = uni.getSystemInfoSync()
|
|
|
+ if(/win|mac/.test(system)){
|
|
|
+ y += (fontSize / 3)
|
|
|
+ }
|
|
|
+ // 垂直布局
|
|
|
+ switch (va) {
|
|
|
+ case 'top':
|
|
|
+ break
|
|
|
+ case 'middle':
|
|
|
+ y -= fontSize / 2
|
|
|
+ break
|
|
|
+ case 'bottom':
|
|
|
+ y -= fontSize
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+ let to = x
|
|
|
+ switch (textAlign) {
|
|
|
+ case 'left':
|
|
|
+ x = x
|
|
|
+ to+= textWidth
|
|
|
+ break
|
|
|
+ case 'center':
|
|
|
+ x = x - textWidth / 2
|
|
|
+ to = x + textWidth
|
|
|
+ break
|
|
|
+ case 'right':
|
|
|
+ to = x
|
|
|
+ x = x - textWidth
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ if(td) {
|
|
|
+ ctx.setLineWidth(fontSize / 13);
|
|
|
+ ctx.beginPath();
|
|
|
+
|
|
|
+ if (/\bunderline\b/.test(td)) {
|
|
|
+ y -= inlinePaddingTop * 0.8
|
|
|
+ ctx.moveTo(x, y);
|
|
|
+ ctx.lineTo(to, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (/\boverline\b/.test(td)) {
|
|
|
+ y += inlinePaddingTop
|
|
|
+ ctx.moveTo(x, y - lineHeight);
|
|
|
+ ctx.lineTo(to, y - lineHeight);
|
|
|
+ }
|
|
|
+ if (/\bline-through\b/.test(td)) {
|
|
|
+ ctx.moveTo(x , y - lineHeight / 2 );
|
|
|
+ ctx.lineTo(to, y - lineHeight /2 );
|
|
|
+ }
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.setStrokeStyle(color);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const _reset = (text, x, y) => {
|
|
|
+ const rs = Object.keys(rulesObj)
|
|
|
+ for (let i = 0; i < rs.length; i++) {
|
|
|
+ const item = rulesObj[rs[i]]
|
|
|
+ // ctx.globalCompositeOperation = "destination-out";
|
|
|
+ ctx.save();
|
|
|
+ ctx.setFillStyle(rules.color);
|
|
|
+ if(item.char) {
|
|
|
+ ctx.fillText(item.char, item.x , item.y)
|
|
|
+ }
|
|
|
+ ctx.restore();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const _setText = (isReset, char) => {
|
|
|
+ if(isReset) {
|
|
|
+ const t1 = Math.round(this.measureText('\u0020', fontSize))
|
|
|
+ const t2 = Math.round(this.measureText('\u3000', fontSize))
|
|
|
+ const t3 = Math.round(this.measureText(char, fontSize))
|
|
|
+ let _char = ''
|
|
|
+ let _num = 1
|
|
|
+ if(t3 == t2){
|
|
|
+ _char ='\u3000'
|
|
|
+ _num = 1
|
|
|
+ } else {
|
|
|
+ _char = '\u0020'
|
|
|
+ _num = Math.ceil(t3 / t1)
|
|
|
+ }
|
|
|
+ return new Array(_num).fill(_char).join('')
|
|
|
+ } else {
|
|
|
+ return char
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const _setRulesObj = (text, index, x, y) => {
|
|
|
+ rulesObj[index].x = x
|
|
|
+ rulesObj[index].y = y
|
|
|
+ rulesObj[index].char = text
|
|
|
+ }
|
|
|
+ const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
|
|
|
+ // 不超过一行
|
|
|
+ if (textWidth + ol <= w && !text.includes('\n')) {
|
|
|
+ x = x + ol
|
|
|
+ const rs = Object.keys(rulesObj)
|
|
|
+ let _text = text.split('')
|
|
|
+ if(rs) {
|
|
|
+ for (let i = 0; i < rs.length; i++) {
|
|
|
+ const index = rs[i]
|
|
|
+ const t = _text[index]
|
|
|
+ let char = _setText(rulesObj[index], t)
|
|
|
+ _text[index] = char
|
|
|
+ _setRulesObj(t, index, x + this.measureText(text.substring(0, index), fontSize), y + inlinePaddingTop)
|
|
|
+ }
|
|
|
+ _reset()
|
|
|
+ }
|
|
|
+ ctx.fillText(_text.join(''), x, y + inlinePaddingTop)
|
|
|
+ y += lineHeight
|
|
|
+ _drawLine(x, y, textWidth)
|
|
|
+ ctx.restore()
|
|
|
+ this.setBorder(box, style)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 多行文本
|
|
|
+ const chars = text.split('')
|
|
|
+ const _y = y
|
|
|
+ let _x = x
|
|
|
+ // 逐行绘制
|
|
|
+ let line = ''
|
|
|
+ let lineIndex = 0
|
|
|
+
|
|
|
+ for(let index = 0 ; index <= chars.length; index++){
|
|
|
+ let ch = chars[index] || ''
|
|
|
+ const isLine = ch === '\n'
|
|
|
+ const isRight = ch == ''// index == chars.length
|
|
|
+ ch = isLine ? '' : ch;
|
|
|
+
|
|
|
+ let textline = line + _setText(rulesObj[index], ch)
|
|
|
+ let textWidth = this.measureText(textline, fontSize)
|
|
|
+
|
|
|
+
|
|
|
+ // 绘制行数大于最大行数,则直接跳出循环
|
|
|
+ if (lineIndex >= maxLines) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if(lineIndex == 0) {
|
|
|
+ textWidth = textWidth + ol
|
|
|
+ _x += ol
|
|
|
+ }
|
|
|
+ if(rulesObj[index]) {
|
|
|
+ _setRulesObj(ch, index, _x + this.measureText(line, fontSize), y + inlinePaddingTop)
|
|
|
+ }
|
|
|
+ if (textWidth > w || isLine || isRight) {
|
|
|
+ lineIndex++
|
|
|
+ line = isRight && textWidth <= w ? textline : line
|
|
|
+ if(lineIndex === maxLines && textWidth > w) {
|
|
|
+ while( this.measureText(`${line}...`, fontSize) > w) {
|
|
|
+ if (line.length <= 1) {
|
|
|
+ // 如果只有一个字符时,直接跳出循环
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ line = line.substring(0, line.length - 1);
|
|
|
+ if(rulesObj[index - 1]) {
|
|
|
+ rulesObj[index - 1].char = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ line += '...'
|
|
|
+ }
|
|
|
+ ctx.fillText(line, _x, y + inlinePaddingTop)
|
|
|
+ y += lineHeight
|
|
|
+ _drawLine(_x, y, textWidth)
|
|
|
+ line = ch
|
|
|
+ if ((y + lineHeight) > (_y + h)) break
|
|
|
+ } else {
|
|
|
+ line = textline
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const rs = Object.keys(rulesObj)
|
|
|
+ if(rs) {
|
|
|
+ _reset()
|
|
|
+ }
|
|
|
+ ctx.restore()
|
|
|
+ this.setBorder(box, style)
|
|
|
+ }
|
|
|
+ async findNode(element, parent = {}, index = 0, siblings = [], source) {
|
|
|
+ let computedStyle = Object.assign({}, this.getComputedStyle(element, parent, index));
|
|
|
+ let attributes = await this.getAttributes(element)
|
|
|
+ let node = {
|
|
|
+ id: id++,
|
|
|
+ parent,
|
|
|
+ computedStyle,
|
|
|
+ rules: element.rules,
|
|
|
+ attributes: Object.assign({}, attributes),
|
|
|
+ name: element?.type || 'view',
|
|
|
+ }
|
|
|
+ if(JSON.stringify(parent) === '{}' && !element.type) {
|
|
|
+ const {left = 0, top = 0, width = 0, height = 0 } = computedStyle
|
|
|
+ node.layoutBox = {left, top, width, height }
|
|
|
+ } else {
|
|
|
+ node.layoutBox = Object.assign({left: 0, top: 0}, this.getLayoutBox(node, parent, index, siblings, source))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (element?.views) {
|
|
|
+ let childrens = []
|
|
|
+ node.children = []
|
|
|
+ for (let i = 0; i < element.views.length; i++) {
|
|
|
+ let v = element.views[i]
|
|
|
+ childrens.push(await this.findNode(v, node, i, childrens, element))
|
|
|
+ }
|
|
|
+ node.children = childrens
|
|
|
+ }
|
|
|
+ return node
|
|
|
+ }
|
|
|
+ getComputedStyle(element, parent = {}, index = 0) {
|
|
|
+ const style = {}
|
|
|
+ const node = JSON.stringify(parent) == '{}' && !element.type ? element : element.css;
|
|
|
+ if(parent.computedStyle) {
|
|
|
+ for (let value of Object.keys(parent.computedStyle)){
|
|
|
+ const item = parent.computedStyle[value]
|
|
|
+ if(['color', 'fontSize', 'lineHeight', 'verticalAlign', 'fontWeight', 'textAlign'].includes(value)) {
|
|
|
+ style[value] = /em|px$/.test(item) ? toPx(item, node?.fontSize) : item
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if(!node) return style
|
|
|
+ for (let value of Object.keys(node)) {
|
|
|
+ const item = node[value]
|
|
|
+ if(value == 'views') {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (['boxShadow', 'shadow'].includes(value)) {
|
|
|
+ let shadows = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
|
|
|
+ style.boxShadow = shadows
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (value.includes('border') && !value.includes('adius')) {
|
|
|
+ const prefix = value.match(/^border([BTRLa-z]+)?/)[0]
|
|
|
+ const type = value.match(/[W|S|C][a-z]+/)
|
|
|
+ let v = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
|
|
|
+
|
|
|
+ if(v.length > 1) {
|
|
|
+ style[prefix] = {
|
|
|
+ [prefix + 'Width'] : v[0] || 1,
|
|
|
+ [prefix + 'Style'] : v[1] || 'solid',
|
|
|
+ [prefix + 'Color'] : v[2] || 'black'
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ style[prefix] = {
|
|
|
+ [prefix + 'Width'] : 1,
|
|
|
+ [prefix + 'Style'] : 'solid',
|
|
|
+ [prefix + 'Color'] : 'black'
|
|
|
+ }
|
|
|
+ style[prefix][prefix + type[0]] = v[0]
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (['background', 'backgroundColor'].includes(value)) {
|
|
|
+ style['backgroundColor'] = item
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if(value.includes('padding') || value.includes('margin') || value.includes('adius')) {
|
|
|
+ let isRadius = value.includes('adius')
|
|
|
+ let prefix = isRadius ? 'borderRadius' : value.match(/[a-z]+/)[0]
|
|
|
+ let pre = [0,0,0,0].map((item, i) => isRadius ? ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'][i] : [prefix + 'Top', prefix + 'Right', prefix + 'Bottom', prefix + 'Left'][i] )
|
|
|
+ if(value === 'padding' || value === 'margin' || value === 'radius' || value === 'borderRadius') {
|
|
|
+ let v = item?.split(' ').map((item) => /^\d/.test(item) && toPx(item, node['width']), []) ||[0];
|
|
|
+ let type = isRadius ? 'borderRadius' : value;
|
|
|
+ if(v.length == 1) {
|
|
|
+ style[type] = v[0]
|
|
|
+ } else {
|
|
|
+ let [t, r, b, l] = v
|
|
|
+ style[type] = {
|
|
|
+ [pre[0]]: t,
|
|
|
+ [pre[1]]: isNumber(r) ? r : t,
|
|
|
+ [pre[2]]: isNumber(b) ? b : t,
|
|
|
+ [pre[3]]: isNumber(l) ? l : r
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if(typeof style[prefix] === 'object') {
|
|
|
+ style[prefix][value] = toPx(item, node['width'])
|
|
|
+ } else {
|
|
|
+ style[prefix] = {
|
|
|
+ [pre[0]]: style[prefix] || 0,
|
|
|
+ [pre[1]]: style[prefix] || 0,
|
|
|
+ [pre[2]]: style[prefix] || 0,
|
|
|
+ [pre[3]]: style[prefix] || 0
|
|
|
+ }
|
|
|
+ style[prefix][value] = toPx(item, node['width'])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if(value.includes('width') || value.includes('height')) {
|
|
|
+ if(/%$/.test(item)) {
|
|
|
+ style[value] = toPx(item, parent.layoutBox[value])
|
|
|
+ } else {
|
|
|
+ style[value] = /px|rpx$/.test(item) ? toPx(item) : item
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if(value.includes('transform')) {
|
|
|
+ style[value]= {}
|
|
|
+ item.replace(/([a-zA-Z]+)\(([0-9,-\.%rpxdeg\s]+)\)/g, (g1, g2, g3) => {
|
|
|
+ const v = g3.split(',').map(k => k.replace(/(^\s*)|(\s*$)/g,''))
|
|
|
+ const transform = (v, r) => {
|
|
|
+ return v.includes('deg') ? v * 1 : toPx(v, r)
|
|
|
+ }
|
|
|
+ if(g2.includes('matrix')) {
|
|
|
+ style[value][g2] = v.map(v => v * 1)
|
|
|
+ } else if(g2.includes('rotate')) {
|
|
|
+ style[value][g2] = g3.match(/\d+/)[0] * 1
|
|
|
+ }else if(/[X, Y]/.test(g2)) {
|
|
|
+ style[value][g2] = /[X]/.test(g2) ? transform(v[0], node['width']) : transform(v[0], node['height'])
|
|
|
+ } else {
|
|
|
+ style[value][g2+'X'] = transform(v[0], node['width'])
|
|
|
+ style[value][g2+'Y'] = transform(v[1] || v[0], node['height'])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if(/em$/.test(item) && !value.includes('lineHeight')) {
|
|
|
+ style[value] = Math.ceil(parseFloat(item.replace('em')) * toPx(node['fontSize'] || 14))
|
|
|
+ } else {
|
|
|
+ style[value] = /%|px|rpx$/.test(item) ? toPx(item, node['width']) : item
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if((element.name == 'image' || element.type == 'image') && !style.mode) {
|
|
|
+ style.mode = 'aspectFill'
|
|
|
+ if((!node.width || node.width == 'auto') && (!node.height || node.width == 'auto') ) {
|
|
|
+ style.mode = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return style
|
|
|
+ }
|
|
|
+ getLayoutBox(element, parent = {}, index = 0, siblings = [], source = {}) {
|
|
|
+ let box = {}
|
|
|
+ let {name, computedStyle: cstyle, layoutBox, attributes} = element || {}
|
|
|
+ if(!name) return box
|
|
|
+ const {ctx} = this
|
|
|
+ const pbox = parent.layoutBox || this.root
|
|
|
+ const pstyle = parent.computedStyle
|
|
|
+ let {
|
|
|
+ verticalAlign: v,
|
|
|
+ left: x,
|
|
|
+ top: y,
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ fontSize = 14,
|
|
|
+ lineHeight = '1.4em',
|
|
|
+ maxLines,
|
|
|
+ fontWeight,
|
|
|
+ fontFamily,
|
|
|
+ textStyle,
|
|
|
+ position,
|
|
|
+ display
|
|
|
+ } = cstyle;
|
|
|
+
|
|
|
+ const { paddingTop: pt = 0, paddingRight: pr = 0, paddingBottom: pb = 0, paddingLeft: pl = 0, } = cstyle.padding || {}
|
|
|
+ const { marginTop: mt = 0, marginRight: mr = 0, marginBottom: mb = 0, marginLeft: ml = 0, } = cstyle.margin || {}
|
|
|
+
|
|
|
+ if(position == 'relative') {
|
|
|
+ x += pbox.left
|
|
|
+ y += pbox.top
|
|
|
+ }
|
|
|
+ if(name === 'text') {
|
|
|
+ const text = attributes.text ||''
|
|
|
+ lineHeight = toPx(lineHeight, fontSize)
|
|
|
+ ctx.save()
|
|
|
+ this.setFont({fontFamily, fontSize, fontWeight, textStyle})
|
|
|
+ const {layoutBox: lbox, computedStyle: ls} = siblings[index - 1] || {}
|
|
|
+ const {layoutBox: rbox, computedStyle: rs} = siblings[index + 1] || {}
|
|
|
+ const isLeft = index == 0
|
|
|
+ const isblock = display === 'block' || ls?.display === 'block'
|
|
|
+ const isOnly = isLeft && !rbox || !parent?.id
|
|
|
+ const lboxR = isLeft || isblock ? 0 : lbox.offsetRight || 0
|
|
|
+ let texts = text.split('\n')
|
|
|
+ let lineIndex = 1
|
|
|
+ let line = ''
|
|
|
+ const textIndent = cstyle.textIndent || 0
|
|
|
+ if(!isOnly) {
|
|
|
+ texts.map((t, i) => {
|
|
|
+ lineIndex += i
|
|
|
+ const chars = t.split('')
|
|
|
+ for (let j = 0; j < chars.length; j++) {
|
|
|
+ let ch = chars[j]
|
|
|
+ let textline = line + ch
|
|
|
+ let textWidth = this.measureText(textline, fontSize)
|
|
|
+ if(lineIndex == 1) {
|
|
|
+ textWidth = textWidth + (isblock ? 0 : lboxR) + textIndent
|
|
|
+ }
|
|
|
+ if(textWidth > pbox.width) {
|
|
|
+ lineIndex++
|
|
|
+ line = ch
|
|
|
+ } else {
|
|
|
+ line = textline
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ line = text
|
|
|
+ lineIndex = Math.max(texts.length, Math.ceil(this.measureText(text, fontSize) / ((w || pbox.width) - this.measureText('0', fontSize))))
|
|
|
+ }
|
|
|
+ box.offsetLeft = (isNumber(x) || isblock || isOnly ? textIndent : lboxR) + pl + ml;
|
|
|
+ // 剩下的字宽度
|
|
|
+ const remain = (this.measureText(line, fontSize))
|
|
|
+ let width = lineIndex > 1 ? pbox.width : remain + box.offsetLeft;
|
|
|
+ box.offsetRight = box.offsetLeft + (w ? w : (isblock ? pbox.width : remain)) + pr + mr;
|
|
|
+
|
|
|
+
|
|
|
+ const _getLeft = () => {
|
|
|
+ return (x || pbox.left)
|
|
|
+ }
|
|
|
+ const _getWidth = () => {
|
|
|
+ return w || (isOnly ? pbox.width : (width > pbox.width - box.left || lineIndex > 1 ? pbox.width - box.left : width))
|
|
|
+ }
|
|
|
+ const _getHeight = () => {
|
|
|
+ if(h) {
|
|
|
+ return h
|
|
|
+ } else if(lineIndex > 1 ) {
|
|
|
+ return (maxLines || lineIndex) * lineHeight + pt + pb + mt + mb
|
|
|
+ } else {
|
|
|
+ return lineHeight + pt + pb + mt + mb
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const _getTop = () => {
|
|
|
+ let _y = y
|
|
|
+ if(_y) {
|
|
|
+ return _y + pt + mt
|
|
|
+ }
|
|
|
+ if(isLeft) {
|
|
|
+ _y = pbox.top
|
|
|
+ } else if(lbox.width < pbox.width) {
|
|
|
+ _y = lbox.top
|
|
|
+ } else {
|
|
|
+ _y = lbox.top + lbox.height - (ls?.lineHeight || 0)
|
|
|
+ }
|
|
|
+ return _y + pt + mt + (isblock && ls?.lineHeight || 0 )
|
|
|
+ }
|
|
|
+ box.left = _getLeft()
|
|
|
+ box.width = _getWidth()
|
|
|
+ box.height = _getHeight()
|
|
|
+ box.top = _getTop()
|
|
|
+ ctx.restore()
|
|
|
+ } else if(['view', 'qrcode'].includes(name)) {
|
|
|
+ box.left = x || pbox.left
|
|
|
+ box.width = (w || pbox?.width) - pl - pr
|
|
|
+ box.height = h || 0
|
|
|
+ box.top = y || pbox.top
|
|
|
+ } else if(name === 'image') {
|
|
|
+ box.left = x || pbox.left
|
|
|
+ box.width = (w || pbox?.width) - pl - pr
|
|
|
+ const {
|
|
|
+ width: rWidth,
|
|
|
+ height: rHeight
|
|
|
+ } = attributes
|
|
|
+ box.height = h || box.width * rHeight / rWidth
|
|
|
+ box.top = y || pbox.top
|
|
|
+ }
|
|
|
+ return box
|
|
|
+ }
|
|
|
+ async getAttributes(element) {
|
|
|
+ let arr = { }
|
|
|
+ if(element?.url || element?.src) {
|
|
|
+ arr.src = element.url || element?.src;
|
|
|
+ const {width = 0, height = 0, path: src} = await getImageInfo(arr.src, this.isH5PathToBase64) || {}
|
|
|
+ arr = Object.assign({}, arr, {width, height, src})
|
|
|
+ }
|
|
|
+ if(element?.text) {
|
|
|
+ arr.text = element.text
|
|
|
+ }
|
|
|
+ return arr
|
|
|
+ }
|
|
|
+ async drawBoard(element) {
|
|
|
+ const node = await this.findNode(element)
|
|
|
+ return this.drawNode(node)
|
|
|
+ }
|
|
|
+ async drawNode(element) {
|
|
|
+ const {
|
|
|
+ layoutBox,
|
|
|
+ computedStyle,
|
|
|
+ name,
|
|
|
+ rules
|
|
|
+ } = element
|
|
|
+ const {
|
|
|
+ src,
|
|
|
+ text
|
|
|
+ } = element.attributes
|
|
|
+ if (name === 'view') {
|
|
|
+ this.drawView(layoutBox, computedStyle)
|
|
|
+ } else if (name === 'image' && src) {
|
|
|
+ await this.drawImage(element.attributes, layoutBox, computedStyle, false)
|
|
|
+ } else if (name === 'text') {
|
|
|
+ this.drawText(text, layoutBox, computedStyle, rules)
|
|
|
+ } else if (name === 'qrcode') {
|
|
|
+ QR.api.draw(text, this, layoutBox, computedStyle)
|
|
|
+ }
|
|
|
+ if (!element.children) return
|
|
|
+ const childs = Object.values ? Object.values(element.children) : Object.keys(element.children).map((key) => element.children[key]);
|
|
|
+ for (const child of childs) {
|
|
|
+ await this.drawNode(child)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|