first commit

This commit is contained in:
编码猿
2024-09-27 02:06:13 +08:00
commit 852d94fbb9
36760 changed files with 3274413 additions and 0 deletions

View File

@@ -0,0 +1,670 @@
import { toPx, CHAR_WIDTH_SCALE_MAP, base64src } from './utils'
import { GD } from './gradient'
let id = 0
export class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = false, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
if(!r) {
ctx.rect(x, y, w, h)
} 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.closePath()
}
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
measureText(text, fontSize) {
const ctx = this.ctx
// #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, fontSize, fontWeight, textStyle}) {
let ctx = this.ctx
// 设置属性
// #ifndef MP-TOUTIAO
fontWeight = fontWeight === 'bold' ? 'bold' : 'normal'
textStyle = textStyle === 'italic' ? 'italic' : 'normal'
// #endif
// #ifdef MP-TOUTIAO
fontWeight = fontWeight === 'bold' ? 'bold' : ''
textStyle = textStyle === 'italic' ? 'italic' : ''
// #endif
fontSize = toPx(fontSize)
ctx.font = `${textStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
}
dradwBackground(bd, w, h) {
const ctx = this.ctx
if (!bd) {
// #ifndef MP-TOUTIAO
ctx.setFillStyle('transparent')
// #endif
// #ifdef MP-TOUTIAO
ctx.setFillStyle('rgba(0,0,0,0)')
// #endif
// } else if(bd.startsWith('#') || bd.startsWith('rgba') || bd.toLowerCase() === 'transparent' ) {
// ctx.setFillStyle(bd)
} else if(GD.isGradient(bd)) {
GD.doGradient(bd, w, h, ctx);
} else {
ctx.setFillStyle(bd)
}
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x,
top: y,
width: w,
height: h
} = box
let {
boxShadow = [],
borderRadius = 0,
borderWidth = 0,
borderStyle,
borderColor,
color = '#000000',
backgroundColor: bg,
rotate,
shadow
} = style
ctx.save()
// 旋转
if (rotate) {
ctx.translate(x + w / 2, y + h / 2)
ctx.rotate(rotate * Math.PI / 180)
ctx.translate(-x - w / 2, -y - h / 2)
}
// 投影
if (boxShadow.length) {
const [x, y, b, c] = boxShadow
ctx.setShadow(x, y, b, c)
}
// 描边
if (borderStyle) {
ctx.lineWidth = borderWidth
if (borderStyle == 'dashed') {
ctx.setLineDash([Math.ceil(borderWidth * 4 / 3), Math.ceil(borderWidth * 4 / 3)])
} else if (borderStyle == 'dotted') {
ctx.setLineDash([borderWidth, borderWidth])
}
ctx.setStrokeStyle(borderColor)
}
// 背景
this.dradwBackground(bg, w, h)
this.roundRect(x, y, w, h, borderRadius, true, borderColor ? true : false)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise(async (resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0,
mode,
backgroundColor: bg
} = style
const {
left: x,
top: y,
width: w,
height: h
} = box
ctx.save()
// 背景
this.dradwBackground(bg || 'white', w, h)
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.path, x, y, w, h);
} else {
ctx.drawImage(img.path, startX, startY, rWidth, rHeight, x, y, w, h)
}
}
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
_modeImage(img)
ctx.restore()
setTimeout(()=> resolve(), 100)
}
Image.onerror = () => {
reject(new Error(`createImage fail: ${img}`))
}
Image.src = img
} else {
_modeImage(img)
ctx.restore()
setTimeout(()=> resolve(), 100)
}
}
// #ifdef MP
if(/^data:image\/(\w+);base64/.test(img)) {
img = await base64src(img)
}
// #endif
uni.getImageInfo({
src: img,
success: (image) => {
image.path = /^(http|\/\/|\/|wxfile|data:image\/(\w+);base64|file|bdfile|ttfile)/.test(image.path) ? image.path : `/${image.path}`;
console.log('image:::', image)
_drawImage(image)
},
fail(err) {
if(/^\.|^\/(?=[^\/])/.test(img) || /^data:image\/(\w+);base64/.test(img)) {
_drawImage({path: img})
} else {
console.error(`getImageInfo:fail ${img} failed ${JSON.stringify(err)}`);
reject(new Error(`getImageInfo:fail ${img} ${JSON.stringify(err)}`));
}
}
})
})
}
drawLine(x, y, x2, y2, color, type) {
const ctx = this.ctx
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.setStrokeStyle(color);
ctx.stroke();
}
// 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 = '#000000',
lineHeight = '1.4em',
fontSize = 14,
fontWeight,
fontFamily = 'sans-serif',
textStyle,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor: bg,
maxLines,
textDecoration
} = style
if (typeof lineHeight === 'string') { // 1.4em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
// || (lineHeight > h)
if (!text) return
ctx.save()
ctx.setTextBaseline(verticalAlign)
// 设置属性
this.setFont({fontFamily, fontSize, fontWeight, textStyle})
ctx.setTextAlign(textAlign)
// 背景色
this.dradwBackground(bg, w, h)
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.setFillStyle(color)
// 水平布局
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 (verticalAlign) {
case 'top':
break
case 'middle':
y += fontSize / 2 // paddingTop
break
case 'bottom':
y += fontSize // 2 * paddingTop
break
default:
break
}
// 绘线
const _drawLine = (x, y, textWidth) => {
const { system } = uni.getSystemInfoSync()
if(/win|mac/.test(system)){
y += (fontSize / 3)
}
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(textDecoration) {
ctx.setLineWidth(fontSize / 13);
ctx.beginPath();
if (/\bunderline\b/.test(textDecoration)) {
y -= inlinePaddingTop * 0.8
ctx.moveTo(x, y);
ctx.lineTo(to, y);
}
if (/\boverline\b/.test(textDecoration)) {
y += inlinePaddingTop
ctx.moveTo(x, y - lineHeight);
ctx.lineTo(to, y - lineHeight);
}
if (/\bline-through\b/.test(textDecoration)) {
ctx.moveTo(x , y - lineHeight / 2.5 );
ctx.lineTo(to, y - lineHeight /2.5 );
}
ctx.closePath();
ctx.setStrokeStyle(color);
ctx.stroke();
}
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w && !text.includes('\n')) {
ctx.fillText(text, x, y + inlinePaddingTop)
_drawLine(x, y, textWidth)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
let lineIndex = 0
let textArray = []
for(let index = 0 ; index <= chars.length; index++){
let ch = chars[index]
// for (let ch of chars) {
const isLine = ch === '\n'
const isRight = index === chars.length - 1
ch = isLine ? '' : ch
let testLine = line + ch
const testWidth = this.measureText(testLine, fontSize) //ctx.measureText(testLine).width
// 绘制行数大于最大行数,则直接跳出循环
if (lineIndex >= maxLines) {
break;
}
if (testWidth > w || isLine || isRight) {
lineIndex++
line = isRight ? testLine : line
if(lineIndex === maxLines) {
// ctx.measureText(`${line}...`).width
while( this.measureText(`${line}...`, fontSize) > w) {
if (line.length <= 1) {
// 如果只有一个字符时,直接跳出循环
break;
}
line = line.substring(0, line.length - 1);
}
line += '...'
}
textArray.push(line)
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
_drawLine(x, y, testWidth)
line = ch
if ((y + lineHeight) > (_y + h)) break
// if ((y + lineHeight) > h) break
} else {
line = testLine
}
}
// 避免溢出
// if ((y + lineHeight) <= (_y + h)) {
// ctx.fillText(line, x, y + inlinePaddingTop)
// }
ctx.restore()
}
findNode(element, parent = {}, index = 0, siblings = [], source) {
let computedStyle = Object.assign({}, this.getComputedStyle(element, parent, index));
let node = {
id: id++,
parent,
computedStyle,
attributes: Object.assign({}, this.getAttributes(element)),
name: element?.type || 'view',
}
if(JSON.stringify(parent) === '{}') {
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 = []
element.views.forEach((v, i) => {
childrens.push(this.findNode(v, node, i, childrens, element))
})
node.children = childrens
}
return node
}
getComputedStyle(element, parent = {}, index = 0) {
const style = {}
if(parent.computedStyle) {
for (let value of Object.keys(parent.computedStyle)){
const item = parent.computedStyle[value]
if(['color', 'fontSize', 'lineHeight', 'verticalAlign', 'fontWeight'].includes(value)) {
style[value] = /px$/.test(item) ? toPx(item) : item
}
}
}
const node = element?.css ? element.css : element;
if(!node) return style
for (let value of Object.keys(node)) {
const item = node[value]
if (['boxShadow', 'shadow'].includes(value)) {
let shadows = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
style.boxShadow = shadows
}
if (value == 'border') {
let border = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
style.borderWidth = border[0]
style.borderStyle = border[1]
style.borderColor = border[2]
}
if (['background', 'backgroundColor'].includes(value)) {
style['backgroundColor'] = item
}
// 圆角
if (value.includes('adius')) {
if(value == 'radius') {
let radius = item?.split(' ').map((item) => /^\d/.test(item) && toPx(item, style['width']), []) ||[0];
if (radius.length == 1) {
style.borderRadius = radius[0]
} else {
let [tl, tr, br, bl] = radius
style.borderRadius = {
borderTopLeftRadius: tl,
borderTopRightRadius: tr || tl,
borderBottomRightRadius: br || tl,
borderBottomLeftRadius: tr
}
}
} else {
if(typeof style.borderRadius === 'object') {
style.borderRadius[value] = toPx(item, style['width'])
}else {
style.borderRadius = {
borderTopLeftRadius: style.borderRadius || 0,
borderTopRightRadius: style.borderRadius || 0,
borderBottomRightRadius: style.borderRadius || 0,
borderBottomLeftRadius: style.borderRadius ||0
}
style.borderRadius[value] = toPx(item, style['width'])
}
}
} else if(value == 'views') {
} else {
style[value] = /%|px|rpx$/.test(item) ? toPx(item) : item
}
}
return style
}
getLayoutBox(element, parent = {}, index = 0, siblings = [], source = {}) {
let box = {}
let {name, computedStyle, layoutBox, attributes} = element || {}
if(!name) return box
const isText = name === 'text'
const isParentText = parent.name ==='text';
const ctx = this.ctx
// 获取left
const getNodeLeft = () => {
if(typeof computedStyle.left === 'number') {
return computedStyle.left
}
// 如果是块元素
if(!isText) {
return parent.layoutBox?.left || 0
}
// 如果是第1个元素
const isLeft = index == 0
if(isLeft) {
// 如果父级是文本
if(isParentText) {
return (parent?.layoutBox?.left || 0) + (parent?.layoutBox?.width || 0)
} else {
return (parent?.layoutBox?.left || 0)
}
} else {
const leftNode = siblings[index - 1]
return leftNode.layoutBox.left + leftNode.layoutBox.width
}
}
// 获取宽度
const getNodeWidth = () => {
if(typeof computedStyle.width === 'number') {
return computedStyle.width
}
if(!isText) {
return parent?.layoutBox?.width
}
if(isText) {
let {
fontSize = 14,
lineHeight = '1.4em',
fontWeight,
fontFamily = 'sans-serif',
textStyle
} = computedStyle || {}
this.setFont({fontFamily, fontSize, fontWeight, textStyle})
let width = this.measureText(attributes.text, fontSize)
if(!isParentText) {
if(width < (parent?.layoutBox?.width || 0)) {
return width
} else {
return parent?.layoutBox?.width || 0
}
} else {
const res = this.getParent(parent, 'view')
const maxWidth = (res.layoutBox.width + res.layoutBox.left) - box.left
return maxWidth > width ? width : maxWidth
}
}
}
// 获取高度
const getNodeHeight = () => {
if(computedStyle.height) {
return computedStyle.height
}
if(!isText) {
return 0
}
// 如果父级有高度
if(parent.layoutBox.height == parent.computedStyle.height && parent.computedStyle.height != 0) {
return parent.layoutBox.height
}
// 如果父级没有高度
if(!parent.computedStyle.height) {
let {
fontSize = 14,
lineHeight = '1.4em',
} = computedStyle || {}
if (typeof lineHeight === 'string') { // 1.4em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
parent.layoutBox.height = parent.layoutBox.height > lineHeight ? parent.layoutBox.height : lineHeight
return lineHeight
}
// const res = this.getParent(parent, 'view')
// const maxHeight = res.layoutBox.height + res.layoutBox.top - box.top
// return maxHeight
}
// 获取top
const getNodeTop = () => {
const { verticalAlign } = computedStyle
if(computedStyle.top) {
return computedStyle.top
}
if(verticalAlign === 'bottom') {
return parent?.layoutBox?.top + (parent?.layoutBox?.height - box.height || 0)
}
if(verticalAlign === 'middle') {
return parent?.layoutBox?.top + (parent?.layoutBox?.height - box.height || 0) / 2
}
return parent?.layoutBox?.top || 0
}
box.left = getNodeLeft()
ctx.save()
box.width = getNodeWidth()
box.height = getNodeHeight()
// 获取top
box.top = getNodeTop()
ctx.restore()
// for (let value of Object.keys(node)) {
// const item = node[value]
// if(['left', 'right', 'top', 'bottom', 'width', 'height'].includes(value)) {
// box[value] = toPx(item) + (parent?.layoutBox[value] || 0)
// }
// }
return box
}
getParent(element, name) {
if(element.name === name) {
return element
} else if(element.parent){
return this.getParent(element.parent, name)
}
}
getAttributes(element) {
let arr = { }
if(element?.url) {
arr.src = element.url
}
if(element?.text) {
arr.text = element.text
}
return arr
}
async drawBoard(element) {
const node = this.findNode(element)
console.log('node', node)
return this.drawNode(node)
}
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)
}
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)
}
}
}