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