让canvas支持jQuery式的链式语法

这里的canvas是指html5 中的canvas,如有雷同,恕不解释。

文章更新:

2013.08.28:

此次更新是修复上一次更新的遗留问题,即”有返回值的函数无法得到正确的返回值”,比如getImageData,isPointInPath等,这个问题是因为为了实现链式语法,函数会总是返回this.其实要修复这个问题是很简单的,就是判断函数执行后是否有返回值,有的话就返回这个返回值,没有就继续返回this(大多数情况下都没有).

不过由于这个判断的原因,可能会对整体效率有那么一点点的影响;而且,有返回值的函数的后面,能继续链式语法.

另外我把原来的用来放函数名的数组变成了一个字符串,因为这样可以少写很多引号.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var XtendCanvas = function () {
    var pro = 'save,restore,scale,rotate,translate,transform,createLinearGradient,createRadialGradient,getLineDash,clearRect,fillRect,beginPath,closePath,moveTo,lineTo,quadraticCurveTo,bezierCurveTo,arcTo,rect,arc,fill,stroke,clip,clearShadow,fillText,strokeText,strokeRect,drawImage,drawImageFromRect,putImageData,createPattern,createImageData,textBaseLine,strokeStyle,lineWidth,globalAlpha,fillStyle,font,shadowOffsetX,shadowOffsetY,shadowBlur,shadowColor,lineCap,lineJoin,miterLimit,getImageData,isPointInPath'.split(',');
    function fn (canvas) {
        this.context = canvas.getContext('2d');
    }
    var old = document.createElement('CANVAS').getContext('2d');
    for(var i = 1,p=pro[0];p;p=pro[i++]) {
        // console.log(i +' >> '+ p + ' >> ' + typeof CanvasRenderingContext2D.prototype[p]);
        fn.prototype[p] = function  (p) {
            return (typeof old[p] === 'function') ? function  () {
                    var r = this.context[p].apply(this.context,arguments);
                    return r === undefined ? this : r;
                } : function  () {
                    this.context[p] = Array.prototype.join.call(arguments);
                    return this;
                };
        }(p);
    }  
    return function  (canvas) {
        return new fn(canvas);
    };  
}()

2013.03.19:

优化了XtendCanvas结构,数组中去掉了一些没必要存在的函数(如isPointInPath),修复了Firefox下报错的问题,并尽量减少了判断与循环的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var XtendCanvas = function () {
    var pro = ['save','restore', 'scale', 'rotate', 'translate', 'transform',  'createLinearGradient', 'createRadialGradient', 'getLineDash', 'clearRect', 'fillRect', 'beginPath', 'closePath', 'moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'arcTo', 'rect', 'arc', 'fill', 'stroke', 'clip',  'measureText', 'clearShadow', 'fillText', 'strokeText',   'strokeRect', 'drawImage', 'drawImageFromRect',  'putImageData', 'createPattern', 'createImageData', 'getImageData','textBaseLine','strokeStyle','lineWidth','globalAlpha','fillStyle','font','shadowOffsetX','shadowOffsetY','shadowBlur','shadowColor','lineCap','lineJoin','miterLimit'];
    function fn (canvas) {
        this.context = canvas.getContext('2d');
    }
    var old = document.createElement('CANVAS').getContext('2d');
    for(var i = 1,p=pro[0];p;p=pro[i++]) {
        // console.log(i +' >> '+ p + ' >> ' + typeof CanvasRenderingContext2D.prototype[p]);
        fn.prototype[p] = function  (p) {
            return (typeof old[p] === 'function') ? function  () {
                    this.context[p].apply(this.context,arguments);
                    return this;
                } : function  () {
                    this.context[p] = Array.prototype.join.call(arguments);
                    return this;
                };
        }(p);
    }  
    old = null;
    return function  (canvas) {
        return new fn(canvas);
    };
   
}()

说实话我对canvas的原生绘图语法很不满意。以下是一段很平常的canvas绘图代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ctx.arc(centerX,centerY,radius,0,PI*2,true);
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = "10";
ctx.fill();
ctx.beginPath();
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.moveTo(centerX-radius,centerY);
ctx.lineTo(centerX-radius,centerY - 50);
ctx.lineTo(centerX+radius,centerY - 50);
ctx.lineTo(centerX+radius,centerY);
// ctx.lineTo(centerX-radius,centerY);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,1)';
ctx.arc(centerX,centerY-50,radius,0,PI*2,true);
ctx.fill();

这段代码的语法有个很明显的特点,就是每一句开头都有一个ctx——这个ctx即Canvas的Context2d对象,也就是画布的画笔。

这就是我对canvas绘图最不爽的地方:既然你的每一个操作,每一个属性都是挂在ctx上面的,那么何必有每次都要写ctx呢?不觉得繁琐吗?不可以省略吗?

答案是,还真就不能省略。

如果我们能把每一行的这个ctx去掉,都能省下不少字节的代码。

有的人可能会让我用with,但众所周知with语句会造成效率问题,而且,如果我中途有其他的非ctx的操作,就不得不中断,写更多的with,就会显得不方便。

另外,canvas不支持链式语法也让我郁闷。所谓的链式语法,如果你用过jQuery,肯定会很熟悉并喜欢他:

1
$('#div1').show(300).html(p).delay(3000).slideUp(300).remove();

如果canvas支持链式语法,那我们画图时就可以这样:

1
ctx.moveTo(500,0).lineTo(500,500).strokeStyle('#f00').stroke();

多么简洁,行云流水啊。但canvas不支持。

所以我就打算修复一下这些问题,一是去掉每句开头的ctx,二是让他支持链式语法。

本来我打算让这个类从canvas的原生CanvasRenderingContext2D上面直接继承,然后改写,但不幸的是,就连chrome和firefox,对canvas的支持都不一样,各自有不少的私有函数,如果程序员企图用firefox中的某个函数套在chrome里执行,那就会报错。

所以,我没有让这个类直接从CanvasRenderingContext2D继承。

所幸的是,canvas还是有一些通用的无疑义的方法,这个类就只实现了这些通用的方法。

由于不能从CanvasRenderingContext2D继承,所以,只能手写这个方法数组了——当然,实际上我不是手写的,我遍历了CanvasRenderingContext2D的原型,然后去掉了其中的私有方法。

不过如此一来,这个类中大部分的代码字节都是这个方法数组占去了。

然后,我新建一个函数,并为函数的原型架上原生的CanvasRenderingContext2D中的方法。

此时我发现了一个问题,就是canvas.context2d中,并不是所有都是“函数”,还包括“属性”,如ctx.lineWidth = 5;类似的。

刚开始我不知所措,以为我的计划要搁浅了,后来发现解决很简单,只要判断此元素是不是一个函数就行了。是函数,就带参数执行;不是函数——那么就肯定是属性了,就把参数赋给这个属性。

完整代码如下,非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 让canvas支持链式语法,来自十年灯
~function () {
var pro = ['save','restore', 'scale', 'rotate', 'translate', 'transform', 'createLinearGradient', 'createRadialGradient', 'getLineDash', 'clearRect', 'fillRect', 'beginPath', 'closePath', 'moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'arcTo', 'rect', 'arc', 'fill', 'stroke', 'clip', 'isPointInPath', 'measureText', 'clearShadow', 'fillText', 'strokeText', 'strokeRect', 'drawImage', 'drawImageFromRect', 'putImageData', 'createPattern', 'createImageData', 'getImageData', 'lineWidth','strokeStyle','globalAlpha','fillStyle','font','shadowOffsetX','shadowOffsetY','shadowBlur','shadowColor','lineCap','lineJoin','miterLimit'];
function XtendCanvas (canvas) {

var ctx = canvas.getContext('2d'),
fn = function(){},
fnP = fn.prototype;
for(var j = 0,p=pro[0];p;p=pro[j++]) {
fn.prototype[p] = function (p) {
return function () {
var args = Array.prototype.slice.call(arguments);
// console.log(args);
if(typeof ctx[p] == 'function') {
ctx[p].apply(ctx,args);
} else {
ctx[p] = args+'';
}
return fnP;
};
}(p);
}
return new fn;
};
window.XtendCanvas = XtendCanvas;
}();

这个类就是一个函数,他接受一个参数,这个参数就是一个canvas对象。他的使用方法如下:

1
2
var cvs = document.getElementById('cvs');
var ctx = XtendCanvas(cvs);

然后,你就可以像使用普通的context2d对象来使用这个ctx了,但不同的是,你可以用链式语法了:

1
ctx.clearRect(200,y-20,20,20).fillRect(200,y,20,20).clearRect(300,y-10,30,20).fillRect(300,y+10,30,20);

你可以把所有操作都放在一句话里,你也可以随时中断,做其他的事,然后继续。

这个“类”,由于代码短,完全可以嵌入到任何js库之中,不会造成拖累。

代码中肯定有值得改进的地方,大家可以自行完善。但——吃水不忘挖井人,希望大家记得我,最重要的是思路,对吧?

最后我要说的是,这不是一个canvas的增强类,只是改进一下他的语法而已,以后我也许会发布一个canvas的增强类,但要等我学好了再说了。

1 评论

发表评论

电子邮件地址不会被公开。