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