一起撸个组件库(二):带妹上分之撸个<大家来找茬>'辅助'工具

hello,大家好。我们继续来撸组件,这次我们一起撸个很cool的组件,那就是大家来找茬的辅助工具。最初的点子来自黄佚老师的铁粉群,老师再玩找茬,然后截了个图出来,底下除了喊666,不知道说什么,第一次感觉到了技术离生活这么近。觉得很cool,于是自己研究了一番,终于实现了,再此分享给大家。

组件库源码

image

在此感谢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里面,并将它的左上角放到了最大:

image

通过吸管取值,发现和图片相接触的几个点它们的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课程里的截图:

image

这里解释下,从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像素范围。

image

之前说明了,遍历的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);
  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吧, 这也是我持续更新的动力~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容