好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

最近项目刚好用到这种效果,也就是有点像刮刮卡一样,在移动设备上,把某张图片刮掉显示出另一张图片。效果图如下:

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

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

XM L/HTML Code 复制内容到剪贴板

ctx.save()    ctx.be gin Path()    ctx.arc(x2,y2,a,0,2*Math.PI);    ctx.clip()    ctx.clearRect(0,0,canvas.width,canvas.h ei ght);    ctx.re Store ();   

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

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

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

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

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

VAR   a asin  = a*Math.sin(Math.atan((y2-y1)/(x2-x1)));    var  a acos  = a*Math.cos(Math.atan((y2-y1)/(x2-x1)))    var  x3  =  x1 +asin;    var  y3  =  y1 -acos;    var  x4  =  x1 -asin;    var  y4  =  y1 +acos;    var  x5  =  x2 +asin;    var  y5  =  y2 -acos;    var  x6  =  x2 -asin;    var  y6  =  y2 +acos;   

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

var  hastouch  =  "ontouchstart"  in window?true:false,//判断 是否 为移动设备         tapstart  =  hastouch ?"touchstart":" ;m ousedown",         tapmove  =  hastouch ?"touchmove":"mousemove",         ta PE nd  =  hastouch ?"touchend":"mouseup";       canvas.addEventListener(tapstart , function(e){        e.p rev entDefault();                 x1  =  hastouch ?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;         y1  =  hastouch ?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;              //鼠标第一次点下的时候擦除一个圆形区域,同时记录第一个坐标点        ctx.save()        ctx.beginPath()        ctx.arc(x1,y1,a,0,2*Math.PI);        ctx.clip()        ctx.clearRect(0,0,canvas.width,canvas.height);        ctx.restore();                canvas.addEventListener(tapmove , tapmoveHandler);        canvas.addEventListener(tapend , function(){            canvas.removeEventListener(tapmove , tapmoveHandler);        });      //鼠标移动时触发该事件        function tapmoveHandler(e){            e. PR eventDefault()             x2  =  hastouch ?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;             y2  =  hastouch ?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;                    //获取两个点之间的剪辑区域四个端点            var  a asin  = a*Math.sin(Math.atan((y2-y1)/(x2-x1)));            var  a acos  = a*Math.cos(Math.atan((y2-y1)/(x2-x1)))            var  x3  =  x1 +asin;            var  y3  =  y1 -acos;            var  x4  =  x1 -asin;            var  y4  =  y1 +acos;            var  x5  =  x2 +asin;            var  y5  =  y2 -acos;            var  x6  =  x2 -asin;            var  y6  =  y2 +acos;                    //保证线条的连贯,所以在矩形一端画圆            ctx.save()            ctx.beginPath()            ctx.arc(x2,y2,a,0,2*Math.PI);            ctx.clip()            ctx.clearRect(0,0,canvas.width,canvas.height);            ctx.restore();                //清除矩形剪辑区域里的像素            ctx.save()            ctx.beginPath()            ctx.moveTo(x3,y3);            ctx.lineTo(x5,y5);            ctx.lineTo(x6,y6);            ctx.lineTo(x4,y4);            ctx.closePath();            ctx.clip()            ctx.clearRect(0,0,canvas.width,canvas.height);            ctx.restore();                    //记录最后坐标             x1  =  x2 ;             y1  =  y2 ;        }    })  

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

XML/HTML Code 复制内容到剪贴板

var  imgData  =  ctx .getImageData(0,0,canvas.width,canvas.height);    var  dd  =  0 ;    for(var  x = 0 ;x < imgData.width ;x+=1){        for(var  y = 0 ;y < imgData.height ;y+=1){            var  i  = (y*imgData.width + x)*4;            if(imgData.data[i+3]  >  0){                dd++            }        }    }    if(dd/(imgData.width*imgData.height) < 0.4 ){         canvas. classname  =  "noOp" ;    }  

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

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

XML/HTML Code 复制内容到剪贴板

timeout  =  setTimeout (function(){        var  imgData  =  ctx .getImageData(0,0,canvas.width,canvas.height);        var  dd  =  0 ;        for(var  x = 0 ;x < imgData.width ;x+=30){            for(var  y = 0 ;y < imgData.height ;y+=30){                var  i  = (y*imgData.width + x)*4;                if(imgData.data[i+3]  > 0){                    dd++                }            }        }        if(dd/(imgData.width*imgData.height/900) < 0.4 ){             canvas.classN am e  =  "noOp" ;        }    },100)   

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

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

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


XML/HTML Code 复制内容到剪贴板

//通过修改globalCompositeOperation来达到擦除的效果    function tapClip(){        var  hastouch  =  "ontouchstart"  in window?true:false,             tapstart  =  hastouch ?"touchstart":"mousedown",             tapmove  =  hastouch ?"touchmove":"mousemove",             tapend  =  hastouch ?"touchend":"mouseup";                canvas.addEventListener(tapstart , function(e){         clearTimeout(timeout)            e.preventDefault();                         x1  =  hastouch ?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;             y1  =  hastouch ?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;                         ctx.lineCap  =  "round" ;  //设置线条两端为圆弧             ctx.lineJoin  =  "round" ;  //设置线条转折为圆弧             ctx.lineWidth  =  a *2;               ctx.globalCompositeOperation  =  "destination-out" ;                        ctx.save();            ctx.beginPath()            ctx.arc(x1,y1,1,0,2*Math.PI);            ctx.fill();            ctx.restore();                        canvas.addEventListener(tapmove , tapmoveHandler);            canvas.addEventListener(tapend , function(){                canvas.removeEventListener(tapmove , tapmoveHandler);                            timeout  =  setTimeout (function(){                var  imgData  =  ctx .getImageData(0,0,canvas.width,canvas.height);                var  dd  =  0 ;                for(var  x = 0 ;x < imgData.width ;x+=30){                    for(var  y = 0 ;y < imgData.height ;y+=30){                        var  i  = (y*imgData.width + x)*4;                        if(imgData.data[i+3]  >  0){                            dd++                        }                    }                }                if(dd/(imgData.width*imgData.height/900) < 0.4 ){                     canvas.className  =  "noOp" ;                }           },100)            });            function tapmoveHandler(e){                e.preventDefault()                 x2  =  hastouch ?e.targetTouches[0].pageX:e.clientX-canvas.offsetLeft;                 y2  =  hastouch ?e.targetTouches[0].pageY:e.clientY-canvas.offsetTop;                                ctx.save();                ctx.moveTo(x1,y1);                ctx.lineTo(x2,y2);                ctx. stroke ();                ctx.restore()                                 x1  =  x2 ;                 y1  =  y2 ;            }        })    }   

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

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

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

总结

以上是 为你收集整理的 用HTML5 实现橡皮擦的涂抹效果的教程 全部内容,希望文章能够帮你解决 用HTML5 实现橡皮擦的涂抹效果的教程 所遇到的问题。

如果觉得 网站内容还不错, 推荐好友。

查看更多关于用HTML5 实现橡皮擦的涂抹效果的教程的详细内容...

  阅读:26次