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     _initializeFromArray: function(options) {
 90       var isWidthSet = 'width' in options,
 91           isHeightSet = 'height' in options;
 92           
 93       this.path = this._parsePath();
 94       
 95       if (!isWidthSet || !isHeightSet) {
 96         extend(this, this._parseDimensions());
 97         if (isWidthSet) {
 98           this.width = this.options.width;
 99         }
100         if (isHeightSet) {
101           this.height = this.options.height;
102         }
103       }
104     },
105     
106     _render: function(ctx) {
107       var current, // current instruction 
108           x = 0, // current x 
109           y = 0, // current y
110           controlX = 0, // current control point x
111           controlY = 0, // current control point y
112           tempX, 
113           tempY,
114           l = -(this.width / 2),
115           t = -(this.height / 2);
116           
117       for (var i = 0, len = this.path.length; i < len; ++i) {
118         
119         current = this.path[i];
120         
121         switch (current[0]) { // first letter
122           
123           case 'l': // lineto, relative
124             x += current[1];
125             y += current[2];
126             ctx.lineTo(x + l, y + t);
127             break;
128             
129           case 'L': // lineto, absolute
130             x = current[1];
131             y = current[2];
132             ctx.lineTo(x + l, y + t);
133             break;
134             
135           case 'h': // horizontal lineto, relative
136             x += current[1];
137             ctx.lineTo(x + l, y + t);
138             break;
139             
140           case 'H': // horizontal lineto, absolute
141             x = current[1];
142             ctx.lineTo(x + l, y + t);
143             break;
144             
145           case 'v': // vertical lineto, relative
146             y += current[1];
147             ctx.lineTo(x + l, y + t);
148             break;
149             
150           case 'V': // verical lineto, absolute
151             y = current[1];
152             ctx.lineTo(x + l, y + t);
153             break;
154             
155           case 'm': // moveTo, relative
156             x += current[1];
157             y += current[2];
158             ctx.moveTo(x + l, y + t);
159             break;
160           
161           case 'M': // moveTo, absolute
162             x = current[1];
163             y = current[2];
164             ctx.moveTo(x + l, y + t);
165             break;
166             
167           case 'c': // bezierCurveTo, relative
168             tempX = x + current[5];
169             tempY = y + current[6];
170             controlX = x + current[3];
171             controlY = y + current[4];
172             ctx.bezierCurveTo(
173               x + current[1] + l, // x1
174               y + current[2] + t, // y1
175               controlX + l, // x2
176               controlY + t, // y2
177               tempX + l,
178               tempY + t
179             );
180             x = tempX;
181             y = tempY;
182             break;
183             
184           case 'C': // bezierCurveTo, absolute
185             x = current[5];
186             y = current[6];
187             controlX = current[3];
188             controlY = current[4];
189             ctx.bezierCurveTo(
190               current[1] + l, 
191               current[2] + t, 
192               controlX + l, 
193               controlY + t, 
194               x + l, 
195               y + t
196             );
197             break;
198           
199           case 's': // shorthand cubic bezierCurveTo, relative
200             // transform to absolute x,y
201             tempX = x + current[3];
202             tempY = y + current[4];
203             // calculate reflection of previous control points            
204             controlX = 2 * x - controlX;
205             controlY = 2 * y - controlY;
206             ctx.bezierCurveTo(
207               controlX + l,
208               controlY + t,
209               x + current[1] + l,
210               y + current[2] + t,
211               tempX + l,
212               tempY + t
213             );
214             x = tempX;
215             y = tempY;
216             break;
217             
218           case 'S': // shorthand cubic bezierCurveTo, absolute
219             tempX = current[3];
220             tempY = current[4];
221             // calculate reflection of previous control points            
222             controlX = 2*x - controlX;
223             controlY = 2*y - controlY;
224             ctx.bezierCurveTo(
225               controlX + l,
226               controlY + t,
227               current[1] + l,
228               current[2] + t,
229               tempX + l,
230               tempY + t
231             );
232             x = tempX;
233             y = tempY;
234             break;
235             
236           case 'q': // quadraticCurveTo, relative
237             x += current[3];
238             y += current[4];
239             ctx.quadraticCurveTo(
240               current[1] + l, 
241               current[2] + t, 
242               x + l, 
243               y + t
244             );
245             break;
246             
247           case 'Q': // quadraticCurveTo, absolute
248             x = current[3];
249             y = current[4];
250             controlX = current[1];
251             controlY = current[2];
252             ctx.quadraticCurveTo(
253               controlX + l,
254               controlY + t,
255               x + l,
256               y + t
257             );
258             break;
259           
260           case 'T':
261             tempX = x;
262             tempY = y;
263             x = current[1];
264             y = current[2];
265             // calculate reflection of previous control points
266             controlX = -controlX + 2 * tempX;
267             controlY = -controlY + 2 * tempY;
268             ctx.quadraticCurveTo(
269               controlX + l,
270               controlY + t,
271               x + l, 
272               y + t
273             );
274             break;
275             
276           case 'a':
277             // TODO (kangax): implement arc (relative)
278             break;
279           
280           case 'A':
281             // TODO (kangax): implement arc (absolute)
282             break;
283           
284           case 'z':
285           case 'Z':
286             ctx.closePath();
287             break;
288         }
289       }
290     },
291     
292     render: function(ctx, noTransform) {
293       ctx.save();
294       var m = this.transformMatrix;
295       if (m) {
296         ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
297       }
298       if (!noTransform) {
299         this.transform(ctx);
300       }
301       // ctx.globalCompositeOperation = this.fillRule;
302 
303       if (this.overlayFill) {
304         ctx.fillStyle = this.overlayFill;
305       }
306       else if (this.fill) {
307         ctx.fillStyle = this.fill;
308       }
309       
310       if (this.stroke) {
311         ctx.strokeStyle = this.stroke;
312       }
313       ctx.beginPath();
314       
315       this._render(ctx);
316       
317       if (this.fill) {
318         ctx.fill();
319       }
320       if (this.stroke) {
321         ctx.strokeStyle = this.stroke;
322         ctx.lineWidth = this.strokeWidth;
323         ctx.lineCap = ctx.lineJoin = 'round';
324         ctx.stroke();
325       }
326       if (!noTransform && this.active) {
327         this.drawBorders(ctx);
328         this.hideCorners || this.drawCorners(ctx);
329       }
330       ctx.restore();
331     },
332     
333     /**
334      * Returns string representation of an instance
335      * @method toString
336      * @return {String} string representation of an instance
337      */
338     toString: function() {
339       return '#<fabric.Path ('+ this.complexity() +'): ' + 
340         JSON.stringify({ top: this.top, left: this.left }) +'>';
341     },
342     
343     /**
344      * @method toObject
345      * @return {Object}
346      */
347     toObject: function() {
348       var o = extend(this.callSuper('toObject'), {
349         path: this.path
350       });
351       if (this.sourcePath) {
352         o.sourcePath = this.sourcePath;
353       }
354       if (this.transformMatrix) {
355         o.transformMatrix = this.transformMatrix;
356       }
357       return o;
358     },
359     
360     /**
361      * @method toDatalessObject
362      * @return {Object}
363      */
364     toDatalessObject: function() {
365       var o = this.toObject();
366       if (this.sourcePath) {
367         o.path = this.sourcePath;
368       }
369       delete o.sourcePath;
370       return o;
371     },
372     
373     /**
374      * Returns number representation of an instance complexity
375      * @method complexity
376      * @return {Number} complexity
377      */
378     complexity: function() {
379       return this.path.length;
380     },
381     
382     set: function(prop, value) {
383       return this.callSuper('set', prop, value);
384     },
385     
386     _parsePath: function() {
387       
388       var result = [],
389           currentPath, 
390           chunks;
391       
392       // use plain loop for perf.
393       for (var i = 0, len = this.path.length; i < len; i++) {
394         currentPath = this.path[i];
395         chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
396         result.push([currentPath.charAt(0)].concat(chunks.map(parseFloat)));
397       }
398       return result;
399     },
400     
401     /**
402      * @method _parseDimensions
403      */
404     _parseDimensions: function() {
405       var aX = [], 
406           aY = [], 
407           previousX, 
408           previousY, 
409           isLowerCase = false, 
410           x, 
411           y;
412       
413       this.path.forEach(function(item, i) {
414         if (item[0] !== 'H') {
415           previousX = (i === 0) ? getX(item) : getX(this.path[i-1]);
416         }
417         if (item[0] !== 'V') {
418           previousY = (i === 0) ? getY(item) : getY(this.path[i-1]);
419         }
420         
421         // lowercased letter denotes relative position; 
422         // transform to absolute
423         if (item[0] === item[0].toLowerCase()) {
424           isLowerCase = true;
425         }
426         
427         // last 2 items in an array of coordinates are the actualy x/y (except H/V);
428         // collect them
429         
430         // TODO (kangax): support relative h/v commands
431             
432         x = isLowerCase
433           ? previousX + getX(item)
434           : item[0] === 'V' 
435             ? previousX 
436             : getX(item);
437             
438         y = isLowerCase
439           ? previousY + getY(item)
440           : item[0] === 'H' 
441             ? previousY 
442             : getY(item);
443         
444         var val = parseInt(x, 10);
445         if (!isNaN(val)) aX.push(val);
446         
447         val = parseInt(y, 10);
448         if (!isNaN(val)) aY.push(val);
449         
450       }, this);
451       
452       var minX = min(aX), 
453           minY = min(aY), 
454           deltaX = deltaY = 0;
455       
456       var o = {
457         top: minY - deltaY,
458         left: minX - deltaX,
459         bottom: max(aY) - deltaY,
460         right: max(aX) - deltaX
461       };
462       
463       o.width = o.right - o.left;
464       o.height = o.bottom - o.top;
465       
466       return o;
467     }
468   });
469   
470   /**
471    * Creates an instance of fabric.Path from an object
472    * @static
473    * @method fabric.Path.fromObject
474    * @return {fabric.Path} Instance of fabric.Path
475    */
476   fabric.Path.fromObject = function(object) {
477     return new fabric.Path(object.path, object);
478   };
479   
480   var ATTRIBUTE_NAMES = fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity fill-rule stroke stroke-width transform'.split(' ');
481   
482   /**
483    * Creates an instance of fabric.Path from an SVG <PATH> element
484    * @static
485    * @method fabric.Path.fromElement
486    * @param {SVGElement} element to parse
487    * @param {Object} options object
488    * @return {fabric.Path} Instance of fabric.Path
489    */
490   fabric.Path.fromElement = function(element, options) {
491     var parsedAttributes = fabric.parseAttributes(element, ATTRIBUTE_NAMES),
492         path = parsedAttributes.d;
493     delete parsedAttributes.d;
494     return new fabric.Path(path, extend(parsedAttributes, options));
495   };
496 })();