二维码在现代生活中已经处在了不可替换的地位,使用二维码进行收款候着分享现在也是一种呈上升趋势的潮流。本篇文章将会带给喜爱二维码制作爱好者一份别样的惊喜,带领您们了解二维码制作和美化的底层原理,且听我娓娓道来。

解读二维码美化的实现原理插图

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”] }