| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 | class Draw {  constructor(context, canvas, use2dCanvas = false) {    this.ctx = context    this.canvas = canvas || null    this.use2dCanvas = use2dCanvas  }  roundRect(x, y, w, h, r, fill = true, stroke = false) {    if (r < 0) return    const ctx = this.ctx    ctx.beginPath()    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)    ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)    ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)    ctx.lineTo(x, y + r)    if (stroke) ctx.stroke()    if (fill) ctx.fill()  }  drawView(box, style) {    const ctx = this.ctx    const {      left: x, top: y, width: w, height: h    } = box    const {      borderRadius = 0,      borderWidth = 0,      borderColor,      color = '#000',      backgroundColor = 'transparent',    } = style    ctx.save()    // 外环    if (borderWidth > 0) {      ctx.fillStyle = borderColor || color      this.roundRect(x, y, w, h, borderRadius)    }    // 内环    ctx.fillStyle = backgroundColor    const innerWidth = w - 2 * borderWidth    const innerHeight = h - 2 * borderWidth    const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0    this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)    ctx.restore()  }  async drawImage(img, box, style) {    await new Promise((resolve, reject) => {      const ctx = this.ctx      const canvas = this.canvas      const {        borderRadius = 0      } = style      const {        left: x, top: y, width: w, height: h      } = box      ctx.save()      this.roundRect(x, y, w, h, borderRadius, false, false)      ctx.clip()      const _drawImage = (img) => {        if (this.use2dCanvas) {          const Image = canvas.createImage()          Image.onload = () => {            ctx.drawImage(Image, x, y, w, h)            ctx.restore()            resolve()          }          Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }          Image.src = img        } else {          ctx.drawImage(img, x, y, w, h)          ctx.restore()          resolve()        }      }      const isTempFile = /^wxfile:\/\//.test(img)      const isNetworkFile = /^https?:\/\//.test(img)      if (isTempFile) {        _drawImage(img)      } else if (isNetworkFile) {        wx.downloadFile({          url: img,          success(res) {            if (res.statusCode === 200) {              _drawImage(res.tempFilePath)            } else {              reject(new Error(`downloadFile:fail ${img}`))            }          },          fail() {            reject(new Error(`downloadFile:fail ${img}`))          }        })      } else {        reject(new Error(`image format error: ${img}`))      }    })  }  // eslint-disable-next-line complexity  drawText(text, box, style) {    const ctx = this.ctx    let {      left: x, top: y, width: w, height: h    } = box    let {      color = '#000',      lineHeight = '1.4em',      fontSize = 14,      textAlign = 'left',      verticalAlign = 'top',      backgroundColor = 'transparent'    } = style    if (typeof lineHeight === 'string') { // 2em      lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)    }    if (!text || (lineHeight > h)) return    ctx.save()    ctx.textBaseline = 'top'    ctx.font = `${fontSize}px sans-serif`    ctx.textAlign = textAlign    // 背景色    ctx.fillStyle = backgroundColor    this.roundRect(x, y, w, h, 0)    // 文字颜色    ctx.fillStyle = color    // 水平布局    switch (textAlign) {      case 'left':        break      case 'center':        x += 0.5 * w        break      case 'right':        x += w        break      default: break    }    const textWidth = ctx.measureText(text).width    const actualHeight = Math.ceil(textWidth / w) * lineHeight    let paddingTop = Math.ceil((h - actualHeight) / 2)    if (paddingTop < 0) paddingTop = 0    // 垂直布局    switch (verticalAlign) {      case 'top':        break      case 'middle':        y += paddingTop        break      case 'bottom':        y += 2 * paddingTop        break      default: break    }    const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)    // 不超过一行    if (textWidth <= w) {      ctx.fillText(text, x, y + inlinePaddingTop)      return    }    // 多行文本    const chars = text.split('')    const _y = y    // 逐行绘制    let line = ''    for (const ch of chars) {      const testLine = line + ch      const testWidth = ctx.measureText(testLine).width      if (testWidth > w) {        ctx.fillText(line, x, y + inlinePaddingTop)        y += lineHeight        line = ch        if ((y + lineHeight) > (_y + h)) break      } else {        line = testLine      }    }    // 避免溢出    if ((y + lineHeight) <= (_y + h)) {      ctx.fillText(line, x, y + inlinePaddingTop)    }    ctx.restore()  }  async drawNode(element) {    const {layoutBox, computedStyle, name} = element    const {src, text} = element.attributes    if (name === 'view') {      this.drawView(layoutBox, computedStyle)    } else if (name === 'image') {      await this.drawImage(src, layoutBox, computedStyle)    } else if (name === 'text') {      this.drawText(text, layoutBox, computedStyle)    }    const childs = Object.values(element.children)    for (const child of childs) {      await this.drawNode(child)    }  }}module.exports = {  Draw}
 |