1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * <iframe src='../../../examples/Graphics.html?noHeader' width = '430' height = '400' scrolling='no'></iframe>
  9  * <br/>
 10  * @class Graphics类包含一组创建矢量图形的方法。
 11  * @augments View
 12  * @mixes CacheMixin
 13  * @borrows CacheMixin#cache as #cache
 14  * @borrows CacheMixin#updateCache as #updateCache
 15  * @borrows CacheMixin#setCacheDirty as #setCacheDirty
 16  * @param {Object} properties 创建对象的属性参数。可包含此类所有可写属性。
 17  * @module hilo/view/Graphics
 18  * @requires hilo/core/Hilo
 19  * @requires hilo/core/Class
 20  * @requires hilo/view/View
 21  * @requires hilo/view/CacheMixin
 22  * @property {Number} lineWidth 笔画的线条宽度。默认为1。只读属性。
 23  * @property {Number} lineAlpha 笔画的线条透明度。默认为1。只读属性。
 24  * @property {String} lineCap 笔画的线条端部样式。可选值有:butt、round、square等,默认为null。只读属性。
 25  * @property {String} lineJoin 笔画的线条连接样式。可选值有:miter、round、bevel等,默认为null。只读属性。
 26  * @property {Number} miterLimit 斜连线长度和线条宽度的最大比率。此属性仅当lineJoin为miter时有效。默认值为10。只读属性。
 27  * @property {String} strokeStyle 笔画边框的样式。默认值为'0',即黑色。只读属性。
 28  * @property {String} fillStyle 内容填充的样式。默认值为'0',即黑色。只读属性。
 29  * @property {Number} fillAlpha 内容填充的透明度。默认值为0。只读属性。
 30  */
 31 var Graphics = (function(){
 32 
 33 var canvas = document.createElement('canvas');
 34 var helpContext = canvas.getContext && canvas.getContext('2d');
 35 
 36 return Class.create(/** @lends Graphics.prototype */{
 37     Extends: View,
 38     Mixes:CacheMixin,
 39     constructor: function(properties){
 40         properties = properties || {};
 41         this.id = this.id || properties.id || Hilo.getUid('Graphics');
 42         Graphics.superclass.constructor.call(this, properties);
 43 
 44         this._actions = [];
 45     },
 46 
 47     lineWidth: 1,
 48     lineAlpha: 1,
 49     lineCap: null, //'butt', 'round', 'square'
 50     lineJoin: null, //'miter', 'round', 'bevel'
 51     miterLimit: 10,
 52     hasStroke: false,
 53     strokeStyle: '0',
 54     hasFill: false,
 55     fillStyle: '0',
 56     fillAlpha: 0,
 57 
 58     /**
 59      * 指定绘制图形的线条样式。
 60      * @param {Number} thickness 线条的粗细值。默认为1。
 61      * @param {String} lineColor 线条的CSS颜色值。默认为黑色,即'0'。
 62      * @param {Number} lineAlpha 线条的透明度值。默认为不透明,即1。
 63      * @param {String} lineCap 线条的端部样式。可选值有:butt、round、square等,默认值为null。
 64      * @param {String} lineJoin 线条的连接样式。可选值有:miter、round、bevel等,默认值为null。
 65      * @param {Number} miterLimit 斜连线长度和线条宽度的最大比率。此属性仅当lineJoin为miter时有效。默认值为10。
 66      * @returns {Graphics} Graphics对象本身。
 67      */
 68     lineStyle: function(thickness, lineColor, lineAlpha, lineCap, lineJoin, miterLimit){
 69         var me = this, addAction = me._addAction;
 70 
 71         addAction.call(me, ['lineWidth', (me.lineWidth = thickness || 1)]);
 72         addAction.call(me, ['strokeStyle', (me.strokeStyle = lineColor || '0')]);
 73         addAction.call(me, ['lineAlpha', (me.lineAlpha = lineAlpha || 1)]);
 74         if(lineCap != undefined) addAction.call(me, ['lineCap', (me.lineCap = lineCap)]);
 75         if(lineJoin != undefined) addAction.call(me, ['lineJoin', (me.lineJoin = lineJoin)]);
 76         if(miterLimit != undefined) addAction.call(me, ['miterLimit', (me.miterLimit = miterLimit)]);
 77         me.hasStroke = true;
 78         return me;
 79     },
 80 
 81     /**
 82      * 设置虚线样式
 83      * @param {Number[]} segments 一组描述交替绘制线段和间距(坐标空间单位)长度的数字
 84      * @see https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/setLineDash
 85      */
 86     setLineDash: function(segments){
 87         return this._addAction(['setLineDash', segments]);
 88     },
 89 
 90     /**
 91      * 指定绘制图形的填充样式和透明度。
 92      * @param {String} fill 填充样式。可以是color、gradient或pattern。
 93      * @param {Number} alpha 透明度。
 94      * @returns {Graphics} Graphics对象本身。
 95      */
 96     beginFill: function(fill, alpha){
 97         var me = this, addAction = me._addAction;
 98 
 99         addAction.call(me, ['fillStyle', (me.fillStyle = fill)]);
100         addAction.call(me, ['fillAlpha', (me.fillAlpha = alpha || 1)]);
101         me.hasFill = true;
102         return me;
103     },
104 
105     /**
106      * 应用并结束笔画的绘制和图形样式的填充。
107      * @returns {Graphics} Graphics对象本身。
108      */
109     endFill: function(){
110         var me = this, addAction = me._addAction;
111 
112         if(me.hasStroke) addAction.call(me, ['stroke']);
113         if(me.hasFill) addAction.call(me, ['fill']);
114         me.setCacheDirty(true);
115         return me;
116     },
117 
118     /**
119      * 指定绘制图形的线性渐变填充样式。
120      * @param {Number} x0 渐变的起始点的x轴坐标。
121      * @param {Number} y0 渐变的起始点的y轴坐标。
122      * @param {Number} x1 渐变的结束点的x轴坐标。
123      * @param {Number} y1 渐变的结束点的y轴坐标。
124      * @param {Array} colors 渐变中使用的CSS颜色值数组。
125      * @param {Array} ratios 渐变中开始与结束之间的位置数组。需与colors数组里的颜色值一一对应,介于0.0与1.0之间的值。
126      * @returns {Graphics} Graphics对象本身。
127      */
128     beginLinearGradientFill: function(x0, y0, x1, y1, colors, ratios){
129         var me = this, gradient = helpContext.createLinearGradient(x0, y0, x1, y1);
130 
131         for (var i = 0, len = colors.length; i < len; i++){
132             gradient.addColorStop(ratios[i], colors[i]);
133         }
134         me.hasFill = true;
135         return me._addAction(['fillStyle', (me.fillStyle = gradient)]);
136     },
137 
138     /**
139      * 指定绘制图形的放射性渐变填充样式。
140      * @param {Number} x0 渐变的起始圆的x轴坐标。
141      * @param {Number} y0 渐变的起始圆的y轴坐标。
142      * @param {Number} r0 渐变的起始圆的半径。
143      * @param {Number} x1 渐变的结束圆的x轴坐标。
144      * @param {Number} y1 渐变的结束圆的y轴坐标。
145      * @param {Number} r1 渐变的结束圆的半径。
146      * @param {Array} colors 渐变中使用的CSS颜色值数组。
147      * @param {Array} ratios 渐变中开始与结束之间的位置数组。需与colors数组里的颜色值一一对应,介于0.0与1.0之间的值。
148      * @returns {Graphics} Graphics对象本身。
149      */
150     beginRadialGradientFill: function(x0, y0, r0, x1, y1, r1, colors, ratios){
151         var me = this, gradient = helpContext.createRadialGradient(x0, y0, r0, x1, y1, r1);
152         for (var i = 0, len = colors.length; i < len; i++)
153         {
154             gradient.addColorStop(ratios[i], colors[i]);
155         }
156         me.hasFill = true;
157         return me._addAction(['fillStyle', (me.fillStyle = gradient)]);
158     },
159 
160     /**
161      * 开始一个位图填充样式。
162      * @param {HTMLImageElement} image 指定填充的Image对象。
163      * @param {String} repetition 指定填充的重复设置参数。它可以是以下任意一个值:repeat, repeat-x, repeat-y, no-repeat。默认为''。
164      * @returns {Graphics} Graphics对象本身。
165      */
166     beginBitmapFill: function(image, repetition){
167         var me = this, pattern = helpContext.createPattern(image, repetition || '');
168         me.hasFill = true;
169         return me._addAction(['fillStyle', (me.fillStyle = pattern)]);
170     },
171 
172     /**
173      * 开始一个新的路径。
174      * @returns {Graphics} Graphics对象本身。
175      */
176     beginPath: function(){
177         return this._addAction(['beginPath']);
178     },
179 
180     /**
181      * 关闭当前的路径。
182      * @returns {Graphics} Graphics对象本身。
183      */
184     closePath: function(){
185         return this._addAction(['closePath']);
186     },
187 
188     /**
189      * 将当前绘制位置移动到点(x, y)。
190      * @param {Number} x x轴坐标。
191      * @param {Number} y y轴坐标。
192      * @returns {Graphics} Graphics对象本身。
193      */
194     moveTo: function(x, y){
195         return this._addAction(['moveTo', x, y]);
196     },
197 
198     /**
199      * 绘制从当前位置开始到点(x, y)结束的直线。
200      * @param {Number} x x轴坐标。
201      * @param {Number} y y轴坐标。
202      * @returns {Graphics} Graphics对象本身。
203      */
204     lineTo: function(x, y){
205         return this._addAction(['lineTo', x, y]);
206     },
207 
208     /**
209      * 绘制从当前位置开始到点(x, y)结束的二次曲线。
210      * @param {Number} cpx 控制点cp的x轴坐标。
211      * @param {Number} cpy 控制点cp的y轴坐标。
212      * @param {Number} x x轴坐标。
213      * @param {Number} y y轴坐标。
214      * @returns {Graphics} Graphics对象本身。
215      */
216     quadraticCurveTo: function(cpx, cpy, x, y){
217         return this._addAction(['quadraticCurveTo', cpx, cpy, x, y]);
218     },
219 
220     /**
221      * 绘制从当前位置开始到点(x, y)结束的贝塞尔曲线。
222      * @param {Number} cp1x 控制点cp1的x轴坐标。
223      * @param {Number} cp1y 控制点cp1的y轴坐标。
224      * @param {Number} cp2x 控制点cp2的x轴坐标。
225      * @param {Number} cp2y 控制点cp2的y轴坐标。
226      * @param {Number} x x轴坐标。
227      * @param {Number} y y轴坐标。
228      * @returns {Graphics} Graphics对象本身。
229      */
230     bezierCurveTo: function(cp1x, cp1y, cp2x, cp2y, x, y){
231         return this._addAction(['bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y]);
232     },
233 
234     /**
235      * 绘制一个矩形。
236      * @param {Number} x x轴坐标。
237      * @param {Number} y y轴坐标。
238      * @param {Number} width 矩形的宽度。
239      * @param {Number} height 矩形的高度。
240      * @returns {Graphics} Graphics对象本身。
241      */
242     drawRect: function(x, y, width, height){
243         return this._addAction(['rect', x, y, width, height]);
244     },
245 
246     /**
247      * 绘制一个复杂的圆角矩形。
248      * @param {Number} x x轴坐标。
249      * @param {Number} y y轴坐标。
250      * @param {Number} width 圆角矩形的宽度。
251      * @param {Number} height 圆角矩形的高度。
252      * @param {Number} cornerTL 圆角矩形的左上圆角大小。
253      * @param {Number} cornerTR 圆角矩形的右上圆角大小。
254      * @param {Number} cornerBR 圆角矩形的右下圆角大小。
255      * @param {Number} cornerBL 圆角矩形的左下圆角大小。
256      * @returns {Graphics} Graphics对象本身。
257      */
258     drawRoundRectComplex: function(x, y, width, height, cornerTL, cornerTR, cornerBR, cornerBL){
259         var me = this, addAction = me._addAction;
260         addAction.call(me, ['moveTo', x + cornerTL, y]);
261         addAction.call(me, ['lineTo', x + width - cornerTR, y]);
262         addAction.call(me, ['arc', x + width - cornerTR, y + cornerTR, cornerTR, -Math.PI/2, 0, false]);
263         addAction.call(me, ['lineTo', x + width, y + height - cornerBR]);
264         addAction.call(me, ['arc', x + width - cornerBR, y + height - cornerBR, cornerBR, 0, Math.PI/2, false]);
265         addAction.call(me, ['lineTo', x + cornerBL, y + height]);
266         addAction.call(me, ['arc', x + cornerBL, y + height - cornerBL, cornerBL, Math.PI/2, Math.PI, false]);
267         addAction.call(me, ['lineTo', x, y + cornerTL]);
268         addAction.call(me, ['arc', x + cornerTL, y + cornerTL, cornerTL, Math.PI, Math.PI*3/2, false]);
269         return me;
270     },
271 
272     /**
273      * 绘制一个圆角矩形。
274      * @param {Number} x x轴坐标。
275      * @param {Number} y y轴坐标。
276      * @param {Number} width 圆角矩形的宽度。
277      * @param {Number} height 圆角矩形的高度。
278      * @param {Number} cornerSize 圆角矩形的圆角大小。
279      * @returns {Graphics} Graphics对象本身。
280      */
281     drawRoundRect: function(x, y, width, height, cornerSize){
282         return this.drawRoundRectComplex(x, y, width, height, cornerSize, cornerSize, cornerSize, cornerSize);
283     },
284 
285     /**
286      * 绘制一个圆。
287      * @param {Number} x x轴坐标。
288      * @param {Number} y y轴坐标。
289      * @param {Number} radius 圆的半径。
290      * @returns {Graphics} Graphics对象本身。
291      */
292     drawCircle: function(x, y, radius){
293         return this._addAction(['arc', x + radius, y + radius, radius, 0, Math.PI * 2, 0]);
294     },
295 
296     /**
297      * 绘制一个椭圆。
298      * @param {Number} x x轴坐标。
299      * @param {Number} y y轴坐标。
300      * @param {Number} width 椭圆的宽度。
301      * @param {Number} height 椭圆的高度。
302      * @returns {Graphics} Graphics对象本身。
303      */
304     drawEllipse: function(x, y, width, height){
305         var me = this;
306         if(width == height) return me.drawCircle(x, y, width);
307 
308         var addAction = me._addAction;
309         var w = width / 2, h = height / 2, C = 0.5522847498307933, cx = C * w, cy = C * h;
310         x = x + w;
311         y = y + h;
312 
313         addAction.call(me, ['moveTo', x + w, y]);
314         addAction.call(me, ['bezierCurveTo', x + w, y - cy, x + cx, y - h, x, y - h]);
315         addAction.call(me, ['bezierCurveTo', x - cx, y - h, x - w, y - cy, x - w, y]);
316         addAction.call(me, ['bezierCurveTo', x - w, y + cy, x - cx, y + h, x, y + h]);
317         addAction.call(me, ['bezierCurveTo', x + cx, y + h, x + w, y + cy, x + w, y]);
318         return me;
319     },
320 
321     /**
322      * 根据参数指定的SVG数据绘制一条路径。不支持Arcs。
323      * 代码示例:
324      * <p>var path = 'M250 150 L150 350 L350 350 Z';</p>
325      * <p>var shape = new Hilo.Graphics({width:500, height:500});</p>
326      * <p>shape.drawSVGPath(path).beginFill('#0ff').endFill();</p>
327      * @param {String} pathData 要绘制的SVG路径数据。
328      * @returns {Graphics} Graphics对象本身。
329      */
330     drawSVGPath: function(pathData){
331         var me = this, addAction = me._addAction,
332             path = pathData.replace(/,/g, ' ').replace(/-/g, ' -').split(/(?=[a-zA-Z])/);
333         addAction.call(me, ['beginPath']);
334         var currentPoint = {x:0, y:0};
335         var lastControlPoint = {x:0, y:0};
336         var lastCmd;
337         for(var i = 0, len = path.length; i < len; i++){
338             var str = path[i];
339             if(!str.length){
340                 continue;
341             }
342             var realCmd = str[0];
343             var cmd = realCmd.toUpperCase();
344             var p = this._getSVGParams(str);
345             var useRelative = cmd !== realCmd;
346 
347             switch(cmd){
348                 case 'M':
349                     if(useRelative){
350                         this._convertToAbsolute(currentPoint, p);
351                     }
352                     addAction.call(me, ['moveTo', p[0], p[1]]);
353                     this._setCurrentPoint(currentPoint, p[0], p[1]);
354                     break;
355                 case 'L':
356                     if(useRelative){
357                         this._convertToAbsolute(currentPoint, p);
358                     }
359                     addAction.call(me, ['lineTo', p[0], p[1]]);
360                     this._setCurrentPoint(currentPoint, p[0], p[1]);
361                     break;
362                 case 'H':
363                     if(useRelative){
364                         p[0] += currentPoint.x;
365                     }
366                     addAction.call(me, ['lineTo', p[0], currentPoint.y]);
367                     currentPoint.x = p[0];
368                     break;
369                 case 'V':
370                     if(useRelative){
371                         p[0] += currentPoint.y;
372                     }
373                     addAction.call(me, ['lineTo', currentPoint.x, p[0]]);
374                     currentPoint.y = p[0];
375                     break;
376                 case 'Z':
377                     addAction.call(me, ['closePath']);
378                     break;
379                 case 'C':
380                     if(useRelative){
381                         this._convertToAbsolute(currentPoint, p);
382                     }
383                     addAction.call(me, ['bezierCurveTo', p[0], p[1], p[2], p[3], p[4], p[5]]);
384                     lastControlPoint.x = p[2];
385                     lastControlPoint.y = p[3];
386                     this._setCurrentPoint(currentPoint, p[4], p[5]);
387                     break;
388                 case 'S':
389                     if(useRelative){
390                         this._convertToAbsolute(currentPoint, p);
391                     }
392                     if(lastCmd === 'C' || lastCmd === 'S'){
393                         controlPoint = this._getReflectionPoint(currentPoint, lastControlPoint);
394                     }
395                     else{
396                         controlPoint = currentPoint;
397                     }
398                     addAction.call(me, ['bezierCurveTo', controlPoint.x, controlPoint.y, p[0], p[1], p[2], p[3]]);
399                     lastControlPoint.x = p[0];
400                     lastControlPoint.y = p[1];
401                     this._setCurrentPoint(currentPoint, p[2], p[3]);
402                     break;
403                 case 'Q':
404                     if(useRelative){
405                         this._convertToAbsolute(currentPoint, p);
406                     }
407                     addAction.call(me, ['quadraticCurveTo', p[0], p[1], p[2], p[3]]);
408                     lastControlPoint.x = p[0];
409                     lastControlPoint.y = p[1];
410                     this._setCurrentPoint(currentPoint, p[2], p[3]);
411                     break;
412                 case 'T':
413                     if(useRelative){
414                         this._convertToAbsolute(currentPoint, p);
415                     }
416                     var controlPoint;
417                     if(lastCmd === 'Q' || lastCmd === 'T'){
418                         controlPoint = this._getReflectionPoint(currentPoint, lastControlPoint);
419                     }
420                     else{
421                         controlPoint = currentPoint;
422                     }
423                     addAction.call(me, ['quadraticCurveTo', controlPoint.x, controlPoint.y, p[0], p[1]]);
424                     lastControlPoint = controlPoint;
425                     this._setCurrentPoint(currentPoint, p[0], p[1]);
426                     break;                
427             }
428             lastCmd = cmd;
429             
430         }
431         return me;
432     },
433     _getSVGParams:function(str){
434         var p = str.substring(1).replace(/[\s]+$|^[\s]+/g, '').split(/[\s]+/);
435         if(p[0].length == 0) {
436             p.shift();
437         }
438         for(var i = 0, l = p.length;i < l;i ++){
439             p[i] = parseFloat(p[i]);
440         }
441         return p;
442     },
443     _convertToAbsolute:function(currentPoint, data){
444         for(var i = 0, l = data.length;i < l;i ++){
445             if(i%2 === 0){
446                 data[i] += currentPoint.x;
447             }
448             else{
449                 data[i] += currentPoint.y;
450             }
451         }
452     },
453     _setCurrentPoint:function(currentPoint, x, y){
454         currentPoint.x = x;
455         currentPoint.y = y;
456     },
457     _getReflectionPoint:function(centerPoint, point){
458         return {
459             x:centerPoint.x * 2 - point.x,
460             y:centerPoint.y * 2 - point.y
461         };
462     },
463 
464     /**
465      * 执行全部绘制动作。内部私有方法。
466      * @private
467      */
468     _draw: function(context){
469         var me = this, actions = me._actions, len = actions.length, i;
470 
471         context.beginPath();
472         for(i = 0; i < len; i++){
473             var action = actions[i],
474                 f = action[0],
475                 args = action.length > 1 ? action.slice(1) : null;
476 
477             if(typeof(context[f]) == 'function') context[f].apply(context, args);
478             else context[f] = action[1];
479         }
480     },
481 
482     /**
483      * 重写渲染实现。
484      * @private
485      */
486     render: function(renderer, delta){
487         var me = this;
488         if(renderer.renderType === 'canvas'){
489             me._draw(renderer.context);
490         }else{
491             me.cache();
492             renderer.draw(me);
493         }
494     },
495 
496     /**
497      * 清除所有绘制动作并复原所有初始状态。
498      * @returns {Graphics} Graphics对象本身。
499      */
500     clear: function(){
501         var me = this;
502 
503         me._actions.length = 0;
504         me.lineWidth = 1;
505         me.lineAlpha = 1;
506         me.lineCap = null;
507         me.lineJoin = null;
508         me.miterLimit = 10;
509         me.hasStroke = false;
510         me.strokeStyle = '0';
511         me.hasFill = false;
512         me.fillStyle = '0';
513         me.fillAlpha = 1;
514 
515         me.setCacheDirty(true);
516         return me;
517     },
518 
519     /**
520      * 添加一个绘制动作。内部私有方法。
521      * @private
522      */
523     _addAction: function(action){
524         var me = this;
525         me._actions.push(action);
526         return me;
527     }
528 
529 });
530 
531 })();
532