You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
5.5 KiB
JavaScript
226 lines
5.5 KiB
JavaScript
|
2 years ago
|
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
|
||
|
|
}
|