前段时间在将网页版三合一收款码转移到小程序版的过程中遇到了一个问题,就是原来的艺术码生成库只能在网页使用,小程序中无法使用。后来在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 | 必须,二维码宽度 | Number | 520 |
height | 必须,二维码高度 | Number | 520 |
text | 必须,二维码内容 | String | https://github.com/BWmelon/artQrcode |
canvasId | 必须,绘制的canvasId | String | 'myQrcode' |
x | 非必须,二维码x轴相对于背景图起始位置,默认为0 | Number | 100 |
y | 非必须,二维码y轴相对于背景图起始位置,默认为0 | Number | 100 |
correctLevel | 非必须,二维码纠错级别,默认值为高级,取值:{ L: 1, M: 0, Q: 3, H: 2 } | Number | 1 |
_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"] } |
5 条评论
大佬为啥引入不到drawQrcode库,提示Uncaught SyntaxError: Cannot use import statement outside a module
可以代码私我看下
顶礼膜拜 ∠( ᐛ 」∠)_
1
1