微信小程序画图wx-canvas-2d

885 阅读4分钟

需求一:生成二维码名片海报

image.png

<!--
 * @Author: mawenli
 * @Date: 2021-12-03 14:01:42
 * @LastEditors: lizhen
 * @LastEditTime: 2021-12-16 16:10:49
 * @Description: 
-->
<template>
    <div class="sharePannel-box">
        <div 
            class="main-box"
            :style="[
                {
                    background: `url(${PUBLIC_IMG_PATH}/doctor-card.jpeg)  no-repeat `,
                    backgroundSize: '100%',
                },
            ]">
                <image @tap="handelClickHidden" :src="`${PUBLIC_IMG_PATH}/cancelIcon.png`" alt="" class="cancel-img">
                <div class="info-box">
                    <image :src="doctorInfo.docAvatar?doctorInfo.docAvatar:`${PUBLIC_IMG_PATH}/default-doctor.png`" mode="aspectFill" alt="" class="icon-img">
                    <div class="name">{{doctorInfo.docName.length > 10 ?doctorInfo.docName.slice(0,10)+'...':doctorInfo.docName}}</div>
                    <div class="text-box" v-if="doctorInfo.titleName" >
                        <div class="title">{{doctorInfo.titleName}}</div>
                        <div class="line"></div>
                        <div class="title1">{{doctorInfo.officeName}}</div>
                    </div>
                    <div :class="{'code-box':1,'mg-top':!doctorInfo.titleName}">
                        <image :src="doctorInfo.qrCode" class="code-img" />
                    </div>
                    <div class="btn-box">
                        <m-button
                            btnText="分享二维码给患者"
                            fontsize="28"
                            radius="60"
                            :customStyle="{
                                width: '334rpx',
                                height: '80rpx',
                            }"
                            @click="handleClickShare"
                        >
                            <template v-slot:btn-icon>
                                <image
                                    class="share-icon"
                                    :src="`${PUBLIC_IMG_PATH}/shareIcon.png`"
                                    mode="aspectFill"
                                />
                            </template>
                        </m-button>
                    </div>
                </div>
        </div>
        <div class="canvas-wrapper" style="width: 590rpx; height: 916rpx;">
            <canvas type="2d" ref="canvas" id="poster-canvas" class="poster-canvas" style="width: 100%; height: 100%;" />
        </div>
    </div>
</template>

<script>
import { WxCanvas2d } from '@/utils/wx-canvas-2d';
export default {
    props:['PUBLIC_IMG_PATH','doctorInfo'],
    data() {
        return {
            canvas: null | WxCanvas2d,
            canDraw:false,
            posterURL: '',

        }
    },
    onReady() {
        this.canvas = new WxCanvas2d();
        this.createCanvas()
    },
    methods: {
        handelClickHidden () {
            this.$emit('hiddenFun')
        },
        toJSON(){},
        sharePoster() {
            const posterURL = this.posterURL;
            if (posterURL) {
                uni.hideLoading();
                uni.showShareImageMenu({
                    path: posterURL,
                });
            } else {
                this.drawCanvas();
            }
        },
        createCanvas() {
        return new Promise((resolve, reject) => {
            // 创建
            this.canvas.create({
                query: '.poster-canvas',
                rootWidth: 750, // 参考设备宽度 (即开发时UI设计稿的宽度,默认375,可改为750)
                component: this, // 自定义组件内需要传 this
                radius: 16,
                })
                .then(() => {
                    console.log('画布创建成功');
                    this.canDraw = true
                    resolve(null);
                })
                .catch((err) => {
                    console.log('----',err)
                    uni.hideLoading();
                    uni.showToast({ title: '生成失败,请稍后重试!' });
                    reject(err);
                });
            });
        },
        drawCanvas () {
            let series = []
            let docName = this.doctorInfo.docName.length > 10 ? this.doctorInfo.docName.slice(0,10)+'...' : this.doctorInfo.docName
            if(this.doctorInfo.titleName) {
                series = [
                   {
                       type: 'image', // 图片
                       url: `${this.PUBLIC_IMG_PATH}/doctor-card.jpeg`,
                       x: 0,
                       y: 0,
                       width: 590,
                       height: 916,
                       mode: 'aspectFill',
                   },
                   {
                       type: 'rect',
                       x: 235,
                       y: 48,
                       width: 120,
                       height: 120,
                       bgColor: '#ffffff',
                       radius: 60,
                   },
                   {
                       type: 'image',
                        url: this.doctorInfo.docAvatar ? this.doctorInfo.docAvatar :`${this.PUBLIC_IMG_PATH}/default-doctor.png`,
                       x: 237,
                       y: 50,
                       width: 116,
                       height: 116,
                       mode: 'aspectFill',
                       radius: 58,
                   },
                   {
                       type: 'text',
                       text: docName,
                       x: 295,
                       y: 184,
                       color: '#ffffff',
                       fontSize: 40,
                       fontWeight: 'bold',
                       lineHeight: 56,
                       align: 'center',
                   },
                   {
                       type: 'rect',
                       x: 294,
                       y: 253,
                       width: 2,
                       height: 24,
                       bgColor: 'rgba(255, 255, 255, 0.8)',
                   },
                   {
                       type: 'text',
                       text: this.doctorInfo.titleName,
                       x: 294 - this.doctorInfo.titleName.length * 24 - 8,
                       y: 248,
                       color: '#ffffff',
                       fontSize: 24,
                       lineHeight: 34,
                       align: 'left',
                   },
                   {
                       type: 'text',
                       text: this.doctorInfo.officeName,
                       x: 304,
                       y: 248,
                       color: '#ffffff',
                       fontSize: 24,
                       lineHeight: 34,
                       align: 'left',
                   },
                   {
                       type: 'rect',
                       x: 115,
                       y: 456,
                       width: 360,
                       height: 360,
                       bgColor: 'rgb(255, 255, 255)',
                       radius: 16,
                   },
                   {
                       type: 'image',
                       url: this.doctorInfo.qrCode,
                       x: 115,
                       y: 456,
                       width: 360,
                       height: 360,
                       mode: 'scaleToFill',
                       radius: 16,
                   },
               ]
            } else {
                series = [
                   {
                       type: 'image', // 图片
                       url: `${this.PUBLIC_IMG_PATH}/doctor-card.jpeg`,
                       x: 0,
                       y: 0,
                       width: 590,
                       height: 916,
                       mode: 'aspectFill',
                   },
                   {
                       type: 'rect',
                       x: 235,
                       y: 48,
                       width: 120,
                       height: 120,
                       bgColor: '#ffffff',
                       radius: 60,
                   },
                   {
                       type: 'image',
                       url: this.doctorInfo.docAvatar ? this.doctorInfo.docAvatar :`${this.PUBLIC_IMG_PATH}/default-doctor.png`,
                       x: 237,
                       y: 50,
                       width: 116,
                       height: 116,
                       mode: 'aspectFill',
                       radius: 58,
                   },
                   {
                       type: 'text',
                       text: docName,
                       x: 295,
                       y: 184,
                       color: '#ffffff',
                       fontSize: 40,
                       fontWeight: 'bold',
                       lineHeight: 56,
                       align: 'center',
                   },
                   {
                       type: 'rect',
                       x: 115,
                       y: 456,
                       width: 360,
                       height: 360,
                       bgColor: 'rgb(255, 255, 255)',
                       radius: 16,
                   },
                   {
                       type: 'image',
                       url: this.doctorInfo.qrCode,
                       x: 115,
                       y: 456,
                       width: 360,
                       height: 360,
                       mode: 'scaleToFill',
                       radius: 16,
                   },
               ]
            }
            this.canvas.draw({
                series: series
            })
            .then(() => {
                this.toDataURL();
            })  
            .catch((err) => {
                console.log('====',err)
                uni.hideLoading();
                uni.showToast({ title: '生成失败,请稍后重试!',icon: 'none' });
            });
        },
        toDataURL() {
            this.canvas
            .toDataURL()
            .then((res) => {
                this.posterURL = res
                this.sharePoster();

            })
            .catch(() => {
                wx.hideLoading();
                wx.showToast({ title: '生成失败,请稍后重试!3' });
            });
        },
        handleClickShare() {
            uni.showLoading({ title: '生成中' });
            // this.drawCanvas()

            if (this.canDraw) {
                this.sharePoster();
            } else {
                this.createCanvas().then(() => {
                    this.sharePoster();
                });
            }
        },

    }
}
</script>

<style lang="scss" scoped>
    .sharePannel-box{
        position: fixed;
        width: 100vw;
        height: 100vh;
        top: 0;
        left: 0;
        background-color: rgba(0,0,0,0.7);
        z-index: 1000;

        .main-box {
            width: 590rpx;
            height: 916rpx;
            position: absolute;
            top: 332rpx;
            left: 50%;
            transform: translateX(-50%);
            border-radius: 24rpx;
            z-index: 1002;
            
        }
        .cancel-img {
            width: 46rpx;
            height: 46rpx;
            display: block;
            position: absolute;
            right: -23rpx;
            top: -70rpx;
            z-index: 1001;
        }
        .info-box {
            margin-top: 48rpx;
            display: flex;
            flex-direction: column;
            align-items: center;
            .icon-img {
                width: 116rpx;
                height: 116rpx;
                border-radius: 50%;
                border: 2px solid $color-white;
            }
            .name {
                @include font-size(40,500);
                width: 80%;
                text-align: center;
                color: $color-white;
                margin-top: 16rpx;
            }
            .text {
                margin-top: 8rpx;
                color: #d5dbff;
                @include font-size(24);
            }
            .text-box {
                margin-top: 8rpx;
                color: #d5dbff;
                @include font-size(24);
                display: flex;
                align-items: center;
                width: 590rpx;
                .title {
                    width: 50%;
                    text-align: right;
                }
                .title1 {
                    width: 50%;
                    text-align: left;
                }
                .line {
                    width: 1rpx;
                    height: 28rpx;
                    background-color: #d5dbff;
                    margin: 0 14rpx;
                }
            }
             .code-box {
                width: 360rpx;
                height: 360rpx;
                background-color: $color-white;
                border-radius: 16rpx;
                margin-top: 114rpx;
                display: flex;
                align-items: center;
                justify-content: center;
                .code-img {
                    width: 320rpx;
                    height: 320rpx;
                }
            }
            .mg-top {
                margin-top: 156rpx;
            }
            .btn-box {
                margin-top: 32rpx;
            }
            .share-icon {
                width: 30rpx;
                height: 30rpx;
                margin-right: 16rpx;
            }
        }
        .canvas-wrapper {
            position: fixed;
            left: -1000px;
            top: -1000px;
        }
       
    }
</style>

需求二:分享截取特定区域的内容

image.png

<view class="blood-canvas">
    <BloodCircle ref="bloodCircleRef" :chartData="chartData" canvasId="blood-circle"/>
</view>

<view class="canvas-wrapper" style="width: 686rpx; height: 600rpx">
    <canvas type="2d" id="poster-canvas" class="poster-canvas" style="width: 100%; height: 100%" />
</view>

.canvas-wrapper {
    position: fixed;
    left: -1000px;
    top: -1000px;
}
import { WxCanvas2d } from '@/utils/wx-canvas-2d'

data() {
    return {
        canvas: null | WxCanvas2d,
        canDraw: false,
    }
},
onReady() {
    this.canvas = new WxCanvas2d()
    this.$nextTick(() => {
        this.createCanvas()
    })
}, 


// 转发功能
        onShareAppMessage(res) {
            if (this.bloodData.bloodSugarNum) {
                const promise = new Promise(async (resolve) => {
                    let res = await this.getTempImg()
                    let shareImgUrl = await this.uploadFile(res)
                    console.log(shareImgUrl, '######')
                    resolve({
                        title: '记录血糖、饮食,栗子助你控糖',
                        path: '/pages/home/home',
                        imageUrl: shareImgUrl,
                    })
                })
                return {
                    title: '记录血糖、饮食,栗子助你控糖',
                    path: '/pages/home/home',
                    promise,
                }
            } else {
                return {
                    title: '记录血糖、饮食,栗子助你控糖',
                    path: '/pages/home/home',
                }
            }
        },
        getTempImg() {
            return new Promise((resolve, reject) => {
                if (this.$refs.bloodCircleRef) {
                    this.$refs.bloodCircleRef.getImage(async (path) => {
                        this.circleImg = path
                        // 画图
                        let canvasUrl = await this.drawCanvas()
                        resolve(canvasUrl)
                    })
                } else {
                    reject('err')
                }
            })
        },
        createCanvas() {
            return new Promise((resolve, reject) => {
                // 创建
                this.canvas
                    .create({
                        query: '.poster-canvas',
                        rootWidth: 750, // 参考设备宽度 (即开发时UI设计稿的宽度,默认375,可改为750)
                        radius: 16,
                    })
                    .then(() => {
                        console.log('画布创建成功')
                        this.canDraw = true
                        resolve(null)
                    })
                    .catch((err) => {
                        console.log('画布创建失败', err)
                        uni.hideLoading()
                        reject(err)
                    })
            })
        },
        drawCanvas() {
            let series = []
            series = [
                {
                    type: 'text',
                    text: `${this.$date.formatDate(this.bloodData.dateTime, 'YYYY/MM/DD hh:mm')}`,
                    x: 32,
                    y: 40,
                    color: '#101734',
                    fontSize: 32,
                    fontWeight: 'bold',
                    lineHeight: 45,
                },
                {
                    type: 'text',
                    text: '历史记录 >',
                    x: 487,
                    y: 41,
                    color: '#ACAEB4',
                    fontSize: 32,
                    fontWeight: 'normal',
                    lineHeight: 45,
                },
                {
                    type: 'image',
                    url: this.circleImg,
                    x: 146,
                    y: 116,
                    width: 408,
                    height: 400,
                    mode: 'aspectFill',
                    align: 'center',
                },
                {
                    type: 'text',
                    text: `${this.bloodData.dataType}`,
                    x: 346,
                    y: 221,
                    color: '#212222',
                    fontSize: 32,
                    fontWeight: 'normal',
                    lineHeight: 45,
                    align: 'center',
                },
                {
                    type: 'text',
                    text: `${this.bloodData.bloodSugarNum}`,
                    x: 346,
                    y: 293,
                    color: '#212222',
                    fontSize: 88,
                    fontWeight: 'normal',
                    lineHeight: 88,
                    align: 'center',
                },
                {
                    type: 'text',
                    text: 'mmol/L',
                    x: 346,
                    y: 386,
                    color: '#7F8383',
                    fontSize: 28,
                    fontWeight: 'normal',
                    align: 'center',
                },
                {
                    type: 'text',
                    text: `你的血糖数值属于:`,
                    x: 167,
                    y: 495,
                    color: '#606269',
                    fontSize: 32,
                    fontWeight: 'normal',
                },
                {
                    type: 'text',
                    text: `${this.bloodData.bloodSugarResult}`,
                    x: 455,
                    y: 495,
                    color: `${this.bloodData.bloodSugarResultCode == 11 ? '#23C0A4' : this.bloodData.bloodSugarResultCode == 12 ? '#329DFF' : '#FF9900'}`,
                    fontSize: 32,
                    fontWeight: 'normal',
                },
            ]
            return new Promise((resolve) => {
                this.canvas
                    .draw({
                        series: series,
                    })
                    .then(async () => {
                        let res = await this.toDataURL()
                        resolve(res)
                    })
                    .catch((err) => {
                        console.log('生成图片失败', err)
                        uni.hideLoading()
                    })
            })
        },
        toDataURL() {
            return new Promise((resolve) => {
                this.canvas
                    .toDataURL()
                    .then((res) => {
                        resolve(res)
                    })
                    .catch((err) => {
                        console.log('====', err)
                        wx.hideLoading()
                    })
            })
        },
        async uploadFile(filePath) {
            let _that = this
            let token = await AuthProvider.getAccessToken()
            let resArr = await uni.uploadFile({
                url: _that.$config.UPLOAD_FILE_URL,
                name: 'file',
                filePath,
                header: { Authorization: token },
            })
            if (resArr[0]) {
                console.log('识别图片报错')
                _that.showError(resArr[0].errMsg)
                return false
            } else {
                const res = resArr[1]
                const resData = JSON.parse(res.data)
                if (resData.code == 100) {
                    return resData.data.url
                }
                return false
            }
        },

wx-canvas-2d.js

/**
 * 错误码字典
 * @var {Object}
 */
const ERR_CODE = {
    100: '生成图片失败',
    101: '获取设置信息失败',
    102: '保存图片到相册失败',
    103: '授权失败',
    104: '用户拒绝授权',
    105: '用户前往授权页',
    106: '获取图片信息失败',
    107: '加载图片失败',
}

let systemInfo
function getSystemInfoSync() {
    if (!systemInfo) {
        systemInfo = wx.getSystemInfoSync()
    }

    return systemInfo
}
const SYS_INFO = getSystemInfoSync()

/*
 * 微信小程序 canvas-2d 绘图工具,轻量、便捷、容易维护。
 * 基于 https://github.com/kiccer/wx-canvas-2d 修订
 */
class WxCanvas2d {
    query = '' // canvas 的查询条件
    bgColor = null // canvas 背景色
    radius = null // canvas 背景色
    component = null // 如果是在自定义组件中,需要获取到自定义组件的内部 this 变量 (即,传入 this)
    canvas = null // canvas 节点
    ctx = null // canvas 上下文
    dpr = 1 // 像素比
    rootWidth = 0 // UI设计稿宽度
    fontFamily = 'sans-serif' // 默认字体,目前好像只有这个是可用的
    startTime = Date.now()
    debugger = false // 调试模式

    constructor() {
        // Object.defineProperty(this, 'extendDrawMethods', {
        //     value: {},
        //     writable: false,
        //     enumerable: false,
        //     configurable: true
        // })
    }

    static extendDrawMethods = {}
    static addSeries = function (type, handle) {
        WxCanvas2d.extendDrawMethods[type] = handle
    }

    // 创建画布
    create(opts) {
        // console.log(opts)
        return new Promise((resolve, reject) => {
            const options = {
                query: '',
                rootWidth: 375,
                ...opts,
            }

            if (!options.query) reject(new Error("[WxCanvas2d] 'query' is empty."))

            this.query = options.query
            this.bgColor = options.bgColor
            this.component = options.component
            this.radius = options.radius

            const query = this.component ? wx.createSelectorQuery().in(this.component) : wx.createSelectorQuery()

            query
                .select(this.query)
                .fields({ node: true, size: true })
                .exec((res) => {
                    // console.log(res)
                    if (!res[0]) return false

                    const canvas = res[0].node
                    const ctx = canvas.getContext('2d')
                    const dpr = SYS_INFO.pixelRatio

                    this.canvas = canvas
                    this.ctx = ctx
                    this.dpr = dpr
                    this.rootWidth = options.rootWidth

                    this.canvas.width = res[0].width * this.dpr
                    this.canvas.height = res[0].height * this.dpr
                    // this.ctx.scale(this.dpr, this.dpr)

                    resolve(true)
                    return true
                })
        })
    }

    // 清空画布
    clear() {
        this.ctx.clearRect(0, 0, this.xDpr(this.canvas.width), this.xDpr(this.canvas.height))

        if (this.radius) {
            this.drawRectPath({
                x: 0,
                y: 0,
                width: (this.canvas.width / SYS_INFO.screenWidth / this.dpr) * this.rootWidth,
                height: (this.canvas.height / SYS_INFO.screenWidth / this.dpr) * this.rootWidth,
                radius: this.radius,
            })

            this.ctx.clip()
        }

        if (this.bgColor) {
            this.ctx.fillStyle = this.bgColor
            this.ctx.fillRect(0, 0, this.xDpr(this.canvas.width), this.xDpr(this.canvas.height))
        }
    }

    // canvas 大小适配
    xDpr(val) {
        return (val * this.dpr * SYS_INFO.screenWidth) / this.rootWidth
    }

    // 绘制画布 (重设画布大小)
    draw(opts) {
        // console.log(opts)
        return new Promise((resolve, reject) => {
            this.startTime = Date.now()
            const { series } = opts

            const query = this.component ? wx.createSelectorQuery().in(this.component) : wx.createSelectorQuery()

            query
                .select(this.query)
                .fields({ node: true, size: true })
                .exec((res) => {
                    // console.log(res)
                    if (!res[0]) return false

                    // 重设画布大小
                    this.canvas.width = res[0].width * this.dpr
                    this.canvas.height = res[0].height * this.dpr
                    // this.ctx.scale(this.dpr, this.dpr)

                    this.clear() // 画之前先清空一次画布

                    // 根据 zIndex 排序 (从小到大,先画小的,这样越大的显示在越上方)
                    const _series = series.map((n) => ({ ...n, zIndex: n.zIndex || 0 })).sort((n, m) => n.zIndex - m.zIndex)

                    // 绘制方法映射表
                    const drawFunc = {
                        rect: (cvs, opts) => this.drawRect(opts),
                        image: (cvs, opts) => this.drawImage(opts),
                        text: (cvs, opts) => this.drawText(opts),
                        line: (cvs, opts) => this.drawLine(opts),
                        arc: (cvs, opts) => this.drawArc(opts),
                        ...WxCanvas2d.extendDrawMethods,
                    }

                    // 按顺序绘制图层方法
                    const next = (index = 0) => {
                        if (index < _series.length) {
                            const options = _series[index]
                            if (drawFunc[options.type]) {
                                // debugLogout(`正在绘制 [${options.type}] (${index + 1}/${_series.length})`)
                                this.styleClear() // 绘制新图层前,先还原一次样式设置
                                drawFunc[options.type](this, options)
                                    .then(() => {
                                        // debugLogout(`绘制成功 [${options.type}] (${index + 1}/${_series.length})`);
                                        next(++index)
                                    })
                                    .catch((err) => {
                                        // debugLogout('绘制失败');
                                        reject(err) // 绘制失败抛错
                                    })
                            } else {
                                // console.warn(`[WxCanvas2d] Unknown type: '${options.type}'`)
                                // debugLogout(`未知类型 type: '${options.type}'`, 'error');
                                next(++index)
                            }
                        } else {
                            //   debugLogout(`绘制完成 (${Date.now() - this.startTime}ms)`);
                            resolve(1) // 所有图层绘制完毕
                        }
                    }

                    //   debugLogout('开始绘制');
                    next() // 开始按顺序绘制图层
                    return true
                })
        })
    }

    // 清空 (初始化) 样式
    styleClear() {
        this.ctx.setTextAlign = 'left'
        this.ctx.textBaseline = 'top'
        this.ctx.fillStyle = '#000'
        this.ctx.font = `${this.xDpr((12 * this.rootWidth) / SYS_INFO.screenWidth)}px ${this.fontFamily}`
        this.ctx.lineCap = 'butt'
        this.ctx.setLineDash([1, 0])
        this.ctx.lineDashOffset = 0
        this.ctx.lineJoin = 'bevel'
        this.ctx.lineWidth = this.xDpr(1)
        this.ctx.strokeStyle = '#000'
        // this.setContainerRadius()
    }

    // 设置线的样式
    setLineStyle(lineStyle = {}) {
        const {
            cap = 'butt', // butt | round | square
            join = 'bevel', // bevel | round | miter
            offset = 0,
            dash = [1, 0],
            color = '#000',
            width = 2,
        } = lineStyle

        this.ctx.lineCap = cap
        this.ctx.setLineDash(dash.map((n) => this.xDpr(n)))
        this.ctx.lineDashOffset = this.xDpr(offset)
        this.ctx.lineJoin = join
        this.ctx.lineWidth = this.xDpr(width)
        this.ctx.strokeStyle = color
    }

    // 绘制矩形
    drawRect(opts) {
        return new Promise((resolve) => {
            const {
                x = 0,
                y = 0,
                width = 0,
                height = 0,
                // color = '',
                bgColor = '',
                radius = 0,
                lineStyle,
                // blur = 0
            } = opts

            // 防止 radius 设置过大
            const radiusMin = Math.min(radius, Math.min(width, height) / 2)

            // this.ctx.strokeStyle = color
            // 设置线段样式
            this.setLineStyle(lineStyle)
            // 设置填充色
            this.ctx.fillStyle = bgColor

            this.drawRectPath({
                x: x,
                y: y,
                width: width,
                height: height,
                radius: radiusMin,
            })

            if (lineStyle?.color) {
                this.ctx.stroke()
            }

            if (bgColor) {
                this.ctx.fill()
            }

            resolve(1)
        })
    }

    // 绘制矩形路径
    drawRectPath(opts) {
        const { x = 0, y = 0, width = 0, height = 0, radius = 0 } = opts
        // console.log(_opts)

        // 圆角起始/结束方向
        const angle = {
            top: Math.PI * 1.5,
            right: 0,
            bottom: Math.PI * 0.5,
            left: Math.PI,
        }

        // 圆角方向
        const angleArr = [
            [angle.left, angle.top],
            [angle.top, angle.right],
            [angle.right, angle.bottom],
            [angle.bottom, angle.left],
        ]

        // 圆角中心点坐标
        const arcPos = [
            [x + radius, y + radius].map((n) => this.xDpr(n)), // left top
            [x + width - radius, y + radius].map((n) => this.xDpr(n)), // top right
            [x + width - radius, y + height - radius].map((n) => this.xDpr(n)), // right bottom
            [x + radius, y + height - radius].map((n) => this.xDpr(n)), // bottom left
        ]

        this.ctx.beginPath()

        Array(4)
            .fill(0)
            .forEach((n, i) => {
                this.ctx.arc(...arcPos[i], this.xDpr(radius), ...angleArr[i])
            })

        this.ctx.closePath()
    }

    // 绘制图片
    drawImage(opts) {
        // console.log(opts)
        const { url = '', x = 0, y = 0, width = 0, height = 0, mode = 'scaleToFill', radius = 0 } = opts

        // scaleToFill: 缩放: 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
        // aspectFit: 缩放: 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
        // aspectFill: 缩放: 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
        // widthFix: 缩放: 宽度不变,高度自动变化,保持原图宽高比不变
        // top: 裁剪: 不缩放图片,只显示图片的顶部区域
        // bottom: 裁剪: 不缩放图片,只显示图片的底部区域
        // center: 裁剪: 不缩放图片,只显示图片的中间区域
        // left: 裁剪: 不缩放图片,只显示图片的左边区域
        // right: 裁剪: 不缩放图片,只显示图片的右边区域
        // top left: 裁剪: 不缩放图片,只显示图片的左上边区域
        // top right: 裁剪: 不缩放图片,只显示图片的右上边区域
        // bottom left: 裁剪: 不缩放图片,只显示图片的左下边区域

        return new Promise((resolve, reject) => {
            wx.getImageInfo({
                src: url,
                success: (res) => {
                    // console.log(res)
                    // console.log(wx.getFileSystemManager)
                    // console.log(wx.getFileSystemManager().readFileSync(res, 'base64'))
                    const imgWidth = res.width
                    const imgHeight = res.height
                    const aspectRatio = width / height
                    let widthRatio = 1
                    let heightRatio = 1

                    // 原图等比例缩放后截取范围的长宽比
                    if (mode === 'aspectFit') {
                        widthRatio = res.width / res.height < aspectRatio ? ((width / res.width) * res.height) / height : 1
                        heightRatio = res.width / res.height > aspectRatio ? ((height / res.height) * res.width) / width : 1
                    } else if (mode === 'aspectFill') {
                        widthRatio = res.width / res.height > aspectRatio ? ((width / res.width) * res.height) / height : 1
                        heightRatio = res.width / res.height < aspectRatio ? ((height / res.height) * res.width) / width : 1
                    }

                    const imgCut = {
                        scaleToFill: [0, 0, imgWidth, imgHeight], // 缩放: 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
                        aspectFit: [
                            (res.width - res.width * widthRatio) / 2,
                            (res.height - res.height * heightRatio) / 2,
                            res.width * widthRatio,
                            res.height * heightRatio,
                        ], // 缩放: 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
                        aspectFill: [
                            (res.width - res.width * widthRatio) / 2,
                            (res.height - res.height * heightRatio) / 2,
                            res.width * widthRatio,
                            res.height * heightRatio,
                        ], // 缩放: 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
                        widthFix: [], // 缩放: 宽度不变,高度自动变化,保持原图宽高比不变
                        top: [(imgWidth - width) / 2, 0, width, height], // 裁剪: 不缩放图片,只显示图片的顶部区域
                        bottom: [(imgWidth - width) / 2, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的底部区域
                        center: [(imgWidth - width) / 2, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的中间区域
                        left: [0, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的左边区域
                        right: [imgWidth - width, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的右边区域
                        'top left': [0, 0, width, height], // 裁剪: 不缩放图片,只显示图片的左上边区域
                        'top right': [imgWidth - width, 0, width, height], // 裁剪: 不缩放图片,只显示图片的右上边区域
                        'bottom left': [0, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的左下边区域
                        'bottom right': [imgWidth - width, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的右下边区域
                    }
                    // console.log(mode)

                    const img = this.canvas.createImage()

                    img.src = res.path
                    img.onload = () => {
                        if (radius) {
                            this.ctx.save()
                            this.drawRectPath({ x, y, width, height, radius })
                            this.ctx.clip()
                        }

                        this.ctx.drawImage(
                            img,
                            // ...(imgCut[mode] || imgCut.scaleToFill).map((n, i) => i >= 4 ? this.xDpr(n) : n)
                            ...(imgCut[mode] || []),
                            this.xDpr(x) || 0,
                            this.xDpr(y) || 0,
                            this.xDpr(width || res.width),
                            this.xDpr(height || res.height),
                        )

                        if (radius) {
                            this.ctx.restore()
                        }

                        resolve(1)
                    }

                    img.onerror = (err) => {
                        // debugLogout(err);
                        reject(errCode(107))
                    }
                },
                fail: (err) => {
                    //   debugLogout(err);
                    reject(errCode(106))
                },
            })
        })
    }

    // 绘制文本
    drawText(opts) {
        // console.log(opts)
        return new Promise((resolve) => {
            const _opts = {
                // text: '',
                x: 0,
                y: 0,
                color: '#000',
                fontSize: 12,
                fontWeight: '',
                width: Infinity,
                baseline: 'top', // top | hanging | middle | alphabetic | ideographic | bottom
                align: 'left', // left | right | center | start | end
                ...opts,
                text: String(opts.text) || '',
                ellipsis: Math.max(+opts.ellipsis, 0), // 最多显示行数,超出显示...
                lineHeight: opts.lineHeight || opts.fontSize || 12,
            }

            let start = 0 // 截取的起始下标
            let index = 0 // 行数下标
            let splitStr = [] // 拆分后的文本数组

            this.ctx.textAlign = _opts.align
            this.ctx.textBaseline = _opts.baseline
            this.ctx.fillStyle = _opts.color
            this.ctx.font = `${_opts.fontWeight} ${this.xDpr(_opts.fontSize)}px ${this.fontFamily}`

            // 拆分文本
            _opts.text.split('').forEach((n, i) => {
                const str = _opts.text.slice(start, i + 1)
                if (this.ctx.measureText(str).width < this.xDpr(_opts.width)) {
                    splitStr[index] = str
                } else {
                    start = i
                    index++
                }
            })

            // 最大显示行,超出显示省略号
            if (_opts.ellipsis && splitStr.length > _opts.ellipsis) {
                splitStr = splitStr.slice(0, _opts.ellipsis)
                splitStr[_opts.ellipsis - 1] = splitStr[_opts.ellipsis - 1].slice(0, -1) + '...'
            }
            // 循环绘制文本
            splitStr.forEach((n, i) => {
                const y = _opts.y + _opts.lineHeight * i + (_opts.lineHeight - _opts.fontSize) / 2
                this.ctx.fillText(n, this.xDpr(_opts.x), this.xDpr(y), 590)
            })

            resolve(1)
        })
    }

    // 绘制线段
    drawLine(opts) {
        // console.log(opts)
        return new Promise((resolve) => {
            const { lineStyle, line = [] } = opts

            // 设置线段样式
            this.setLineStyle(lineStyle)

            // 绘制线段
            line.forEach((n, i) => {
                if (!i) {
                    this.ctx.beginPath()
                    this.ctx.moveTo(...n.point.map((n) => this.xDpr(n)))
                } else {
                    this.ctx.lineTo(...n.point.map((n) => this.xDpr(n)))
                    this.ctx.stroke()
                }
            })

            resolve(1)
        })
    }

    // 绘制弧形
    drawArc(opts) {
        return new Promise((resolve) => {
            const { x = 0, y = 0, r = 0, start = 0, end = 0, reverse = false, lineStyle } = opts

            // 设置线段样式
            this.setLineStyle(lineStyle)

            // 绘制线段
            this.ctx.beginPath()
            this.ctx.arc(this.xDpr(x), this.xDpr(y), this.xDpr(r), start, end, reverse)
            this.ctx.stroke()
            resolve(1)
        })
    }

    // 保存画布内容到相册
    saveToAlbum(opts) {
        return new Promise((resolve, reject) => {
            const _opts = {
                x: 0,
                y: 0,
                width: 0,
                height: 0,
                destWidth: 0,
                destHeight: 0,
                modalOption: {},
                ...opts,
            }

            wx.canvasToTempFilePath({
                x: _opts.x,
                y: _opts.y,
                width: _opts.width,
                height: _opts.height,
                destWidth: this.xDpr(_opts.destWidth),
                destHeight: this.xDpr(_opts.destHeight),
                canvas: this.canvas,
                success: (res) => {
                    const tempFilePath = res.tempFilePath

                    getAuth('writePhotosAlbum')
                        .then((res) => {
                            if (res?.code === 1) {
                                wx.showModal({
                                    title: _opts.modalOption.title || '获取权限',
                                    content: _opts.modalOption.content || '请前往开启相册权限',
                                    success:
                                        _opts.modalOption.success ||
                                        ((res) => {
                                            if (res.confirm) {
                                                wx.openSetting()
                                                // debugLogout(`${ERR_CODE[105]} (105)`, 'error');
                                                reject(errCode(105))
                                            } else if (res.cancel) {
                                                // debugLogout(`${ERR_CODE[104]} (104)`, 'error');
                                                reject(errCode(104))
                                            }
                                        }),
                                })
                            } else if ([2, 3].indexOf(res?.code) >= 0) {
                                saveImageToPhotosAlbum(tempFilePath)
                                    .then(() => {
                                        // debugLogout('保存图片到相册成功');
                                        resolve(1)
                                    })
                                    .catch(() => {
                                        // debugLogout(`${ERR_CODE[102]} (102)`, 'error');
                                        reject(errCode(102))
                                    })
                            }
                        })
                        .catch(() => {
                            //   debugLogout(`${ERR_CODE[101]} (101)`, 'error');
                            reject(errCode(101))
                        })
                },
                fail: () => {
                    //   debugLogout(`${ERR_CODE[100]} (100)`, 'error');
                    reject(errCode(100))
                },
            })
        })
    }
    toDataURL(opts) {
        return new Promise((resolve, reject) => {
            const _opts = {
                x: 0,
                y: 0,
                width: 0,
                height: 0,
                destWidth: 0,
                destHeight: 0,
                modalOption: {},
                ...opts,
            }

            wx.canvasToTempFilePath({
                x: _opts.x,
                y: _opts.y,
                width: _opts.width,
                height: _opts.height,
                destWidth: this.xDpr(_opts.destWidth),
                destHeight: this.xDpr(_opts.destHeight),
                canvas: this.canvas,
                success: (res) => {
                    const tempFilePath = res.tempFilePath
                    resolve(tempFilePath)
                },
                fail: () => {
                    //   debugLogout(`${ERR_CODE[100]} (100)`, 'error');
                    reject(errCode(100))
                },
            })
        })
    }

    // 增加新的绘制类型
    // addSeries (type, handle) {
    //     this.extendDrawMethods[type] = (...opts) => {
    //         handle(this, ...opts)
    //     }
    // }
}

/**
 * 生成错误代码信息
 * @param   {Number}  code  错误码
 * @return  {Object}        错误代码信息
 */
function errCode(code) {
    return {
        code,
        msg: ERR_CODE[code],
    }
}

/**
 * 小程序获取权限
 * @param   {String}  name  权限名称
 * @return  {Promise}        Promise
 */
function getAuth(name) {
    return new Promise((resolve, reject) => {
        wx.getSetting({
            success(res) {
                const callback = (code) => ({ settings: res, code })

                if (res.authSetting['scope.' + name] !== undefined && res.authSetting['scope.' + name] !== true) {
                    // 用户主动取消过
                    resolve(callback(1))
                } else if (res.authSetting['scope.' + name] === undefined) {
                    // 第一次向用户获取
                    resolve(callback(2))
                } else {
                    // 用户已授权
                    resolve(callback(3))
                }
            },
            fail(err) {
                reject(err)
            },
        })
    })
}

/**
 * 保存图片到相册
 * @param   {String}  tempFilePath  临时文件路径
 * @return  {Promise}                Promise
 */
function saveImageToPhotosAlbum(tempFilePath) {
    return new Promise((resolve, reject) => {
        wx.saveImageToPhotosAlbum({
            filePath: tempFilePath,
            success: () => {
                resolve(1)
            },
            fail: (err) => {
                reject(err)
            },
        })
    })
}

export { WxCanvas2d }