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表示就是绘制启始的xy轴位置,后面是绘制的大小。

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像素范围。


之前说明了,遍历的yx就分别表示的是当前的列和行交叉的点,所以我们可以得到这个点它周围点的像素信息,如果它周围的点的RGB通道相加等于404,也就是图片中标记的那几个点,且右边不是的,这样的xy就是我们想要的,找到后,退出循环。

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吧。

飞越疯人院
49 声望2 粉丝

一心写代码的人儿~