不管是作一些2d的小游戏,或者制做小图标,或者抠图都须要用到这个功能,对图片的背景进行透明化,是咱们常常须要用到的一个功能。css
一般状况下咱们都会去下载PS或者美图秀秀这样的软件去制做。html
可是我真的不想仅仅为了作个透明图像就去下载这些软件,这些软件不只体积大,要下载个半天,放在电脑上也占空间。前端
最重要的是每次我作这个事情,都须要去临时百度一下制做透明图片的方法。git
这些软件当然强大,可是功能的众多或者须要一些基础知识,每每形成了一些门槛。github
简单点说,虽然瑞士军刀很6,可是我如今只须要一把起子,我不想知道什么蒙版图层,不想在一堆什么美颜什么各类滤镜之中找半天,我就想上传个图片点两下就行了。chrome
那么能不能在线对图片进行背景透明化呢?canvas
固然是有的,下面是网址数组
http://www.aigei.com/bgremover/浏览器
你觉得我是来推荐网站的?固然不是。app
我之因此提到这个网站,是由于我之前就是用这个作一些处理的,可是真的不是很给力啊。
我并不知道它的原理,也没有看过它的代码,可是它的缺陷很明显:
有问题就解决问题呗,因而就有了今天的小玩意。
与以前的做品同样,直接将功能写在这篇博客里了,因此能够直接在博客园中使用。
使用方法:
固然按照本懒人的惯例,仍是只在chrome浏览器下实现,因此若是您用其它浏览器的话可能没法正常操做。
不过本应用的核心功能与以往同样都是能够在现代浏览器中实现的,只是须要您调一下兼容性。
若是您有闲情逸致,想研究一下的话,这是本项目的 GitHub地址,为了能方便复制进博客园,因此代码是直接写在html中的。
少说废话,如下为应用:
本应用依然只使用纯前端实现,涉及到的技术点以下:
其中技术点1,2,7在以前的一篇博客中有涉及到,因此这里就再也不赘述,不了解的能够去看一下我以前写的那篇博客:在博客园里给图片加水印(canvas + drag)
那么,接下来就让咱们看一下具体的实现吧。
经过type为file类型的input获取到文件,而后经过FileReader读取文件信息后放入到canvas中。
这是前两步所作的工做,如今咱们须要作的是点击图像(其实是canvas)获取到点击处的颜色信息。
首先咱们须要获取到原始图像的像素信息,并保存下来,这一步在图片加载时实现,部分代码以下:
var ctx = document.getElementById('target_canvas').getContext('2d'); imgDataArr = ctx.getImageData(0, 0, imgWidth, imgHeight).data;
小课堂开始:
canvas的getImageData会获取canvas中指定区域内的图像信息,返回一个ImageData对象。
ImageData对象的data属性的值是一个Uint8ClampedArray对象,而这个对象就是图像的像素信息。
Uint8ClampedArray看名字能够了解到,它是一个定型数组,里面的值都是0-255范围以内的值。
假如咱们有一个图片只有四个像素,长2px,宽2px。左上角的像素和右下角的像素为黑色,而右上角和左下角的像素为白色。
以下图:
那么这张图片会以怎样的形式存储在Uint8ClampedArray数组中呢?
首先咱们了解到白色的RGBA值为rgba(255,255,255,255),黑色的RGBA值为rgba(0,0,0,255)。
那么这张图片的分解为rgba值,分别为
rgba(0,0,0,255) rgba(255,255,255,255) rgba(255,255,255,255) rgba(0,0,0,255)
那么颜色值将会以从左到右,从上至下的方式存储到Uint8ClampedArray数组中,以下:
[0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,255]
小课堂讲解完毕,回到正题。
如今咱们已经拿到了原始图像的像素信息了,并存放在了imgDataArr这个Uint8ClampedArray数组中。
如何获取鼠标点击处的像素信息呢?代码以下:
/** * 获取图像数据中指定偏移处的颜色信息 */ function getColorInfo(imgDataArr, offsetX, offsetY) { var pos = canvasInfo.width * 4 * offsetY + offsetX * 4; return { rValue: imgDataArr[pos], gValue: imgDataArr[pos + 1], bValue: imgDataArr[pos + 2], aValue: imgDataArr[pos + 3] } } /** * 非恢复模式下,点击canvas,以点击处颜色为标准,去掉颜色色差在指定色差范围内的颜色 */ function transparetModeCanvasClick(e) { if (imgDataArr.length === 0) { return; } if (resultImgDataArr.length === 0) { resultImgDataArr = imgDataArr.slice(0) } var clickColorInfo = getColorInfo(resultImgDataArr, e.offsetX, e.offsetY) ... }
咱们会给canvas绑定回调函数为transparetModeCanvasClick的click事件,那么,在鼠标点击canvas后,咱们就能够获取到鼠标相对于canvas左上角的点击位置。
imgDataArr里面保存的是原始的图像像素信息,以后还会用到,因此这里不作处理。
那么就copy数据到当前像素信息数组resultImgDataArr中。
而后获取像素信息时须要计算像素在一维数组中的位置:
var pos = canvasInfo.width * 4 * offsetY + offsetX * 4;
根据上面的表达式获得点击的那个像素在一维数组中的位置,若是有仔细阅读以前Uint8ClampedArray存储像素信息的方式,这个表达式应该不难理解。
在获取到点击像素的颜色信息后,咱们须要去遍历整个canvas的像素信息,对于色差小于指定范围的颜色作透明化处理。代码以下:
/** * 获取图像数据指定位置颜色与指定颜色的色差 */ function getColorDiff(imgDataArr, pos, colorInfo) { var value = Math.pow(imgDataArr[pos] - colorInfo.rValue, 2) + Math.pow(imgDataArr[pos + 1] - colorInfo.gValue, 2) + Math.pow(imgDataArr[pos + 2] - colorInfo.bValue, 2); return Math.pow(value, 0.5); } /** * 设置图像数据指定位置为透明色 */ function setTransparent(imgDataArr, pos) { imgDataArr[pos] = 0; imgDataArr[pos + 1] = 0; imgDataArr[pos + 2] = 0; imgDataArr[pos + 3] = 0; } /** * 非恢复模式下,点击canvas,以点击处颜色为标准,去掉颜色色差在指定色差范围内的颜色 */ function transparetModeCanvasClick(e) { if (imgDataArr.length === 0) { return; } if (resultImgDataArr.length === 0) { resultImgDataArr = imgDataArr.slice(0) } var clickColorInfo = getColorInfo(resultImgDataArr, e.offsetX, e.offsetY) // 若是是透明颜色则不作处理 if (clickColorInfo.aValue === 0) { return; } var ctx = document.getElementById('target_canvas').getContext('2d'); for (var pos = 0, len = canvasInfo.width * canvasInfo.height * 4; pos < len; pos = pos + 4) { if (getColorDiff(resultImgDataArr, pos, clickColorInfo) < transparentConfig.colorDiff) { setTransparent(resultImgDataArr, pos); } } ctx.putImageData(new ImageData(resultImgDataArr, canvasInfo.width, canvasInfo.height), 0, 0); setCanvasImgToDownloadLink(); }
色差计算公式为将rgb三个值的颜色相减,将他们的平方和进行开方便可。
这里其实存在一个优化点:
一、一般咱们设为透明色的颜色,每次实际上都比较单一,那么在这里能够设一个临时数组存放已经比较过色差的颜色。再次要对一个颜色比较色差前,能够查看颜色是否在这个数组中,从而避免再次计算色差,所以对于大图像而言,存在不少像素点,这种大量计算能尽可能减小的话可让操做更快。
二、这里的循环也能够进行优化。倒序循环 + Duff's Device能够进行优化。
之因此在这里强调这一点,是由于在这里对大图进行消除单一颜色为透明色时,确实须要消耗更多时间。(不过我还能接受,因此暂时不理了)
至于设置图片为透明色,实际上只须要将
imgDataArr[pos + 3] = 0;
便可。
可是为了和通常声明的透明颜色保持一致,仍是将其余几个值都设为0。
将数据设为透明色的数据以后,须要调用canvas的putImageData方法将整个图像的数据设置到canvas中。
至此,咱们完成了对图像进行透明化处理的整个过程。
可是仍然不够,由于对于复杂图像而言,这种方式处理起来太过粗暴,没法作到精细化的处理。
因此接下来咱们要实现恢复模式,对于处理的很差的地方进行恢复原始图像的操做。
在恢复模式下,当咱们鼠标移动到canvas上时,鼠标显示为一个小方框,小方框内是原始图像。
当咱们点击鼠标时,小方框内的图像会从新覆盖到当前图像上,从而达到恢复原有图像的效果。
全篇下来,其实在这个地方才开始显得有趣。
最初方案(隐藏鼠标 + canvas)
在想到经过这个办法进行精细化操做以后,第一反应是隐藏鼠标,并在移动鼠标时跟随一个小的canvas,这个小canvas中显示的是原始图像。
事实上最开始也是这么作的,若是各位有兴趣的话能够参考我github上的提交方案,上面有这个方案的实现。
虽然这个方案能够实现效果,可是存在一个很明显的性能问题,操做起来会有顿卡的感受。
缘由就是在进行鼠标移动的时候,会频繁的计算图像信息,再写入到小canvas中。
虽而后来加了个防抖函数,并将移动方框放在防抖函数外,使得红色小框能够即时移动,其中的内容不会出现顿卡,可是由于防抖函数的存在,必然会有一点小小的延迟。
做为一个懒人其实我以为作到这里就能够了,由于这样我也能够用了。
然而,纠结了半天仍是改了,这种卡顿实在是蛋疼得紧。
如今的方案(隐藏鼠标 + background-image)
如今的方案是在鼠标移动时,隐藏鼠标,并在鼠标那里加一个div,div里面设置原始图像的背景图片。
在移动鼠标时,不只会对鼠标的位置进行从新计算(说是位置,实际上用的是translate,而不是top+left),还会对背景图片的位置进行从新计算,这样就能够实现一样的效果了。
经过这种方案,在个人电脑上移动红框和计算恢复图像的方式很流畅,彻底感受不到卡顿。
若是在低配电脑上有卡顿的状况,这里也能够加一个防抖函数来处理。
那么让咱们如今来看一看恢复模式下鼠标在canvas上移动时的代码:
// 根据鼠标的偏移位置获取recover_img位置 function getRecoverImgPos(e) { // 给鼠标位置+1,是为了让recover_img不会出如今鼠标下方,从而使得鼠标点击时不会点击在recover_img上 return { x: e.offsetX + 1, y: e.offsetY + 1 } } /** * 恢复模式下,鼠标在canvas上移动,呈现原先图像 */ function recoverModeCanvasMove(e) { if (imgDataArr.length === 0) { return; } var $recoverImg = $("#recover_img"); var recoverImgPos = getRecoverImgPos(e) if (recoverImgPos.x > canvasInfo.width - recoverSize || recoverImgPos.y > canvasInfo.height - recoverSize) { $recoverImg.hide(); return; } else { $recoverImg.show(); } $recoverImg.css({ transform: 'translate(' + recoverImgPos.x + 'px,' + recoverImgPos.y + 'px)', 'background-position': (-recoverImgPos.x - 1) + 'px ' + (-recoverImgPos.y - 1) + 'px' }); }
在上面的代码中咱们会根据鼠标的位置从新计算恢复图像所在的div的位置,而后判断是否触边来决定是否隐藏。
接着再来计算其位置。
这里有一个坑点在注释里面也写了,就是恢复图像实际上并无和鼠标重叠,以防止咱们在点击时点到恢复图像上而不是canvas上。
另外若是你们细心查看css样式的话,会发现两个小坑点:
而后再来看看点击恢复图像的代码:
/** * 恢复模式下,点击canvas,将点击处指定范围内图像恢复原样 */ function recoverModeCanvasClick(e) { if (imgDataArr.length === 0) { return; } var recoverImgPos = getRecoverImgPos(e); for (var i = 0, ylen = recoverSize; i < ylen; i++) { var pos = canvasInfo.width * 4 * (recoverImgPos.y + i) + recoverImgPos.x * 4; for (var j = pos, xlen = pos + recoverSize * 4; j < xlen; j++) { resultImgDataArr[j] = imgDataArr[j] } } var ctx = document.getElementById('target_canvas').getContext('2d'); ctx.putImageData(new ImageData(resultImgDataArr, canvasInfo.width, canvasInfo.height), 0, 0); setCanvasImgToDownloadLink() }
一样是先根据鼠标位置获取恢复图像的位置,而后根据偏移量去计算位置。
咱们得注意到虽然咱们的恢复图像是一块完整的相链接的区域,可是在
Uint8ClampedArray数组中的数据并非相链接的,须要咱们去计算。
将最开始咱们保存初始图像像素数组imgDataArr赋值到当前处理的图像像素数组中。
最后将处理好的像素数组以putImageData的方式放入数组便可。
这个小应用如今能够知足个人需求了,可是它依然存在不少不足与改进空间,好比:
本期分享结束,依然是一个小应用,依然是一堆你可能知道也可能不知道的小知识点。
最后例行说明,右下角的精灵球是点赞,这已是我连续几篇文章说明了,而且那么大几个字已经写明了。
由于有几个园友引用了这个精灵球,因此大家应该也造成了精灵球便是点赞的心理预期和用户习惯了吧。
若是你不当心点错了,请刷新页面,那么精灵球那里会出现取消点赞的按钮。
下次不会再解释了。