draw.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. class Draw {
  2. constructor(context, canvas, use2dCanvas = false) {
  3. this.ctx = context
  4. this.canvas = canvas || null
  5. this.use2dCanvas = use2dCanvas
  6. }
  7. roundRect(x, y, w, h, r, fill = true, stroke = false) {
  8. if (r < 0) return
  9. const ctx = this.ctx
  10. ctx.beginPath()
  11. ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
  12. ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
  13. ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
  14. ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
  15. ctx.lineTo(x, y + r)
  16. if (stroke) ctx.stroke()
  17. if (fill) ctx.fill()
  18. }
  19. drawView(box, style) {
  20. const ctx = this.ctx
  21. const {
  22. left: x, top: y, width: w, height: h
  23. } = box
  24. const {
  25. borderRadius = 0,
  26. borderWidth = 0,
  27. borderColor,
  28. color = '#000',
  29. backgroundColor = 'transparent',
  30. } = style
  31. ctx.save()
  32. // 外环
  33. if (borderWidth > 0) {
  34. ctx.fillStyle = borderColor || color
  35. this.roundRect(x, y, w, h, borderRadius)
  36. }
  37. // 内环
  38. ctx.fillStyle = backgroundColor
  39. const innerWidth = w - 2 * borderWidth
  40. const innerHeight = h - 2 * borderWidth
  41. const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
  42. this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
  43. ctx.restore()
  44. }
  45. async drawImage(img, box, style) {
  46. await new Promise((resolve, reject) => {
  47. const ctx = this.ctx
  48. const canvas = this.canvas
  49. const {
  50. borderRadius = 0
  51. } = style
  52. const {
  53. left: x, top: y, width: w, height: h
  54. } = box
  55. ctx.save()
  56. this.roundRect(x, y, w, h, borderRadius, false, false)
  57. ctx.clip()
  58. const _drawImage = (img) => {
  59. if (this.use2dCanvas) {
  60. const Image = canvas.createImage()
  61. Image.onload = () => {
  62. ctx.drawImage(Image, x, y, w, h)
  63. ctx.restore()
  64. resolve()
  65. }
  66. Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
  67. Image.src = img
  68. } else {
  69. ctx.drawImage(img, x, y, w, h)
  70. ctx.restore()
  71. resolve()
  72. }
  73. }
  74. const isTempFile = /^wxfile:\/\//.test(img)
  75. const isNetworkFile = /^https?:\/\//.test(img)
  76. if (isTempFile) {
  77. _drawImage(img)
  78. } else if (isNetworkFile) {
  79. wx.downloadFile({
  80. url: img,
  81. success(res) {
  82. if (res.statusCode === 200) {
  83. _drawImage(res.tempFilePath)
  84. } else {
  85. reject(new Error(`downloadFile:fail ${img}`))
  86. }
  87. },
  88. fail() {
  89. reject(new Error(`downloadFile:fail ${img}`))
  90. }
  91. })
  92. } else {
  93. reject(new Error(`image format error: ${img}`))
  94. }
  95. })
  96. }
  97. // eslint-disable-next-line complexity
  98. drawText(text, box, style) {
  99. const ctx = this.ctx
  100. let {
  101. left: x, top: y, width: w, height: h
  102. } = box
  103. let {
  104. color = '#000',
  105. lineHeight = '1.4em',
  106. fontSize = 14,
  107. textAlign = 'left',
  108. verticalAlign = 'top',
  109. backgroundColor = 'transparent'
  110. } = style
  111. if (typeof lineHeight === 'string') { // 2em
  112. lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
  113. }
  114. if (!text || (lineHeight > h)) return
  115. ctx.save()
  116. ctx.textBaseline = 'top'
  117. ctx.font = `${fontSize}px sans-serif`
  118. ctx.textAlign = textAlign
  119. // 背景色
  120. ctx.fillStyle = backgroundColor
  121. this.roundRect(x, y, w, h, 0)
  122. // 文字颜色
  123. ctx.fillStyle = color
  124. // 水平布局
  125. switch (textAlign) {
  126. case 'left':
  127. break
  128. case 'center':
  129. x += 0.5 * w
  130. break
  131. case 'right':
  132. x += w
  133. break
  134. default: break
  135. }
  136. const textWidth = ctx.measureText(text).width
  137. const actualHeight = Math.ceil(textWidth / w) * lineHeight
  138. let paddingTop = Math.ceil((h - actualHeight) / 2)
  139. if (paddingTop < 0) paddingTop = 0
  140. // 垂直布局
  141. switch (verticalAlign) {
  142. case 'top':
  143. break
  144. case 'middle':
  145. y += paddingTop
  146. break
  147. case 'bottom':
  148. y += 2 * paddingTop
  149. break
  150. default: break
  151. }
  152. const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
  153. // 不超过一行
  154. if (textWidth <= w) {
  155. ctx.fillText(text, x, y + inlinePaddingTop)
  156. return
  157. }
  158. // 多行文本
  159. const chars = text.split('')
  160. const _y = y
  161. // 逐行绘制
  162. let line = ''
  163. for (const ch of chars) {
  164. const testLine = line + ch
  165. const testWidth = ctx.measureText(testLine).width
  166. if (testWidth > w) {
  167. ctx.fillText(line, x, y + inlinePaddingTop)
  168. y += lineHeight
  169. line = ch
  170. if ((y + lineHeight) > (_y + h)) break
  171. } else {
  172. line = testLine
  173. }
  174. }
  175. // 避免溢出
  176. if ((y + lineHeight) <= (_y + h)) {
  177. ctx.fillText(line, x, y + inlinePaddingTop)
  178. }
  179. ctx.restore()
  180. }
  181. async drawNode(element) {
  182. const {layoutBox, computedStyle, name} = element
  183. const {src, text} = element.attributes
  184. if (name === 'view') {
  185. this.drawView(layoutBox, computedStyle)
  186. } else if (name === 'image') {
  187. await this.drawImage(src, layoutBox, computedStyle)
  188. } else if (name === 'text') {
  189. this.drawText(text, layoutBox, computedStyle)
  190. }
  191. const childs = Object.values(element.children)
  192. for (const child of childs) {
  193. await this.drawNode(child)
  194. }
  195. }
  196. }
  197. module.exports = {
  198. Draw
  199. }