用HTML5 实现橡皮擦的涂抹效果的教程


Posted in HTML / CSS onMay 11, 2015

最近项目刚好用到这种效果,也就是有点像刮刮卡一样,在移动设备上,把某张图片刮掉显示出另一张图片。效果图如下:
用HTML5 实现橡皮擦的涂抹效果的教程

DEMO请戳右:DEMO 
这种在网上还是挺常见的,本来就想直接网上找个demo套用下他的方法就行了,套用了才发现,在android上卡出翔了,因为客户要求,在android不要求特别流畅,至少要能玩,但是网上找的那个demo实在太卡,根本就是没法玩的情况。于是就想自己写一个算了,本文也就权当记录一下研究过程。

这种刮图的效果,首先想到就是用HTML5的canvas来实现,而canvas的API中,可以清除像素的就是clearRect方法,但是clearRect方法的清除区域矩形,毕竟大部分人的习惯中的橡皮擦都是圆形的,所以就引入了剪辑区域这个强大的功能,也就是clip方法。用法很简单: 

XML/HTML Code复制内容到剪贴板
  1. ctx.save()   
  2. ctx.beginPath()   
  3. ctx.arc(x2,y2,a,0,2*Math.PI);   
  4. ctx.clip()   
  5. ctx.clearRect(0,0,canvas.width,canvas.height);   
  6. ctx.restore();   

上面那段代码就实现了圆形区域的擦除,也就是先实现一个圆形路径,然后把这个路径作为剪辑区域,再清除像素就行了。有个注意点就是需要先保存绘图环境,清除完像素后要重置绘图环境,如果不重置的话以后的绘图都是会被限制在那个剪辑区域中。

擦除效果有了,现在就是写鼠标移动擦除的效果了,下面我均用鼠标来描述,因为移动端也差不多,就是把mousedown换成touchstart,mousemove换成touchmove,mouseup换成touchend、以及获取坐标点由e.clientX换成e.targetTouches[0].pageX而已。

实现鼠标移动擦除,刚开始就是想到鼠标移动时在触发的mousemove事件中对鼠标所在位置进行圆形区域擦除,写出来后发现,当鼠标移动速度很快的时候,擦除的区域就不连贯了,就会出现下面这种效果,这显然不是我们想要的橡皮擦擦除效果。
用HTML5 实现橡皮擦的涂抹效果的教程

既然所有点不连贯,那接下来要做的事就是把这些点连贯起来,如果是实现画图功能的话,就可以直接通过lineTo把两点之间连接起来再绘制,但是擦除效果中的剪辑区域要求要是闭合路径,如果是单纯的把两个点连起来就无法形成剪辑区域了。然后我就想到用计算的方法,算出两个擦除区域中的矩形四个端点坐标来实现,也就是下图中的红色矩形:
用HTML5 实现橡皮擦的涂抹效果的教程

计算方法也很简单,因为可以知道两个剪辑区域连线两个端点的坐标,又知道我们要多宽的线条,矩形的四个端点坐标就变得容易求了,所以就有了下面的代码:
XML/HTML Code复制内容到剪贴板

  1. var aasin = a*Math.sin(Math.atan((y2-y1)/(x2-x1)));   
  2. var aacos = a*Math.cos(Math.atan((y2-y1)/(x2-x1)))   
  3. var x3 = x1+asin;   
  4. var y3 = y1-acos;   
  5. var x4 = x1-asin;   
  6. var y4 = y1+acos;   
  7. var x5 = x2+asin;   
  8. var y5 = y2-acos;   
  9. var x6 = x2-asin;   
  10. var y6 = y2+acos;   

x1、y1和x2、y2就是两个端点,从而求出了四个端点的坐标。这样一来,剪辑区域就是圈加矩形,代码组织起来就是:
XML/HTML Code复制内容到剪贴板

  1. var hastouch = "ontouchstart" in window?true:false,//判断是否为移动设备   
  2.     tapstart = hastouch?"touchstart":"mousedown",   
  3.     tapmove = hastouch?"touchmove":"mousemove",   
  4.     tapend = hastouch?"touchend":"mouseup";   
  5.   
  6. canvas.addEventListener(tapstart , function(e){   
  7.     e.preventDefault();   
  8.        
  9.     x1 = hastouch?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;   
  10.     y1 = hastouch?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;   
  11.        
  12. //鼠标第一次点下的时候擦除一个圆形区域,同时记录第一个坐标点   

  13.     ctx.save()   
  14.     ctx.beginPath()   
  15.     ctx.arc(x1,y1,a,0,2*Math.PI);   
  16.     ctx.clip()   
  17.     ctx.clearRect(0,0,canvas.width,canvas.height);   
  18.     ctx.restore();   
  19.        
  20.     canvas.addEventListener(tapmove , tapmoveHandler);   
  21.     canvas.addEventListener(tapend , function(){   
  22.         canvas.removeEventListener(tapmove , tapmoveHandler);   
  23.     });   
  24. //鼠标移动时触发该事件   

  25.     function tapmoveHandler(e){   
  26.         e.preventDefault()   
  27.         x2 = hastouch?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;   
  28.         y2 = hastouch?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;   
  29.            
  30. //获取两个点之间的剪辑区域四个端点   

  31.         var aasin = a*Math.sin(Math.atan((y2-y1)/(x2-x1)));   
  32.         var aacos = a*Math.cos(Math.atan((y2-y1)/(x2-x1)))   
  33.         var x3 = x1+asin;   
  34.         var y3 = y1-acos;   
  35.         var x4 = x1-asin;   
  36.         var y4 = y1+acos;   
  37.         var x5 = x2+asin;   
  38.         var y5 = y2-acos;   
  39.         var x6 = x2-asin;   
  40.         var y6 = y2+acos;   
  41.            
  42. //保证线条的连贯,所以在矩形一端画圆   

  43.         ctx.save()   
  44.         ctx.beginPath()   
  45.         ctx.arc(x2,y2,a,0,2*Math.PI);   
  46.         ctx.clip()   
  47.         ctx.clearRect(0,0,canvas.width,canvas.height);   
  48.         ctx.restore();   
  49.        
  50. //清除矩形剪辑区域里的像素   

  51.         ctx.save()   
  52.         ctx.beginPath()   
  53.         ctx.moveTo(x3,y3);   
  54.         ctx.lineTo(x5,y5);   
  55.         ctx.lineTo(x6,y6);   
  56.         ctx.lineTo(x4,y4);   
  57.         ctx.closePath();   
  58.         ctx.clip()   
  59.         ctx.clearRect(0,0,canvas.width,canvas.height);   
  60.         ctx.restore();   
  61.            
  62. //记录最后坐标   

  63.         x1 = x2;   
  64.         y1 = y2;   
  65.     }   
  66. })  

如此一来,鼠标擦除的效果就实现了,不过还有一个要实现的点,就是大部分擦除的效果,当你擦了一定数量的像素后,就会自动把所有图片内容呈现出来,这个效果,我是用imgData来实现的。代码如下:
复制代码

XML/HTML Code复制内容到剪贴板
  1. var imgData = ctx.getImageData(0,0,canvas.width,canvas.height);   
  2. var dd = 0;   
  3. for(var x=0;x<imgData.width;x+=1){   
  4.     for(var y=0;y<imgData.height;y+=1){   
  5.         var i = (y*imgData.width + x)*4;   
  6.         if(imgData.data[i+3] > 0){   
  7.             dd++   
  8.         }   
  9.     }   
  10. }   
  11. if(dd/(imgData.width*imgData.height)<0.4){   
  12.     canvas.className = "noOp";   
  13. }  

获取到imgData,对imgData里的像素进行遍历,然后再对imgData的data数组里的rgba中的alpha进行分析,也就是分析透明度,如果像素被擦除了,那透明度就是0了,也就是把当前画布中透明度不为0的像素的数量跟画布总像素数进行比较,如果透明度不为0 的像素数比例低于40%,那说明当前画布上就以后有百分六十以上的区域被擦除了,就可以自动呈现图片了。

此处注意,我是把检查像素这段代码方法mouseup事件里面的,因为这个计算量相对来说还是不小,如果用户狂点鼠标,就会狂触发mouseup事件,也就是会疯狂的触发那个循环计算像素,计算量大到阻塞进程,导致界面卡住的情况,缓解办法如下:加个timeout,延迟执行像素计算,而在每一次点击的时候再清除timeout,也就是如果用户点击很快,这个计算也就触发不了了,还有一个提升的办法就是抽样检查,我上面的写法是逐个像素检查,逐个像素检查的话像素量太大,肯定会卡的,所以可以采用抽样检查,比如每隔30个像素检查一次,修改后的代码如下:
复制代码

XML/HTML Code复制内容到剪贴板
  1. timeout = setTimeout(function(){   
  2.     var imgData = ctx.getImageData(0,0,canvas.width,canvas.height);   
  3.     var dd = 0;   
  4.     for(var x=0;x<imgData.width;x+=30){   
  5.         for(var y=0;y<imgData.height;y+=30){   
  6.             var i = (y*imgData.width + x)*4;   
  7.             if(imgData.data[i+3] >0){   
  8.                 dd++   
  9.             }   
  10.         }   
  11.     }   
  12.     if(dd/(imgData.width*imgData.height/900)<0.4){   
  13.         canvas.className = "noOp";   
  14.     }   
  15. },100)   

这样就可以较大限度的防止用户狂点击了,如果有其他更好的检查方法欢迎给出意见,谢谢。

到了这一步就都写完了,然后就是测试的时候了,结果并不乐观,在android上还是卡啊卡啊,所以又得另想办法,最终发现了绘图环境中的globalCompositeOperation这个属性,这个属性的默认值是source-over,也就是,当你在已有像素上进行绘图时会叠加,但是还有一个属性是destination-out,官方解释就是:在源图像外显示目标图像。只有源图像外的目标图像部分才会被显示,源图像是透明的。好像不太好理解,但是其实自己测试一下就会发现很简单,也就是在已有像素的基础上进行绘图时,你绘制的区域里的已有像素都会被置为透明,直接看张图更容易理解:
用HTML5 实现橡皮擦的涂抹效果的教程

globalCompositeOperation属性效果图解。
有了这个属性后,就意味着不需要用到clip,也就不需要用sin、cos什么的计算剪辑区域,直接用条粗线就行了,这样一来就能够很大限度的降低了计算量,同时减少了绘图环境API的调用,性能提升了,在android上运行应该也会流畅很多,下面是修改后的代码:

XML/HTML Code复制内容到剪贴板
  1. //通过修改globalCompositeOperation来达到擦除的效果   
  2. function tapClip(){   
  3.     var hastouch = "ontouchstart" in window?true:false,   
  4.         tapstart = hastouch?"touchstart":"mousedown",   
  5.         tapmove = hastouch?"touchmove":"mousemove",   
  6.         tapend = hastouch?"touchend":"mouseup";   
  7.        
  8.     canvas.addEventListener(tapstart , function(e){   
  9.  clearTimeout(timeout)   

  10.         e.preventDefault();   
  11.            
  12.         x1 = hastouch?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;   
  13.         y1 = hastouch?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;   
  14.            
  15.         ctx.lineCap = "round";

    //设置线条两端为圆弧   

  16.         ctx.lineJoin = "round";

    //设置线条转折为圆弧   

  17.         ctx.lineWidth = a*2;

      

  18.         ctx.globalCompositeOperation = "destination-out";   
  19.            
  20.         ctx.save();   
  21.         ctx.beginPath()   
  22.         ctx.arc(x1,y1,1,0,2*Math.PI);   
  23.         ctx.fill();   
  24.         ctx.restore();   
  25.            
  26.         canvas.addEventListener(tapmove , tapmoveHandler);   
  27.         canvas.addEventListener(tapend , function(){   
  28.             canvas.removeEventListener(tapmove , tapmoveHandler);   
  29.                
  30.  

    timeout = setTimeout(function(){   
  31.             var imgData = ctx.getImageData(0,0,canvas.width,canvas.height);   
  32.             var dd = 0;   
  33.             for(var x=0;x<imgData.width;x+=30){   
  34.                 for(var y=0;y<imgData.height;y+=30){   
  35.                     var i = (y*imgData.width + x)*4;   
  36.                     if(imgData.data[i+3] > 0){   
  37.                         dd++   
  38.                     }   
  39.                 }   
  40.             }   
  41.             if(dd/(imgData.width*imgData.height/900)<0.4){   
  42.                 canvas.className = "noOp";   
  43.             }   
  44.  },100)   

  45.         });   
  46.         function tapmoveHandler(e){   
  47.             e.preventDefault()   
  48.             x2 = hastouch?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;   
  49.             y2 = hastouch?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;   
  50.                
  51.             ctx.save();   
  52.             ctx.moveTo(x1,y1);   
  53.             ctx.lineTo(x2,y2);   
  54.             ctx.stroke();   
  55.             ctx.restore()   
  56.                
  57.             x1 = x2;   
  58.             y1 = y2;   
  59.         }   
  60.     })   
  61. }   

擦除那部分代码就这么一点,也就相当于画图功能,直接设置line属性后通过lineTo进行绘制线条,只要事前把globalCompositeOperation设成destination-out,你所进行的一切绘制,都变成了擦除效果。鼠标滑动触发的事件里面代码也少了很多,绘图对象的调用次数减少了,计算也减少了,性能提升大大滴。

改好代码后就立即用自己的android机子测试了一下,果然如此,跟上一个相比,流畅了很多,至少达到了客户要求的能玩的地步了。

源码地址:https://github.com/whxaxes/canvas-test/blob/gh-pages/src/Funny-demo/clip/clip.html

HTML / CSS 相关文章推荐
如何利用CSS3制作3D效果文字具体实现样式
May 02 HTML / CSS
CSS3简单实现照片墙
Dec 12 HTML / CSS
关于css兼容性问题及一些常见问题汇总
May 03 HTML / CSS
用CSS3实现无限循环的无缝滚动的示例代码
Nov 01 HTML / CSS
纯css3实现宠物小鸡实例代码
Oct 08 HTML / CSS
基于 HTML5 的 WebGL 3D 版俄罗斯方块的示例代码
May 28 HTML / CSS
利用html5 file api读取本地文件示例(如图片、PDF等)
Mar 07 HTML / CSS
突袭HTML5之Javascript API扩展5—其他扩展(应用缓存/服务端消息/桌面通知)
Jan 31 HTML / CSS
详解HTML5 Canvas绘制时指定颜色与透明度的方法
Mar 25 HTML / CSS
html5需遵循的6个设计原则
Apr 27 HTML / CSS
html5 移动端视频video的android兼容(去除播放控件、全屏)
Mar 26 HTML / CSS
如何使用 resize 实现图片切换预览功能
Aug 23 HTML / CSS
用HTML5中的Canvas结合公式绘制粒子运动的教程
May 08 #HTML / CSS
使用分层画布来优化HTML5渲染的教程
May 08 #HTML / CSS
简单介绍HTML5中的文件导入
May 08 #HTML / CSS
使用HTML5的Notification API制作web通知的教程
May 08 #HTML / CSS
利用HTML5中的Canvas绘制一张笑脸的教程
May 07 #HTML / CSS
HTML5实现获取地理位置信息并定位功能
Apr 25 #HTML / CSS
HTML5 canvas标签实现刮刮卡效果
Apr 24 #HTML / CSS
You might like
PHP的中问验证码
2006/11/25 PHP
最新用php获取谷歌PR值算法,附上php查询PR值代码示例
2011/12/25 PHP
PHP 杂谈《重构-改善既有代码的设计》之四 简化条件表达式
2012/04/09 PHP
Linux系统中设置多版本PHP共存配合Nginx服务器使用
2015/12/21 PHP
PHP线程的内存回收问题
2016/07/08 PHP
表单内同名元素的控制
2006/11/22 Javascript
Javascript YUI 读码日记之 YAHOO.util.Dom - Part.2 0
2008/03/22 Javascript
在IE下获取object(ActiveX)的Param的代码
2009/09/15 Javascript
jQuery 开天辟地入门篇一
2009/12/09 Javascript
Jquey拖拽控件Draggable使用方法(asp.net环境)
2010/09/28 Javascript
jQuery Mobile页面跳转后未加载外部JS原因分析及解决
2013/03/18 Javascript
js 输出内容到新窗口具体实现代码
2013/05/31 Javascript
jQuery制作仿腾讯web qq用户体验桌面
2013/08/20 Javascript
使用JavaScript链式编程实现模拟Jquery函数
2014/12/21 Javascript
jQuery插件jcrop+Fileapi完美实现图片上传+裁剪+预览的代码分享
2015/04/22 Javascript
实例分析nodejs模块xml2js解析xml过程中遇到的坑
2017/03/18 NodeJs
JS函数进阶之继承用法实例分析
2020/01/15 Javascript
Javascript执行流程细节原理解析
2020/05/14 Javascript
[08:17]Ti9 现场cosplay
2019/09/10 DOTA
Python 列表list使用介绍
2014/11/30 Python
Python中使用语句导入模块或包的机制研究
2015/03/30 Python
Python的Flask开发框架简单上手笔记
2015/11/16 Python
浅谈python内置变量-reversed(seq)
2017/06/21 Python
对Python 网络设备巡检脚本的实例讲解
2018/04/22 Python
使用Python通过win32 COM实现Word文档的写入与保存方法
2018/05/08 Python
Python使用pyodbc访问数据库操作方法详解
2018/07/05 Python
python爬虫之urllib,伪装,超时设置,异常处理的方法
2018/12/19 Python
python实现机器人卡牌
2019/10/06 Python
Python3 实现爬取网站下所有URL方式
2020/01/16 Python
python操作docx写入内容,并控制文本的字体颜色
2020/02/13 Python
详解python方法之绑定方法与非绑定方法
2020/08/17 Python
详解移动端HTML5音频与视频问题及解决方案
2018/08/22 HTML / CSS
html5贪吃蛇游戏使用63行代码完美实现
2013/06/25 HTML / CSS
GUESS Factory加拿大:牛仔裤、服装及配饰
2019/09/20 全球购物
护士岗位职责
2014/02/16 职场文书
协议书格式模板
2016/03/24 职场文书