参考:https://www.gxlcms.com/JavaScript-237527.html

利用canvas drawImage方法把生成的黑白二维码画到canvas上,canvasGetImageData方法获得一个矩形区域所有像素点的信息,每四个一组,分别代表r,g,b和透明度。修改颜色后,利用canvasPutImageData方法,把更改过的像素信息数组重新扔回画布上。

这里用到了深度优先搜索去遍历图形,然后给码点进行对应的颜色赋值

关于深度优先搜索(DFS)

深度优先搜索是常见的图搜索方法之一。会沿着一条路径一直搜索下去,在无法搜索时,回退到刚刚访问过的节点。深度优先遍历按照深度优先搜索的方式对图进行遍历。并且每个节点只能访问一次。

深搜优先搜索的本质上就是持续搜索,遍历了所有可能的情况,必然能得到解。DFS搜索的流程是一个树的形式,每次一条路走到黑。

深度优先搜索算法步骤
  • 步骤1:初始化图中的所有节点为均未被访问。
  • 步骤2:从图中的某个节点v出发,访问v并标记其已被访问。
  • 步骤3:依次检查v的所有邻接点w,如果w未被访问,则从w出发进行深度优先遍历(递归调用,重复步骤2和3)。

代码:https://github.com/suesoft/color-qrcode

1
2
3
4
5
6
7
8
<canvas
canvas-id="secondCanvas"
:style="{
width: width + 'px',
height: height + 'px',
}"
>
</canvas>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<script>
export default {
props: {
// 必传。原二维码图片(临时路径)
codeImg: {
type: String,
default: "",
},
width: {
type: Number,
default: 200,
},
height: {
type: Number,
default: 200,
},
// 渲染的颜色
// 不传默认#333, 传单个颜色默认全部渲染, 多个颜色需要传方向,不传就默认随机
colors: {
type: Array,
default: ["#333333"],
},
// 颜色渲染方向。
// toRight: 从左往右 toBottom: 从上往下 toRightBottom: 从左上到右下
direction: {
type: String,
default: "",
},
},
data() {
return {
book: [], // 标记数组
imgD: null, // 预留给像素信息数组
};
},
created() {
this.drawCanvas();
},

methods: {
// 画图
drawCanvas() {
const that = this;
const bg = this.colorRgb("#fff"); // 忽略的背景色

this.book = [];
for (var i = 0; i < this.height; i++) {
this.book[i] = [];
for (var j = 0; j < this.width; j++) {
this.book[i][j] = 0;
}
}

// 随机colors数组的一个序号
var ranNum = (function () {
const len = that.colors.length;
return function () {
return Math.floor(Math.random() * len);
};
})();

// canvas 部分
const { width, height } = this;
const ctx = uni.createCanvasContext("secondCanvas", this);
ctx.drawImage(this.codeImg, 0, 0, width, height);
ctx.draw(
setTimeout(() => {
uni.canvasGetImageData(
{
canvasId: "secondCanvas",
x: 0,
y: 0,
width,
height,
success: (res) => {
this.imgD = res;

for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (
this.book[i][j] == 0 &&
this.checkColor(i, j, width, bg)
) {
// 没标记过 且是非背景色
this.book[i][j] = 1;

var color = that.colorRgb(this.colors[ranNum()]);
this.dfs(i, j, color, bg); // 深度优先搜索
}
}
}

setTimeout(() => {
uni.canvasPutImageData(
{
canvasId: "secondCanvas",
x: 0,
y: 0,
width: width,
height: height,
data: this.imgD.data,
success(res) {
console.log("=============2===============", res);
},
fail(err) {
console.log("=============2=====err", err);
},
},
this
);
}, 500);
},
},
this
);
}, 500)
);
},

// 深度优先搜索
async dfs(x, y, color, bg) {
// 必须执行完成数据更新后 再执行后面的递归,不然会导致死循环,堆栈溢出
await this.changeColor(x, y, color);

// 方向数组
const next = [
[0, 1], // 右
[1, 0], // 下
[0, -1], // 左
[-1, 0], // 上
];
for (var k = 0; k <= 3; k++) {
// 下一个坐标
var tx = x + next[k][0];
var ty = y + next[k][1];

// 判断越界
if (tx < 0 || tx >= this.height || ty < 0 || ty >= this.width) {
continue;
}

if (this.book[tx][ty] == 0 && this.checkColor(tx, ty, this.width, bg)) {
// 判断位置
this.book[tx][ty] = 1;
this.dfs(tx, ty, color, bg);
}
}
return;
},

// 验证该位置的像素 不是背景色为true
checkColor(i, j, width, bg) {
var x = this.calc(width, i, j);

if (
this.imgD.data[x] != bg[0] &&
this.imgD.data[x + 1] != bg[1] &&
this.imgD.data[x + 2] != bg[2]
) {
return true;
} else {
return false;
}
},

// 改变颜色值
changeColor(i, j, colorArr) {
var x = this.calc(this.width, i, j);
this.imgD.data[x] = colorArr[0];
this.imgD.data[x + 1] = colorArr[1];
this.imgD.data[x + 2] = colorArr[2];
},

// 返回对应像素点的序号
calc(width, i, j) {
if (j < 0) {
j = 0;
}
return 4 * (i * width + j);
},

// 分离颜色参数 返回一个数组
colorRgb(str) {
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
var sColor = str.toLowerCase();
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
var sColorNew = "#";
for (var i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
// 处理六位的颜色值
var sColorChange = [];
for (var i = 1; i < 7; i += 2) {
sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));
}
return sColorChange;
} else {
var sColorChange = sColor
.replace(/(rgb\()|(\))/g, "")
.split(",")
.map(function (a) {
return parseInt(a);
});
return sColorChange;
}
},
},
};
</script>

1
<canvas class="canvas" canvas-id="secondCanvas"></canvas>

生成海报二维码有时无法识别

利用weapp-qrcode生成二维码再canvas画海报图片,二维码有时无法识别
原有问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// 生成二维码
import drawQrcode from '@/utils/weapp-qrcode.js'

drawQrcode({
width: size.w,
height: size.h,
canvasId: 'mycanvas',
text: '这里是二维码链接url',
callback: () => {
setTimeout(() => {
uni.canvasToTempFilePath({
fileType: "png",
canvasId: "mycanvas",
success: (res) => {
console.log('生成的二维码本地图片', res.tempFilePath);
},
fail: function (err) {
console.log(err, "二维码图片生成失败");
},
});
}, 100)
}
})

使用weapp-qrcode-base64,将链接url存为base64图片
修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import drawQrcode from '@/utils/weapp-qrcode-base64.js'

var base64ImgData = drawQrcode.drawImg(
'这里是二维码链接url',
{
typeNumber: 4,
errorCorrectLevel: 'M',
size: 500
}
)
const templateCodeImg = await this.Base64ToImage(base64ImgData)
console.log('生成的二维码本地图片', templateCodeImg)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 将base64转成二进制保存在本地
* @param {Object} $data base64数据
*/
Base64ToImage($data) {
return new Promise((resolve, reject) => {
const fsm = wx.getFileSystemManager(); // 声明文件系统

var times = new Date().getTime(); // 随机定义路径名称
var codeImg = wx.env.USER_DATA_PATH + '/' + times + '.png';
// 将base64图片写入
fsm.writeFile({
filePath: codeImg,
data: $data.slice(22),
encoding: 'base64',
success: () => {
resolve(codeImg)
},
fail: () => {
console.log('base64存储失败')
resolve('')
}
})
})
}

微信小程序canvas绘制圆角图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 绘制圆角图片
* @param {Object} ctx
* @param {Object} r 圆角的弧度大小
* @param {Object} x 文本起始x坐标
* @param {Object} y 文本起始y坐标
* @param {Object} w 宽度
* @param {Object} h 高度
* @param {Object} img 图片
*/
drawRoundRect(ctx, r, x, y, w, h, img) {
ctx.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, x, y, w, h);
ctx.restore()
}

微信小程序canvas绘制圆角矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
*
* @param {CanvasContext} ctx canvas上下文
* @param {number} x 圆角矩形选区的左上角 x坐标
* @param {number} y 圆角矩形选区的左上角 y坐标
* @param {number} w 圆角矩形选区的宽度
* @param {number} h 圆角矩形选区的高度

// 此处有两个圆角半径是项目需要,只展示左上角 右上角的圆角或左下角 右下角的圆角
* @param {number} r 圆角的半径 左上角 右上角
* @param {number} r1 圆角的半径 左下角 右下角
*/
roundRect(ctx, x, y, w, h, r, r1) {
// 开始绘制
ctx.save() // 先保存状态 已便于画完后面再用
ctx.beginPath()
// 因为边缘描边存在锯齿,最好指定使用 transparent 填充
// 这里是使用 fill 还是 stroke都可以,二选一即可
ctx.setFillStyle('rgba(0,0,0,.3)')
// ctx.setStrokeStyle('transparent')

// 左上角
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)

// border-top
ctx.moveTo(x + r, y)
ctx.lineTo(x + w - r, y)
ctx.lineTo(x + w, y + r)
// 右上角
ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)

// border-right
ctx.lineTo(x + w, y + h - r1)
ctx.lineTo(x + w - r1, y + h)
// 右下角
ctx.arc(x + w - r1, y + h - r1, r1, 0, Math.PI * 0.5)

// border-bottom
ctx.lineTo(x + r1, y + h)
ctx.lineTo(x, y + h - r1)
// 左下角
ctx.arc(x + r1, y + h - r1, r1, Math.PI * 0.5, Math.PI)

// border-left
ctx.lineTo(x, y + r)
ctx.lineTo(x + r, y)

// 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应
ctx.fill()
// ctx.stroke()
ctx.closePath()
// 剪切
ctx.clip()
},

canvas绘制自动换行的字符串(接口返回的文本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 绘制自动换行的字符串
* @param {Object} ctx
* @param {Object} t 文本
* @param {Object} x 文本起始x坐标
* @param {Object} y 文本起始y坐标
* @param {Object} w 文本最大宽度
*/
drawText(ctx, t,x,y,w){
var tArr = t.split("\n");
var result = []

tArr.map((item ,index) => {
var chr = item.split("");
var temp = "";
var row = [];

chr.map((itm, idx) => {
if (ctx.measureText(temp).width < w ) {
;
} else{
row.push(temp);
temp = "";
}
temp += itm;
})
row.push(temp);
row.map(itm => {
result.push(itm)
})
})

result.map((itm, idx) => {
ctx.fillText(itm,x,y+(idx+1)*20);
})
}

生成的海报导出模糊

把输出图片的宽destWidth, 高destHeight设置大点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const dpr = uni.getSystemInfoSync().pixelRatio;

ctx.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath(
{
width: _imgViewWidth,
height: _imgViewHeight,
destWidth: _imgViewWidth * dpr, // 输出图片宽度
destHeight: _imgViewHeight * dpr, // 输出图片高度
canvasId: "clipCanvas",
success: (res) => {
console.log(res.tempFilePath);
},
},
this
);
}, 500)
}, this );

实现思路是使用js监听鼠标实现三个事件(鼠标按下mousedown,鼠标移动mousemove,鼠标放开mouseup),来绝对定位dom的位置

1
2
3
4
5
6
7
8
9
10
<div class="drag"
@mousedown="down"
@mousemove="move"
@mouseup="end"
@touchstart.stop="down"
@touchmove.stop="move"
@touchend.stop="end"
:style="{ top: position.y + 'px', left: position.x + 'px' }"
>
</div>

初始化默认值

1
2
3
4
5
6
7
const dx, dy; // 鼠标位置和目标dom的左上角位置 差值
...
position: {
x: document.body.clientWidth,
y: document.body.clientHeight
}
...

还需要定义一个flags来标识鼠标是否被按下,只有按下才执行mousemove里面具体方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 鼠标按下, 得到鼠标位置和目标dom的左上角位置 的差值
down (event) {
this.flags = true
const touch = event.touches ? event.touches[0] : event

// console.log('鼠标点所在位置', touch.clientX, touch.clientY)
// console.log('目标dom左上角位置', event.target.offsetTop, event.target.offsetLeft)

dx = touch.clientX - event.target.offsetLeft
dy = touch.clientY - event.target.offsetTop
},

// 鼠标移动
move (event) {
if (this.flags) {
const touch = event.touches ? event.touches[0] : event

// 定位滑块的位置
this.position.x = touch.clientX - dx
this.position.y = touch.clientY - dy

// console.log('屏幕大小', screenWidth, screenHeight)

// 限制滑块超出页面
if (this.position.x < 0) {
this.position.x = 0
} else if (this.position.x > screenWidth - touch.target.clientWidth) {
this.position.x = screenWidth - touch.target.clientWidth
}
if (this.position.y < 0) {
this.position.y = 0
} else if (screenHeight - touch.target.clientHeight - touch.clientY < 0) {
this.position.y = screenHeight - touch.target.clientHeight
}

// 阻止页面的滑动默认事件
document.addEventListener(
'touchmove',
function () {
event.preventDefault()
},
false
)
}
},

// 鼠标释放
end () {
this.flags = false
}

给标签添加contenteditable属性可以实现富文本编辑框,随意定义内容的样式

1
<div contenteditable="true"></div>

监听输入,获取内容

1
2
3
4
5
<div 
ref="editorCon"
contenteditable="true"
@input="handleInput">
</div>
1
2
3
4
5
6
handleInput (e) {
// 去除用户在输入时,主动输入的换行符
const html = this.$refs.editorCon.innerHTML.toString().replace(/<br>/g, '')
this.$emit('input', html)
}

获取光标,插入内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 获取光标,插入html
pasteHtmlAtCaret ($html) {
let sel, range
// IE9 and non-IE
if (window.getSelection) {
sel = window.getSelection()

if (sel && sel.rangeCount) range = sel.getRangeAt(0)
if (['', null, undefined].includes(range)) {
range = this.keepCursorEnd(true).getRangeAt(0)
} else {
const contentRange = document.createRange()
contentRange.selectNode(this.$refs.editorCon)

const compareStart = range.compareBoundaryPoints(
Range.START_TO_START,
contentRange
)
const compareEnd = range.compareBoundaryPoints(
Range.END_TO_END,
contentRange
)
const compare = compareStart !== -1 && compareEnd !== 1
if (!compare) range = this.keepCursorEnd(true).getRangeAt(0)
}
let input = range.createContextualFragment($html)
let lastNode = input.lastChild // 记录插入input之后的最后节点位置
range.insertNode(input)
if (lastNode) {
// 如果有最后的节点
range = range.cloneRange()
range.setStartAfter(lastNode)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
} else if (
document['selection'] &&
document['selection'].type !== 'Control'
) {
// IE < 9
document['selection'].createRange().pasteHTML($html)
}
// 解决最后一个是点击输入未传入的情况
// 去除用户在输入时,主动输入的换行符
const html = this.$refs.editorCon.innerHTML.toString().replace(/<br>/g, '')
this.$emit('input', html)
},

/**
* 将光标重新定位到内容最后
* isReturn 是否要将range实例返回
* */
keepCursorEnd ($isReturn) {
if (window.getSelection) {
// ie11 10 9 firefox safari
this.$refs.editorCon.focus()
let sel = window.getSelection() // 创建range
sel.selectAllChildren(this.$refs.editorCon) // range 选择obj下所有子内容
sel.collapseToEnd() // 光标移至最后
if ($isReturn) return sel
} else if (document['selection']) {
// ie9以下
let sel = document['selection'].createRange() // 创建选择对象
sel.moveToElementText(this.$refs.editorCon) // range定位到编辑器
sel.collapse(false) // 光标移至最后
sel.select()
if ($isReturn) return sel
}
}

粘贴时会有原本样式,不要保留的话需要消除样式

1
2
3
4
<div 
contenteditable="true"
@paste="HandlePaste">
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 粘贴时消除样式
HandlePaste (e) {
e.stopPropagation()
e.preventDefault()
let text = ''
const event = e.originalEvent || e
if (event.clipboardData && event.clipboardData.getData) {
text = event.clipboardData.getData('text/plain')
} else if (window['clipboardData'] && window['clipboardData'].getData) {
text = window['clipboardData'].getData('Text')
}

// 清除回车
text = text.replace(/\[\d+\]|\n|\r/ig, '')

// 检查浏览器是否支持指定的编辑器命令
if (document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, text)
} else {
document.execCommand('paste', false, text)
}
}

常见问题

ios下点击软键盘弹出但是无光标显示

出现的原因是:生成了默认样式: -webkit-user-select:none;(无法选中,导致出现问题)。需在该元素上添加以下css:

1
2
-webkit-user-select: text;
user-select: text;

聚焦难,ios机型需要双击或者长按才会获取到焦点

由于项目中使用了Fastclick插件导致无法聚焦。
fastclick在ios条件下ontouchend方法中若needsClick(button、select、textarea、input、label、iframe、video 或类名包含needsclick)为不可点击防止了事件冒泡(preventDefault),所以出现无法点击情况。

1.给该元素添加类needsclick,不阻止事件冒泡
1
2
3
4
<div 
class="needsclick"
contenteditable="true">
</div>
2.重写FastClick
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import fastclick from 'fastclick'
fastclick.attach(document.body)

// 用来解决fastclick在ios需要多次点击才能获取焦点的问题
var deviceIsWindowsPhone = navigator.userAgent.indexOf('Windows Phone') >= 0
var deviceIsIOS = /iP(ad|home|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone
fastclick.prototype.focus = function (targetElement) {
var length

// Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
length = targetElement.value.length
targetElement.setSelectionRange(length, length)
} else {
targetElement.focus()
}
// 新增一行:都获取焦点
targetElement.focus()
}

添加换行(contenteditable本身换行会插入div标签)

1
2
3
4
5
<div 
class="needsclick"
contenteditable="true"
@keydown="handleKeyDown">
</div>
1
2
3
4
5
6
handleKeyDown (e) {
if (e.keyCode === 13) {
document.execCommand('insertHTML', false, '\n&zwnj;')
e.preventDefault()
}
}
1
2
3
.ss-editor {
white-space: pre-wrap;
}

添加placeholder

1
2
3
4
5
<div 
class="ss-editor needsclick"
placeholder="请输入内容"
contenteditable="true">
</div>
1
2
3
4
5
6
.ss-editor {
&:empty::before {
content: attr(placeholder);
color: #cccccc;
}
}
0%