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