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