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