1 //= require "object.class"
  2 
  3 (function(global){
  4   
  5   "use strict";
  6   
  7   var fabric = global.fabric || (global.fabric = { }),
  8       min = fabric.util.array.min,
  9       max = fabric.util.array.max,
 10       extend = fabric.util.object.extend;
 11   
 12   if (fabric.Path) {
 13     fabric.warn('fabric.Path is already defined');
 14     return;
 15   }
 16   if (!fabric.Object) {
 17     fabric.warn('fabric.Path requires fabric.Object');
 18     return;
 19   }
 20   
 21   /**
 22    * @private
 23    */
 24   function getX(item) {
 25     if (item[0] === 'H') {
 26       return item[1];
 27     }
 28     return item[item.length - 2];
 29   }
 30   
 31   /**
 32    * @private
 33    */
 34   function getY(item) {
 35     if (item[0] === 'V') {
 36       return item[1];
 37     }
 38     return item[item.length - 1];
 39   }
 40   
 41   /** 
 42    * @class Path
 43    * @extends fabric.Object
 44    */
 45   fabric.Path = fabric.util.createClass(fabric.Object, /** @scope fabric.Path.prototype */ {
 46     
 47     /**
 48      * @property
 49      * @type String
 50      */
 51     type: 'path',
 52     
 53     /**
 54      * Constructor
 55      * @method initialize
 56      * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens)
 57      * @param {Object} [options] Options object
 58      */
 59     initialize: function(path, options) {
 60       options = options || { };
 61       
 62       this.setOptions(options);
 63       this._importProperties();
 64       
 65       this.originalState = { };
 66       
 67       if (!path) {
 68         throw Error('`path` argument is required');
 69       }
 70       
 71       var fromArray = Object.prototype.toString.call(path) === '[object Array]';
 72       
 73       this.path = fromArray
 74         ? path
 75         : path.match && path.match(/[a-zA-Z][^a-zA-Z]*/g);
 76         
 77       if (!this.path) return;
 78       
 79       // TODO (kangax): rewrite this idiocracy
 80       if (!fromArray) {
 81         this._initializeFromArray(options);
 82       };
 83       
 84       this.setCoords();
 85       
 86       if (options.sourcePath) {
 87         this.setSourcePath(options.sourcePath);
 88       }
 89     },
 90     
 91     /**
 92      * @private
 93      * @method _initializeFromArray
 94      */
 95     _initializeFromArray: function(options) {
 96       var isWidthSet = 'width' in options,
 97           isHeightSet = 'height' in options;
 98           
 99       this.path = this._parsePath();
100       
101       if (!isWidthSet || !isHeightSet) {
102         extend(this, this._parseDimensions());
103         if (isWidthSet) {
104           this.width = this.options.width;
105         }
106         if (isHeightSet) {
107           this.height = this.options.height;
108         }
109       }
110     },
111     
112     /**
113      * @private
114      * @method _render
115      */
116     _render: function(ctx) {
117       var current, // current instruction 
118           x = 0, // current x 
119           y = 0, // current y
120           controlX = 0, // current control point x
121           controlY = 0, // current control point y
122           tempX, 
123           tempY,
124           l = -(this.width / 2),
125           t = -(this.height / 2);
126           
127       for (var i = 0, len = this.path.length; i < len; ++i) {
128         
129         current = this.path[i];
130         
131         switch (current[0]) { // first letter
132           
133           case 'l': // lineto, relative
134             x += current[1];
135             y += current[2];
136             ctx.lineTo(x + l, y + t);
137             break;
138             
139           case 'L': // lineto, absolute
140             x = current[1];
141             y = current[2];
142             ctx.lineTo(x + l, y + t);
143             break;
144             
145           case 'h': // horizontal lineto, relative
146             x += current[1];
147             ctx.lineTo(x + l, y + t);
148             break;
149             
150           case 'H': // horizontal lineto, absolute
151             x = current[1];
152             ctx.lineTo(x + l, y + t);
153             break;
154             
155           case 'v': // vertical lineto, relative
156             y += current[1];
157             ctx.lineTo(x + l, y + t);
158             break;
159             
160           case 'V': // verical lineto, absolute
161             y = current[1];
162             ctx.lineTo(x + l, y + t);
163             break;
164             
165           case 'm': // moveTo, relative
166             x += current[1];
167             y += current[2];
168             ctx.moveTo(x + l, y + t);
169             break;
170           
171           case 'M': // moveTo, absolute
172             x = current[1];
173             y = current[2];
174             ctx.moveTo(x + l, y + t);
175             break;
176             
177           case 'c': // bezierCurveTo, relative
178             tempX = x + current[5];
179             tempY = y + current[6];
180             controlX = x + current[3];
181             controlY = y + current[4];
182             ctx.bezierCurveTo(
183               x + current[1] + l, // x1
184               y + current[2] + t, // y1
185               controlX + l, // x2
186               controlY + t, // y2
187               tempX + l,
188               tempY + t
189             );
190             x = tempX;
191             y = tempY;
192             break;
193             
194           case 'C': // bezierCurveTo, absolute
195             x = current[5];
196             y = current[6];
197             controlX = current[3];
198             controlY = current[4];
199             ctx.bezierCurveTo(
200               current[1] + l, 
201               current[2] + t, 
202               controlX + l, 
203               controlY + t, 
204               x + l, 
205               y + t
206             );
207             break;
208           
209           case 's': // shorthand cubic bezierCurveTo, relative
210             // transform to absolute x,y
211             tempX = x + current[3];
212             tempY = y + current[4];
213             // calculate reflection of previous control points            
214             controlX = 2 * x - controlX;
215             controlY = 2 * y - controlY;
216             ctx.bezierCurveTo(
217               controlX + l,
218               controlY + t,
219               x + current[1] + l,
220               y + current[2] + t,
221               tempX + l,
222               tempY + t
223             );
224             x = tempX;
225             y = tempY;
226             break;
227             
228           case 'S': // shorthand cubic bezierCurveTo, absolute
229             tempX = current[3];
230             tempY = current[4];
231             // calculate reflection of previous control points            
232             controlX = 2*x - controlX;
233             controlY = 2*y - controlY;
234             ctx.bezierCurveTo(
235               controlX + l,
236               controlY + t,
237               current[1] + l,
238               current[2] + t,
239               tempX + l,
240               tempY + t
241             );
242             x = tempX;
243             y = tempY;
244             break;
245             
246           case 'q': // quadraticCurveTo, relative
247             x += current[3];
248             y += current[4];
249             ctx.quadraticCurveTo(
250               current[1] + l, 
251               current[2] + t, 
252               x + l, 
253               y + t
254             );
255             break;
256             
257           case 'Q': // quadraticCurveTo, absolute
258             x = current[3];
259             y = current[4];
260             controlX = current[1];
261             controlY = current[2];
262             ctx.quadraticCurveTo(
263               controlX + l,
264               controlY + t,
265               x + l,
266               y + t
267             );
268             break;
269           
270           case 'T':
271             tempX = x;
272             tempY = y;
273             x = current[1];
274             y = current[2];
275             // calculate reflection of previous control points
276             controlX = -controlX + 2 * tempX;
277             controlY = -controlY + 2 * tempY;
278             ctx.quadraticCurveTo(
279               controlX + l,
280               controlY + t,
281               x + l, 
282               y + t
283             );
284             break;
285             
286           case 'a':
287             // TODO (kangax): implement arc (relative)
288             break;
289           
290           case 'A':
291             // TODO (kangax): implement arc (absolute)
292             break;
293           
294           case 'z':
295           case 'Z':
296             ctx.closePath();
297             break;
298         }
299       }
300     },
301     
302     /**
303      * Renders path on a specified context 
304      * @method render
305      * @param {CanvasRenderingContext2D} ctx context to render path on
306      * @param {Boolean} noTransform When true, context is not transformed
307      */
308     render: function(ctx, noTransform) {
309       ctx.save();
310       var m = this.transformMatrix;
311       if (m) {
312         ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
313       }
314       if (!noTransform) {
315         this.transform(ctx);
316       }
317       // ctx.globalCompositeOperation = this.fillRule;
318 
319       if (this.overlayFill) {
320         ctx.fillStyle = this.overlayFill;
321       }
322       else if (this.fill) {
323         ctx.fillStyle = this.fill;
324       }
325       
326       if (this.stroke) {
327         ctx.strokeStyle = this.stroke;
328       }
329       ctx.beginPath();
330       
331       this._render(ctx);
332       
333       if (this.fill) {
334         ctx.fill();
335       }
336       if (this.stroke) {
337         ctx.strokeStyle = this.stroke;
338         ctx.lineWidth = this.strokeWidth;
339         ctx.lineCap = ctx.lineJoin = 'round';
340         ctx.stroke();
341       }
342       if (!noTransform && this.active) {
343         this.drawBorders(ctx);
344         this.hideCorners || this.drawCorners(ctx);
345       }
346       ctx.restore();
347     },
348     
349     /**
350      * Returns string representation of an instance
351      * @method toString
352      * @return {String} string representation of an instance
353      */
354     toString: function() {
355       return '#<fabric.Path ('+ this.complexity() +'): ' + 
356         JSON.stringify({ top: this.top, left: this.left }) +'>';
357     },
358     
359     /**
360      * Returns object representation of an instance
361      * @method toObject
362      * @return {Object}
363      */
364     toObject: function() {
365       var o = extend(this.callSuper('toObject'), {
366         path: this.path
367       });
368       if (this.sourcePath) {
369         o.sourcePath = this.sourcePath;
370       }
371       if (this.transformMatrix) {
372         o.transformMatrix = this.transformMatrix;
373       }
374       return o;
375     },
376     
377     /**
378      * Returns dataless object representation of an instance
379      * @method toDatalessObject
380      * @return {Object}
381      */
382     toDatalessObject: function() {
383       var o = this.toObject();
384       if (this.sourcePath) {
385         o.path = this.sourcePath;
386       }
387       delete o.sourcePath;
388       return o;
389     },
390     
391     /**
392      * Returns number representation of an instance complexity
393      * @method complexity
394      * @return {Number} complexity
395      */
396     complexity: function() {
397       return this.path.length;
398     },
399     
400     /**
401      * @private
402      * @method _parsePath
403      */
404     _parsePath: function() {
405       
406       var result = [],
407           currentPath, 
408           chunks;
409       
410       // use plain loop for perf.
411       for (var i = 0, len = this.path.length; i < len; i++) {
412         currentPath = this.path[i];
413         chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
414         result.push([currentPath.charAt(0)].concat(chunks.map(parseFloat)));
415       }
416       return result;
417     },
418     
419     /**
420      * @method _parseDimensions
421      */
422     _parseDimensions: function() {
423       var aX = [], 
424           aY = [], 
425           previousX, 
426           previousY, 
427           isLowerCase = false, 
428           x, 
429           y;
430       
431       this.path.forEach(function(item, i) {
432         if (item[0] !== 'H') {
433           previousX = (i === 0) ? getX(item) : getX(this.path[i-1]);
434         }
435         if (item[0] !== 'V') {
436           previousY = (i === 0) ? getY(item) : getY(this.path[i-1]);
437         }
438         
439         // lowercased letter denotes relative position; 
440         // transform to absolute
441         if (item[0] === item[0].toLowerCase()) {
442           isLowerCase = true;
443         }
444         
445         // last 2 items in an array of coordinates are the actualy x/y (except H/V);
446         // collect them
447         
448         // TODO (kangax): support relative h/v commands
449             
450         x = isLowerCase
451           ? previousX + getX(item)
452           : item[0] === 'V' 
453             ? previousX 
454             : getX(item);
455             
456         y = isLowerCase
457           ? previousY + getY(item)
458           : item[0] === 'H' 
459             ? previousY 
460             : getY(item);
461         
462         var val = parseInt(x, 10);
463         if (!isNaN(val)) aX.push(val);
464         
465         val = parseInt(y, 10);
466         if (!isNaN(val)) aY.push(val);
467         
468       }, this);
469       
470       var minX = min(aX), 
471           minY = min(aY), 
472           deltaX = deltaY = 0;
473       
474       var o = {
475         top: minY - deltaY,
476         left: minX - deltaX,
477         bottom: max(aY) - deltaY,
478         right: max(aX) - deltaX
479       };
480       
481       o.width = o.right - o.left;
482       o.height = o.bottom - o.top;
483       
484       return o;
485     }
486   });
487   
488   /**
489    * Creates an instance of fabric.Path from an object
490    * @static
491    * @method fabric.Path.fromObject
492    * @return {fabric.Path} Instance of fabric.Path
493    */
494   fabric.Path.fromObject = function(object) {
495     return new fabric.Path(object.path, object);
496   };
497   
498   /**
499    * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`)
500    * @static
501    * @see http://www.w3.org/TR/SVG/paths.html#PathElement
502    */
503   fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity fill-rule stroke stroke-width transform'.split(' ');
504   
505   /**
506    * Creates an instance of fabric.Path from an SVG <path> element
507    * @static
508    * @method fabric.Path.fromElement
509    * @param {SVGElement} element to parse
510    * @param {Object} options object
511    * @return {fabric.Path} Instance of fabric.Path
512    */
513   fabric.Path.fromElement = function(element, options) {
514     var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES),
515         path = parsedAttributes.d;
516     delete parsedAttributes.d;
517     return new fabric.Path(path, extend(parsedAttributes, options));
518   };
519 })(this);