hello,大家好。我们继续来撸组件,这次我们一起撸个花里胡哨的组件,那就是大家来找茬的辅助工具。最初的点子来自黄轶
老师粉丝群,老师再玩找茬,然后截了个图出来,底下除了喊666
,不知道说什么,第一次感觉到了技术离生活这么近。觉得很cool
,于是自己研究了一番,终于实现了,在此分享给大家。
在此感谢liuyubobobo
老师的canvas
课程,学到很多知识点,开了眼界,原来canvas
还可以这么玩。话不多说,我们赶紧动手来实现它吧。
找茬实现步骤,之后一一详细说明:
1. 获取截图数据
2. 找到关键点
3. 对比两张图
4. 呈现到页面
1. 获取截图数据
1.1获取Ctrl + v
的图像数据
找茬拼的就是速度,所以截图之后马上Ctrl + v
就需要得到图像的数据进行比较。首先写出模板和对应的事件:
<template>
<div>
<input
@paste="pasteImgDate" // 输入框聚焦时执行ctrl + v后触发的事件
@blur="onblur"
readonly
ref="input"
placeholder="ctrl+v复制截图"
/>
<span style="color: red;">{{tips}}</span> // 提示语
</div>
</template>
export default {
data() {
return {
tips: ''
}
},
mounted() {
document.addEventListener("click", this.getFocus); // 点击空白聚焦,为了速度!
},
beforeDestroy() {
document.removeEventListener("click", this.getFocus);
},
methods: {
getFocus() {
this.$refs['input'].focus();
},
pasteImgDate(e) {
const file = e.clipboardData.items[0].getAsFile(); // 得到图像数据
if(!file) {
this.tips = "没有可以复制的数据";
return;
}
...
},
onblur() {
this.tips = ''
}
}
}
首先我们设置一个tips
变量用于提示操作中遇到的问题,然后我们监听paste
事件,对聚焦输入框按下Ctrl + v
后会触发这个事件,在这个事件对象里面就可以拿到对应截图的数据,相当于就是file
类型的input
选择了一张图片,给document
增加点击事件,点击任意的地方都可以获得输入框焦点,一切为了速度!
1.2绘制到canvas
上
然后我们把这个得到数据转成base64
画到canvas
上去:
methods: {
pasteImgDate(e) {
...
const reader = new FileReader();
reader.readAsDataURL(file); // 读取图像数据
reader.onload = e => {
const img = new Image();
img.src = e.target.result; // 得到转化后的base64格式
img.onload = () => {
const canvas = document.createElement("canvas"); // 创建一个canvas标签
const ctx = canvas.getContext("2d");
const width = img.width;
const height = img.height;
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height); // 将图片绘制到canvas标签里
}
}
}
}
既然得到图片数据就好办了,使用new FileReader
读取文件,然后转换化base64
格式,赋值给一个空的img
标签,监听它的onload
事件,最后使用drawImage
这个API
将这个图片绘制到canvas
标签上,函数里的0
,0
表示就是绘制启始的x
,y
轴位置,后面是绘制的大小。
2.找到关键点
2.1确认目标点的像素信息
这里先把这个工具的核心实现原理交代了,其实并不复杂,就是使用canvas
得到整张大图的所有像素信息,然后对比里面两张小图里每一个像素的RGB
值,以此找出不同的地方。
但每张图片的截图位置肯定是不同的,不过经过观察,我们也可以发现很多有规律的地方,两张小图是处于同一个水平线的,以及它们的高宽是相同的,它们间距是相同的,最后它们周围的背景是相同的。所以我们现在的第一步就是要知道左边小图的左上角在哪里。于是我截了张图放到了ps
里面,并将它的左上角放到了最大:
通过吸管取值,发现和图片相接触的几个点它们的RGB
值都是80, 148, 176
,好嘞,找到了,我们从大图的x
轴开始一行行的找,第一个点肯定就是这里了。接下来编写之后的代码:
methods: {
pasteImgDate(e) {
...
ctx.drawImage(img, 0, 0, width, height); //之前代码
const imgData = ctx.getImageData(0, 0, width, height);
const pixelData = imgData.data;
}
}
2.2通过遍历根据条件找
之前使用drawImage
把图片画到了canvas
里,现在我们通过getImageData
读取这个canvas
里面的数据,里面的像素值就保存在data
属性里面。它们的排列是一个一维数组,放一张liuyubobobo老师canvas
课程里的截图:
这里解释下,从canvas
里读取的像素值是一维数组排列的,每四个值表示一个像素的RGBA
。所以这里就会有两种遍历这个数组的方式:
第一种就是单循环的顺序遍历,从第一个节点开始依次挨个遍历到最后一个像素点,就像这样:
for(let i = 0; width * height; i++) {
const r = pixelData[4 * i + 0]; // i像素的r通道值
const g = pixelData[4 * i + 1]; // i像素的g通道值
const b = pixelData[4 * i + 2]; // i像素的b通道值
}
第二种就是双循环的顺序遍历,可知道遍历到了某行某列,就像这样:
for(let y = 0; y < height; y++) {
for(let x = 0; x < width; x++) {
const p = y * width + x; // 得到y列x行
const r = pixelData[p * 4 + 0]; // y列x行像素的r通道值
const g = pixelData[p * 4 + 1]; // y列x行像素的g通道值
const b = pixelData[p * 4 + 2]; // y列x行像素的b通道值
}
}
这里如果改变某一个像素的RGB
值,然后将改变后的数组重新放到canvas
里,就生成了一张新的图像,知道这个后,就可以实现非常多有意思的滤镜。接下来我们使用第二种循环的方式,找出那个关键点来:
export default {
created() {
this.imgPos = {}; // 记录找到点的位置
},
methods: {
handleChenge(e) {
...
for (let y = 0; y < 200; y++) { // 设置200的目的为缩小范围检索
for (let x = 0; x < 200; x++) {
function rgbAddUp(pos) {
return (
pixelData[4 * pos + 0] +
pixelData[4 * pos + 1] +
pixelData[4 * pos + 2] ===
404 // 80 + 148 + 176 404? 是不是故意的
);
}
const p = rgbAddUp(y * img.width + x); // 当前点
const top = rgbAddUp((y - 1) * img.width + x); // 上面的点
const right = rgbAddUp(y * img.width + x + 1); // 右边的点
const bottom = rgbAddUp((y + 1) * img.width + x); // 下面的点
const left = rgbAddUp(y * img.width + x - 1); // 左边的点
const rightTop = rgbAddUp((y - 1) * img.width + x + 1); // 右上的点
const leftBottom = rgbAddUp((y + 1) * img.width + x - 1); // 坐下的点
if (
p &&
top &&
left &&
bottom &&
rightTop &&
leftBottom &&
!right
) {
if (!this.imgPos.y && !this.imgPos.x) {
this.imgPos.y = y;
this.imgPos.x = x;
break;
}
}
}
if (this.imgPos.y && this.imgPos.x) {
break;
}
}
}
}
}
这里为什么设置200
是为了缩小遍历的范围,毕竟像素点太多,避免页面出现停顿,所以就要求截图会有点要求,只会遍历截图左上角200
像素范围。
之前说明了,遍历的y
和x
就分别表示的是当前的列和行交叉的点,所以我们可以得到这个点它周围点的像素信息,如果它周围的点的RGB
通道相加等于404
,也就是图片中标记的那几个点,且右边不是的,这样的x
和y
就是我们想要的,找到后,退出循环。
3. 对比两张图
3.1 找到符合的点
这个时候找到关键点了,接下来提供几个ps
测量到的固定数据给到大家。小图的高是286
,宽是381
,第一张最左边到第二张最左边距离是457
。有了关键点,有了这些固定的值,我们就可以同时遍历两张图片的像素信息,找出它们不同地方了:
pasteImgDate(e) {
...
for (let y = this.imgPos.y; y < 286 + this.imgPos.y; y++) {
for (let x = this.imgPos.x; x < 381 + this.imgPos.x; x++) {
if (
pixelData[(y * img.width + x) * 4 + 0] + 10 >
pixelData[(y * img.width + x + 457) * 4 + 0] &&
pixelData[(y * img.width + x) * 4 + 1] + 10 >
pixelData[(y * img.width + x + 457) * 4 + 1] &&
pixelData[(y * img.width + x) * 4 + 2] + 10 >
pixelData[(y * img.width + x + 457) * 4 + 2]
) {
pixelData[(y * img.width + x + 457) * 4 + 0] = 0;
pixelData[(y * img.width + x + 457) * 4 + 1] = 0;
pixelData[(y * img.width + x + 457) * 4 + 2] = 0;
}
}
}
}
为什么不用!==
来判断两个像素点的区别,因为两张图片不是只有找茬的地方不同,通过机器去计算发现,有太多不同的地方了,都有很小的像素波动的地方,所以使用!==
并不能很准确的反映到找到的图片上。所以换个条件找,把相同点的RGB
都设置成0,也就是设置成黑色。
4. 呈现到页面
4.1 添加到canvas里
像素信息已经被修改了,现在我们将它放到canvas
标签里,然后将canvas
标签放到body
内即可。
pasteImgDate(e) {
...
if (!this.imgPos.y && !this.imgPos.x) {
this.tips = "截图不符合";
return;
}
delete this.imgPos.y; // 移除
delete this.imgPos.x;
const canDraw = document.getElementById("__canvas_diff_");
canDraw && document.body.removeChild(canDraw);
const canvas2 = document.createElement("canvas");
const ctx2 = canvas2.getContext("2d");
canvas2.id = "__canvas_diff_";
canvas2.width = width;
canvas2.height = height;
ctx2.putImageData(imgData, 0, 0, 0, 0, width, height); // 将数据放入到canvas里
document.body.appendChild(canvas2);
}
组件安装
npm i vue-gn-components
import { FindDIff } from 'vue-gn-components';
import "vue-gn-components/lib/style/index.css";
Vue.use(FindDIff)
组件调用
<template>
<find-diff />
</template>
最后
- 如果使用
qq
截图工具,请保证截图是png
格式,因为jpg
的像素会有损压缩。第一次可以先保存一张png
到本地,以后每次就记住你的选择。 - 源码所在地 >>> vue-gn-components。觉得还行,请给个
start
吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。