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