      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 })();