前段时间在将网页版三合一收款码转移到小程序版的过程中遇到了一个问题,就是原来的艺术码生成库只能在网页使用,小程序中无法使用。后来在Github中找了一段时间,然而并没有找到相关的库,之前生成库的作者也有一段时间没在Github上活跃了,也就没厚着脸皮找他将生成库兼容到小程序,便只能自己硬着头皮封装一个小程序版的艺术二维码生成库。

1、定位点绘制

实现艺术二维码的核心是如何将素材按指定要求渲染到背景图上去。二维码的具体生成规则还没有理解,但是从通过小程序二维码生成库weapp-qrcode生成的二维码中可以发现,如果将二维码进行平均划分,左上角、右上角和左下角的三个定位点宽高都占据了七份,这个值是在经过修改二维码纠错级别 correctLevel后无论如何也不会发生改变的。

这样,我们便可计算出三个定位点的宽高和位置:以二维码的宽(width)、高(height)、横向点阵数量(num)为例,那么单个点阵占据的宽高为 pWidth = width / num,定位点的宽高为 (width / num) * 7,左上角定位点的起始绘制位置为 (0, 0),右上角的起始绘制位置为 ((num - 1) - 7) * pWidth, 0),左下角的起始绘制位置为 (0, (num - 1) - 7) * pWidth)

二维码默认样式

2、信息点绘制

二维码的信息点可以根据自己的需要定制,一般来说占用面积越小,出现的概率越大。这里我预设了如下图所示可以使用的信息点:

二维码信息点

我们可以将二维码转换为一个二维数组,有数据的位置标志为1,没有数据的位置标记为0,那么一个二维码可以表示为:

[
    [1, 1, 1, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    [0, 1, 0, 1, 1, 1, 0, 0, 1, 0, ....],
    ...
]

除去三个定位点,我们可以通过遍历是否存在可以放置指定比例素材的位置,如果存在,将素材和素材的位置进行记录。我们以row2col2为例,设第一个信息点的位置为 (x, y),这时去循环判断 (x, y)、(x + 1, y)、(x, y + 1)、(x + 1, y + 1)这四个位置的标志是否都为1,如果满足条件,则将这四个位置标记为已使用,并将起始位置 (x, y)添加到row2col2需要渲染的位置中。

将所有的能够渲染的素材遍历完成之后,我们会得到素材对应的坐标位置,然后将素材进行渲染即可。下面为实现艺术二维码生成的部分代码:

function drawQrcode(options) {
    options = options || {};
    options = extend(true, {
        width: 256,
        height: 256,
        x: 0,
        y: 0,
        typeNumber: -1,
        correctLevel: QRErrorCorrectLevel.H,
        background: '#ffffff',
        foreground: '#000000',
        image: {
            imageResource: '',
            dx: 0,
            dy: 0,
            dWidth: 256,
            dHeight: 256
        },
        materials: {
            eye: [],
            col4: [],
            row4: [],
            row2col3: [],
            row3col2: [],
            col3: [],
            row3: [],
            row2col2: [],
            col2: [],
            row2: [],
            single: []
        }
    }, options);

    if (!options.canvasId && !options.ctx) {
        console.warn('please set canvasId or ctx!');
        return;
    }

    createCanvas();

    function createCanvas() {

        // create the qrcode itself
        var qrcode = new QRCode(options.typeNumber, options.correctLevel);
        qrcode.addData(utf16to8(options.text));
        qrcode.make();

        // get canvas context
        var ctx;
        if (options.ctx) {
            ctx = options.ctx;
        } else {
            ctx = options._this ? wx.createCanvasContext && wx.createCanvasContext(options.canvasId, options._this) : wx.createCanvasContext && wx.createCanvasContext(options.canvasId);
        }

        // count dots
        let dotArray = []
        for (var row = 0; row < qrcode.getModuleCount(); row++) {
            let arr = []
            for (var col = 0; col < qrcode.getModuleCount(); col++) {
                arr.push(qrcode.isDark(row, col) ? 1 : 0)
            }
            dotArray.push(arr)
        }

        let descPosition = {
            eye: [],
            col4: [],
            row4: [],
            row2col3: [],
            row3col2: [],
            col3: [],
            row3: [],
            row2col2: [],
            col2: [],
            row2: [],
            single: []
        }


        let copyDotArray = dotArray.map(item => item.map(iitem => false))

        function isMatchRule(rowIndex, colIndex) {
            return copyDotArray[rowIndex] && copyDotArray[rowIndex][colIndex] === false && dotArray[rowIndex][colIndex] === 1
        }

        // position dot
        dotArray.forEach((row, rowIndex) => {
            row.forEach((col, colIndex) => {
                if ((rowIndex < 7 && colIndex < 7) || (rowIndex < 7 && colIndex > row.length - 1 - 7) || (rowIndex > row.length - 1 - 7 && colIndex < 7)) {
                    copyDotArray[rowIndex][colIndex] = true
                    if ((rowIndex === 0 && colIndex === 0) || (rowIndex === 0 && colIndex === row.length - 7) || (rowIndex === row.length - 7 && colIndex === 0)) {
                        descPosition.eye.push([rowIndex, colIndex])
                    }
                }
            })
        })

        // not position dot
        dotArray.forEach((row, rowIndex) => {
            row.forEach((col, colIndex) => {
                if ((rowIndex < 7 && colIndex < 7) || (rowIndex < 7 && colIndex > row.length - 1 - 7) || (rowIndex > row.length - 1 - 7 && colIndex < 7)) {

                } else {
                    // col4
                    if (options.materials.col4.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1) && isMatchRule(rowIndex, colIndex + 2) && isMatchRule(rowIndex, colIndex + 3)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = copyDotArray[rowIndex][colIndex + 2] = copyDotArray[rowIndex][colIndex + 3] = true
                        descPosition.col4.push([rowIndex, colIndex])
                    }

                    // row4
                    if (options.materials.row4.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex + 1, colIndex) && isMatchRule(rowIndex + 2, colIndex) && isMatchRule(rowIndex + 3, colIndex)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex + 1][colIndex] = copyDotArray[rowIndex + 2][colIndex] = copyDotArray[rowIndex + 3][colIndex] = true
                        descPosition.row4.push([rowIndex, colIndex])
                    }

                    // row2col3
                    if (options.materials.row2col3.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1) && isMatchRule(rowIndex, colIndex + 2) && isMatchRule(rowIndex + 1, colIndex) && isMatchRule(rowIndex + 1, colIndex + 1) && isMatchRule(rowIndex + 1, colIndex + 2)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = copyDotArray[rowIndex][colIndex + 2] = copyDotArray[rowIndex + 1][colIndex] = copyDotArray[rowIndex + 1][colIndex + 1] = copyDotArray[rowIndex + 1][colIndex + 2] = true
                        descPosition.row2col3.push([rowIndex, colIndex])
                    }

                    // row3col2
                    if (options.materials.row3col2.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1) && isMatchRule(rowIndex + 1, colIndex) && isMatchRule(rowIndex + 1, colIndex + 1) && isMatchRule(rowIndex + 2, colIndex) && isMatchRule(rowIndex + 2, colIndex + 1)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = copyDotArray[rowIndex + 1][colIndex] = copyDotArray[rowIndex + 1][colIndex + 1] = copyDotArray[rowIndex + 2][colIndex] = copyDotArray[rowIndex + 2][colIndex + 1] = true
                        descPosition.row3col2.push([rowIndex, colIndex])
                    }

                    // col3
                    if (options.materials.col3.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1) && isMatchRule(rowIndex, colIndex + 2)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = copyDotArray[rowIndex][colIndex + 2] = true
                        descPosition.col3.push([rowIndex, colIndex])
                    }

                    // row3
                    if (options.materials.row3.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex + 1, colIndex) && isMatchRule(rowIndex + 2, colIndex)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex + 1][colIndex] = copyDotArray[rowIndex + 2][colIndex] = true
                        descPosition.row3.push([rowIndex, colIndex])
                    }

                    // row2col2
                    if (options.materials.row2col2.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1) && isMatchRule(rowIndex + 1, colIndex) && isMatchRule(rowIndex + 1, colIndex + 1)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = copyDotArray[rowIndex + 1][colIndex] = copyDotArray[rowIndex + 1][colIndex + 1] = true
                        descPosition.row2col2.push([rowIndex, colIndex])
                    }

                    // col2
                    if (options.materials.col2.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex, colIndex + 1)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex][colIndex + 1] = true
                        descPosition.col2.push([rowIndex, colIndex])
                    }

                    // row2
                    if (options.materials.row2.length && isMatchRule(rowIndex, colIndex) && isMatchRule(rowIndex + 1, colIndex)) {
                        copyDotArray[rowIndex][colIndex] = copyDotArray[rowIndex + 1][colIndex] = true
                        descPosition.row2.push([rowIndex, colIndex])
                    }

                    // single
                    if (options.materials.single.length && isMatchRule(rowIndex, colIndex)) {
                        copyDotArray[rowIndex][colIndex] = true
                        descPosition.single.push([rowIndex, colIndex])
                    }
                }
            })
        })

        if (options.image.imageResource) {
            ctx.drawImage(options.image.imageResource, options.image.dx, options.image.dy, options.image.dWidth, options.image.dHeight);
        }

        // compute tileW/tileH based on options.width/options.height
        var tileW = options.width / qrcode.getModuleCount();
        var tileH = options.height / qrcode.getModuleCount();

        // draw materials
        function drawMaterials(type, colNum, rowNum) {
            descPosition[type].forEach((item, index) => {
                ctx.drawImage(options.materials[type][Math.floor((Math.random() * options.materials[type].length))], options.x + item[1] * tileW, options.y + item[0] * tileH, tileW * colNum, tileH * rowNum)
            })
        }

        drawMaterials('eye', 7, 7)
        drawMaterials('col4', 4, 1)
        drawMaterials('row4', 1, 4)
        drawMaterials('row2col3', 2, 3)
        drawMaterials('row3col2', 3, 2)
        drawMaterials('col3', 3, 1)
        drawMaterials('row3', 1, 3)
        drawMaterials('row2col2', 2, 2)
        drawMaterials('col2', 2, 1)
        drawMaterials('row2', 1, 2)
        drawMaterials('single', 1, 1)

        // callback
        ctx.draw(false, function(e) {
            options.callback && options.callback(e);
        });
    }
}

3、添加背景图

只绘制一个艺术二维码有时候会比较单调,这时可以为艺术二维码添加背景图,然后将二维码按一定比例在背景图上。需要注意的是, 背景图的绘制需要在二维码之前。

4、使用方法

最终的源码我放在了github:

https://github.com/BWmelon/artQrcode

在页面中添加一个 canvas标签和 image标签,代码如下:

<image src="{{url}}" class="image" mode="widthFix" bindtap="handlePreview" style="width: 750rpx;"></image>
<canvas style="width: 900px; height: 1200px;position: absolute;left: -99999rpx;" canvas-id="myQrcodeOne"></canvas>

在js中引入生成库:

import drawQrcode from '../../utils/weapp.artQrcode.js'

生成艺术二维码

drawQrcode({
    width: 520,
    height: 520,
    canvasId: 'myQrcodeOne',
    // ctx: wx.createCanvasContext('myQrcodeOne'),
    text: 'https://qr.no0a.cn',
    x: 180,
    y: 100, // v1.0.0+版本支持在二维码上绘制图片
    image: {
        imageResource: '../../images/materials/xiaohuangren/border.png',
        dx: 0,
        dy: 0,
        dWidth: 900,
        dHeight: 1200
    },
    materials: {
        eye: ["../../images/materials/xiaohuangren/eye.png"],
        row3: ["../../images/materials/xiaohuangren/row3.png"],
        row2col3: ["../../images/materials/xiaohuangren/row2col3.png"],
        row2col2: ["../../images/materials/xiaohuangren/row2col2.png", "../../images/materials/xiaohuangren/row2col2_2.png"],
        row2: ["../../images/materials/xiaohuangren/row2.png", "../../images/materials/xiaohuangren/row2_2.png"],
        single: ["../../images/materials/xiaohuangren/single.png"],
    },
    callback: () => {
        wx.canvasToTempFilePath({
            canvasId: 'myQrcodeOne',
            success: (res) => {
                console.log(res.tempFilePath)
                this.setData({
                    url: res.tempFilePath
                })
            }
        })
    }
})

5、参数说明

参数说明类型示例
width必须,二维码宽度Number520
height必须,二维码高度Number520
text必须,二维码内容Stringhttps://github.com/BWmelon/artQrcode
canvasId必须,绘制的canvasIdString'myQrcode'
x非必须,二维码x轴相对于背景图起始位置,默认为0Number100
y非必须,二维码y轴相对于背景图起始位置,默认为0Number100
correctLevel非必须,二维码纠错级别,默认值为高级,取值:{ L: 1, M: 0, Q: 3, H: 2 }Number1
_this非必须,若在组件中使用,需要传入 this
callback非必须,绘制完成后的回调函数Function() => {console.log('完成')}
image必须,背景图 image.imageResource:背景图位置 image.dx:背景图起始x轴 image.dy:背景图起始y轴 image.dWidth:背景图绘制宽度 image.dHeight:背景图绘制高度Object{ imageRosourse: '../../images/materials/border.png', dx: 0, dy: 0, dWidth: 900, dHeight: 1200 }
materials必须,素材 可选值:eye、col4、row4、row2col3、row3col2、col3、row3、row2col2、col2、row2、single 每一项的值都是一个数组,如果该项素材数量大于1,将会进行随机渲染 没有素材的选项可以不填或者为空数组Object{ eye: ["../../images/materials/xiaohuangren/eye.png"], row3:["../../images/materials/xiaohuangren/row3.png"], row2col3: ["../../images/materials/xiaohuangren/row2col3.png"], row2col2: ["../../images/materials/xiaohuangren/row2col2.png", "../../images/materials/xiaohuangren/row2col2_2.png"], row2: ["../../images/materials/xiaohuangren/row2.png", "../../images/materials/xiaohuangren/row2_2.png"], single: ["../../images/materials/xiaohuangren/single.png"] }

6、效果

艺术二维码生成效果

7、感谢

252860883/ArtQRCode

yingye/weapp-qrcode

最后修改:2021 年 03 月 07 日 05 : 07 PM
如果觉得我的文章对你有用,请随意赞赏