一:前言
上周做完新的手机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);
}
}
复制代码
五:小结
控件还比较稚嫩,欢迎大家不吝指正,互相交流,我很享受分享的过程。每次自己这样全部捋一遍感觉都收获不小,刮刮卡功能很小,每一种交互,只要我们把它们拆成小小的模块,然后再一个一个去攻克,久而久之我们就能做出越来越大的功能啦!加油!