tuy

canvas 的伪蒙版开发刮刮卡js控件

一:前言

上周做完新的手机HTML5项目,产品经理提的一个新功能比较不错。大致是在手机端模拟刮刮卡效果,让用户刮开涂层,然后根据底部预先放置的中奖图片(后台随机传回不同的中奖名次,再通过名次字段调用请求对应图片),刮开暴露中奖信息,本来是一张简单的奖品查看页面,通过刮刮卡形式,立马有趣了起来。

在早期自己写js控件时,通常拿到需求直接开工,导致最后写出来比较杂乱,而且功能点混淆,耦合度高。我深深记得李栋导师曾经叮嘱我,写控件不要追求代码多么晦涩深奥,一定要写的简明扼要,能描述清楚的功能才是好功能。随着自己在前端道路上的三年摸爬打滚,深刻意识到编写可维护的JavaScript是多么重要,从浑浑噩噩的js代码规范中拨开云雾见青天,这里推荐大家阅读《javascript语言精粹》、《javascript模式》,两本书都很薄很强大。说了这么多题外话,咱们开工吧~

二:思绪整理

刮刮卡结构:一般是三层结构,最下面一层是中奖信息,中间则是涂层,一般以灰色居多,最上面一层是放置提示语,比如'刮开有奖'、'刮我吧'等,提示语省略亦可。

HTML DOM结构:上面的刮刮卡实物是三层,但是拿到DOM里面我只分配给它两层,灰色涂层跟提示语统一使用画布去绘制,即一层img图像元素,一层canvas画布元素。

控件对外属性:采用构造函数加原型链的方式去书写控件,实例需要暴露出两个方法,a:把涂层全部刮开;b:把涂层全部盖上重新开始刮。通过这两个方法,方便对涂层进行后期操作。

三:代码拆分

首先通过命名空间写一个构造函数:

  window.TUY = window.TUY || {};
  
  TUY.Canvas_blow  = function(config){
	   this.target = config.target;  //选择器,约定传入原生DOM对象
	   this.txt = config.txt;  //提示语
	   this.condition = config.condition; //刮到多少的时候触发回调函数 默认是一半的时候
	   this.callback = config.callback; //回调函数
	   this.run();
  };

然后就是为构造函数TUY.Canvas_blow添加原型方法了,先添加画布的预设方法,在前面的微信变灰文章中已经涉及了canvas的一些基本操作,这里我们就跳过画布的基本操作,大致就是先把this.txt提示语绘制到canvas,然后在提示语的下方填充画布等比大小的灰色涂层,此处用到一个关键属性context.globalCompositeOperation,也是这款刮刮卡控件的最最最重要技术突破点,当我们把context.globalCompositeOperation属性值设置为'destination-out'时,更多属性值画布就像是photoshop制图软件里面的蒙版,什么,您不清楚ps里面的蒙版?好吧,我举个栗子:假设我们有一张照片,照片上面铺满了一层沙子。我们用手拨开一处沙子就能看见此处下面的照片,拨开越多,照片呈现面积也越大,这层沙子就饰演了蒙版的角色。在我们这个刮刮卡中,蒙版您就可以看做是那层灰色的涂层,我们在灰色涂层上面刮了一个圆圈,相当于在蒙版上面绘制了圆,然后我们就能透过这个圆看到下面的底图了,你圆刮的越大,我们看到的底图面积越大。蒙版上的绘制,等于底图上的展示。

		initCanvas: function(){ 
				var cvs = this.cvs;
			    var txt = this.txt; 
				var context = cvs.getContext('2d');
                this.clearCanvas();
				this.tempFn = this.callback;
				context.globalCompositeOperation = 'source-over';
				context.fillStyle="#000000"; 
                context.font="30px 微软雅黑";
				context.textAlign="center"; 
				context.fillText(txt,cvs.width/2,cvs.height/2);
				context.globalCompositeOperation = 'destination-over';			
				context.fillStyle='#9f9d9e';
				context.fillRect(0, 0, cvs.width, cvs.height);
				context.globalCompositeOperation = 'destination-out'; //整个插件最最关键的就是这一步了,类似于ps里面的蒙版功能,即我们在画布上画出来的图案都会让下面的image透出来
				context.lineJoin = "round";
				context.lineWidth = 15;
		}

初始化DOM结构,主要是拷贝img的位置跟尺寸信息给画布,在它的正上方创建一个等比大小的canvas元素:

		initDom : function () {
			this.cvs = document.createElement('canvas');
			var img = this.target;
			var cvs = this.cvs;
			var txt = this.txt; 
			var that = this;
			if(img.complete || img.readyState == 'loading' || img.readyState == 'complete'){
				setCanvas();
			}
			else{
				img.onload=setCanvas;
			}
		    function setCanvas(){
				cvs.style.position='absolute';
				cvs.style.left=img.offsetLeft+'px';
				cvs.style.top=img.offsetTop+'px';
				cvs.width=img.width;
				cvs.height=img.height;
				img.parentNode.insertBefore(cvs,img);
                that.initCanvas()
			}
        }


初始化事件,此处主要定义用户在刮卡过程中用到的所有事件,start、move、end大致可分为这三个,如果经常做pc前端的同学可能对touch事件相对陌生一些。在手机端touch事件使用场景就非常多了,趁着这个初始化事件,我自己也来回顾整理下touch事件。

touch事件可以分为单点触摸和多点触摸两种,单点触摸高端机一般都支持,Safari2.0、Android3.0以上的版本支持多点触摸,支持最多5个手指同时触摸屏幕,ipad最多支持11个手指同时触摸屏幕,当用户按下手指在屏幕上,ontouchstart会被触发,当用户移动一个或多个手指的时候,ontouchmove会被触发,当用户移走手指, ontouchend被触发。那什么时候触发ontouchcancel呢?当一些更高级别的事件发生的时候,例如同学打电话给你叫你五人黑,或者js程序触发了alert中断,这些都会取消当前的touch操作,即触发ontouchcancel。当你在开发一个web game的时候,ontouchcancel 对你很重要,你可以在ontouchcancel触发的时候暂停游戏或者保存游戏。

当然我们也需要访问事件对象的一系列的属性:targetTouches 目标元素的所有当前触摸; changedTouches 页面上最新更改的所有触摸; touches 页面上的所有触摸。

changedTouches、targetTouches和touches分别包含稍微不同的触摸列表。targetTouches和touches分别包含当前位于 屏幕上的手指列表,但changedTouches仅列出最后发生的触摸。如果你在使用touchend或事件,changedTouches就显得非常重要,因为此时屏幕上都不会再出现手指,因此targetTouches和touches应该为空,但你仍然可以通过查看 changedTouches数组来了解最后状态,比如下面代码里的e=e.changedTouches[e.changedTouches.length-1],把事件对象直接设置为e.changedTouches数组的最后一个元素:

initEvents: function () {
		    var cvs = this.cvs;
			var context = cvs.getContext('2d');
			var that = this;
            var offsetParent=cvs,offsetLeft=0,offsetTop=0;
            var x,y;
            var start='mousedown',move='mousemove',end='mouseup';
            if(document.createTouch){
                start="touchstart";
                move="touchmove";
                end="touchend";
            }
            cvs.addEventListener(start,onTouchStart);
            
            
            function onTouchStart(e){
                e.preventDefault();
                if(e.changedTouches){
                    e=e.changedTouches[e.changedTouches.length-1];
                }
                x=e.pageX - offsetLeft;
                y=e.pageY - offsetTop;
                context.beginPath();
                context.arc(x, y, 35/2, 0, Math.PI*2, true);
                context.closePath();
                context.fill();
				document.addEventListener(end,onTouchEnd);
                cvs.addEventListener(move,onTouch)

            }

            function onTouch(e){
                if(e.changedTouches){
                    e=e.changedTouches[e.changedTouches.length-1];
                }
				console.log(e.pageX+'@@'+e.pageY);
                context.beginPath();
                context.moveTo(x, y);
                context.lineTo(e.pageX - offsetLeft, e.pageY- offsetTop);
                x=e.pageX - offsetLeft;y=e.pageY - offsetTop;
                context.closePath();
                context.stroke();

            }

            function onTouchEnd(){
                cvs.removeEventListener(move,onTouch);
                onEnd();
            }
			
            function onEnd(){
                var st=+new Date();
                data=context.getImageData(0,0,cvs.width,cvs.height).data;
                var length=data.length,k=0;
                for(var i=0;i<length-3;i+=4){
                    if(data[i]==0&&data[i+1]==0&&data[i+2]==0&&data[i+3]==0){
                        k++;
                    }
                }
                var f=k*100/(cvs.width*cvs.height);
				that.tempFn = that.callback;
                if(f>(that.condition||50)){
									if( that.tempFn){
										that.tempFn.call();
										that.tempFn = null; //调用一次之后把函数引用置为null,避免重复触发
									}				
                }
                var t=+new Date()-st;
                console.log('您刮开了区域:'+f.toFixed(2)+'% 用了'+ t+'ms ');
                data=null;
            }
		}

清空画布:

		clearCanvas: function(){
                this.cvs.getContext('2d').clearRect(0, 0, this.cvs.width, this.cvs.height); 
		}

四:代码整合

大功告成,控件全部代码如下:

;
/**
 * 刮刮卡js构造函数插件
 * @param config.target <Object> 原生的DOM选择器,约定传入目标图片元素
 * @param config.txt <String> 刮刮卡的文字
 * @param config.condition <Number> 数字类型,约定刮到多少百分比的时候触发回调函数
 * @param config.callback <Function> 回调函数
 * 
 * @author xudihui
 * @date 2015.08.01 
 */

  window.TUY = window.TUY || {};
  
  TUY.Canvas_blow  = function(config){
	   this.target = config.target;  //选择器,约定传入原生DOM对象
	   this.txt = config.txt;  //刮刮卡的文字
	   this.condition = config.condition; //刮到多少的时候触发回调函数 默认是一半的时候
	   this.callback = config.callback; //回调函数
	   this.run();
  };
  
  TUY.Canvas_blow.prototype = {
		// 启动
		run : function () {
			// 生成dom元素
			this.initDom ();
			// 绑定事件
			this.initEvents ();
		},
		
		//初始化DOM 最好确保只执行一次
		initDom : function () {
			this.cvs = document.createElement('canvas');
			var img = this.target;
			var cvs = this.cvs;
			var txt = this.txt; 
			var that = this;
			if(img.complete || img.readyState == 'loading' || img.readyState == 'complete'){
				setCanvas();
			}
			else{
				img.onload=setCanvas;
			}
		    function setCanvas(){
				cvs.style.position='absolute';
				cvs.style.left=img.offsetLeft+'px';
				cvs.style.top=img.offsetTop+'px';
				cvs.width=img.width;
				cvs.height=img.height;
				img.parentNode.insertBefore(cvs,img);
                that.initCanvas()
			}
        },
		
		//初始化事件
		initEvents: function () {
		    var cvs = this.cvs;
			var context = cvs.getContext('2d');
			var that = this;
            var offsetParent=cvs,offsetLeft=0,offsetTop=0;
            var x,y;
            var start='mousedown',move='mousemove',end='mouseup';
            if(document.createTouch){
                start="touchstart";
                move="touchmove";
                end="touchend";
            }
            cvs.addEventListener(start,onTouchStart);
            
            
            function onTouchStart(e){
                e.preventDefault();
                if(e.changedTouches){
                    e=e.changedTouches[e.changedTouches.length-1];
                }
                x=e.pageX - offsetLeft;
                y=e.pageY - offsetTop;
                context.beginPath();
				context.fillStyle="red";
               // context.fillRect(150,20,75,50);
                context.arc(x, y, 35/2, 0, Math.PI*2, true);
                context.closePath();
                context.fill();
				document.addEventListener(end,onTouchEnd);
                cvs.addEventListener(move,onTouch)

            }

            function onTouch(e){
                if(e.changedTouches){
                    e=e.changedTouches[e.changedTouches.length-1];
                }
				console.log(e.pageX+'@@'+e.pageY);
                context.beginPath();
                context.moveTo(x, y);
                context.lineTo(e.pageX - offsetLeft, e.pageY- offsetTop);
                x=e.pageX - offsetLeft;y=e.pageY - offsetTop;
                context.closePath();
                context.stroke();
            }

            function onTouchEnd(){
                cvs.removeEventListener(move,onTouch);
                onEnd();
            }
			
            function onEnd(){
                var st=+new Date();
                data=context.getImageData(0,0,cvs.width,cvs.height).data;
                var length=data.length,k=0;
                for(var i=0;i<length-3;i+=4){
                    if(data[i]==0&&data[i+1]==0&&data[i+2]==0&&data[i+3]==0){
                        k++;
                    }
                }
                var f=k*100/(cvs.width*cvs.height);
				that.tempFn = that.callback;
                if(f>(that.condition||50)){
									if( that.tempFn){
										that.tempFn.call();
										that.tempFn = null; //调用一次之后把函数引用置为null,避免重复触发
									}				
                }
                var t=+new Date()-st;
                console.log('您刮开了区域:'+f.toFixed(2)+'% 用了'+ t+'ms ');
                data=null;
            }
		},

		//预设画布
		initCanvas: function(){ 
				var cvs = this.cvs;
			    var txt = this.txt; 
				var context = cvs.getContext('2d');
                this.clearCanvas();
				this.tempFn = this.callback;
				context.globalCompositeOperation = 'source-over';
				context.fillStyle="#000000"; 
                context.font="30px 微软雅黑";
				context.textAlign="center"; 
				context.fillText(txt,cvs.width/2,cvs.height/2);
				context.globalCompositeOperation = 'destination-over';			
				context.fillStyle='#9f9d9e';
				context.fillRect(0, 0, cvs.width, cvs.height);
				context.globalCompositeOperation = 'destination-out'; //整个插件最最关键的就是这一步了,类似于ps里面的蒙版功能,即我们在画布上画出来的图案都会让下面的image透出来
				context.lineJoin = "round";
				context.lineWidth = 15;
		},
		
		//清空画布
		clearCanvas: function(){
                this.cvs.getContext('2d').clearRect(0, 0, this.cvs.width, this.cvs.height); 
		}		
		
  }

猛搓右侧查看DEMO:全屏演示源码演示

五:小结

控件还比较稚嫩,欢迎大家不吝指正,互相交流,我很享受分享的过程。每次自己这样全部捋一遍感觉都收获不小,刮刮卡功能很小,每一种交互,只要我们把它们拆成小小的模块,然后再一个一个去攻克,久而久之我们就能做出越来越大的功能啦!加油!

六:点击查看tuy的其它js控件


码字很辛苦,转载请注明来自tuy博客《canvas 的伪蒙版开发刮刮卡js控件》

评论

  1. 初恋 #1

    看不懂 :evil:

    回复
    2015-08-6
  2. 何时 #2

    膜拜大神

    回复
    2015-08-24