(function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), min = fabric.util.array.min, max = fabric.util.array.max, extend = fabric.util.object.extend, _toString = Object.prototype.toString, drawArc = fabric.util.drawArc, commandLengths = { m: 2, l: 2, h: 1, v: 1, c: 6, s: 4, q: 4, t: 2, a: 7 }, repeatedCommands = { m: 'l', M: 'L' }; if (fabric.Path) { fabric.warn('fabric.Path is already defined'); return; } /** * Path class * @class fabric.Path * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} * @see {@link fabric.Path#initialize} for constructor definition */ fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { /** * Type of an object * @type String * @default */ type: 'path', /** * Array of path points * @type Array * @default */ path: null, /** * Minimum X from points values, necessary to offset points * @type Number * @default */ minX: 0, /** * Minimum Y from points values, necessary to offset points * @type Number * @default */ minY: 0, /** * Constructor * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) * @param {Object} [options] Options object * @return {fabric.Path} thisArg */ initialize: function(path, options) { options = options || { }; this.setOptions(options); if (!path) { throw new Error('`path` argument is required'); } var fromArray = _toString.call(path) === '[object Array]'; this.path = fromArray ? path // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) : path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); if (!this.path) { return; } if (!fromArray) { this.path = this._parsePath(); } var calcDim = this._parseDimensions(); this.minX = calcDim.left; this.minY = calcDim.top; this.width = calcDim.width; this.height = calcDim.height; calcDim.left += this.originX === 'center' ? this.width / 2 : this.originX === 'right' ? this.width : 0; calcDim.top += this.originY === 'center' ? this.height / 2 : this.originY === 'bottom' ? this.height : 0; this.top = this.top || calcDim.top; this.left = this.left || calcDim.left; this.pathOffset = this.pathOffset || { x: this.minX + this.width / 2, y: this.minY + this.height / 2 }; if (options.sourcePath) { this.setSourcePath(options.sourcePath); } }, /** * @private * @param {CanvasRenderingContext2D} ctx context to render path on */ _render: function(ctx) { var current, // current instruction previous = null, subpathStartX = 0, subpathStartY = 0, x = 0, // current x y = 0, // current y controlX = 0, // current control point x controlY = 0, // current control point y tempX, tempY, tempControlX, tempControlY, l = -this.pathOffset.x, t = -this.pathOffset.y; if (this.group && this.group.type === 'path-group') { l = 0; t = 0; } ctx.beginPath(); for (var i = 0, len = this.path.length; i < len; ++i) { current = this.path[i]; switch (current[0]) { // first letter case 'l': // lineto, relative x += current[1]; y += current[2]; ctx.lineTo(x + l, y + t); break; case 'L': // lineto, absolute x = current[1]; y = current[2]; ctx.lineTo(x + l, y + t); break; case 'h': // horizontal lineto, relative x += current[1]; ctx.lineTo(x + l, y + t); break; case 'H': // horizontal lineto, absolute x = current[1]; ctx.lineTo(x + l, y + t); break; case 'v': // vertical lineto, relative y += current[1]; ctx.lineTo(x + l, y + t); break; case 'V': // verical lineto, absolute y = current[1]; ctx.lineTo(x + l, y + t); break; case 'm': // moveTo, relative x += current[1]; y += current[2]; subpathStartX = x; subpathStartY = y; ctx.moveTo(x + l, y + t); break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; subpathStartX = x; subpathStartY = y; ctx.moveTo(x + l, y + t); break; case 'c': // bezierCurveTo, relative tempX = x + current[5]; tempY = y + current[6]; controlX = x + current[3]; controlY = y + current[4]; ctx.bezierCurveTo( x + current[1] + l, // x1 y + current[2] + t, // y1 controlX + l, // x2 controlY + t, // y2 tempX + l, tempY + t ); x = tempX; y = tempY; break; case 'C': // bezierCurveTo, absolute x = current[5]; y = current[6]; controlX = current[3]; controlY = current[4]; ctx.bezierCurveTo( current[1] + l, current[2] + t, controlX + l, controlY + t, x + l, y + t ); break; case 's': // shorthand cubic bezierCurveTo, relative // transform to absolute x,y tempX = x + current[3]; tempY = y + current[4]; // calculate reflection of previous control points controlX = controlX ? (2 * x - controlX) : x; controlY = controlY ? (2 * y - controlY) : y; ctx.bezierCurveTo( controlX + l, controlY + t, x + current[1] + l, y + current[2] + t, tempX + l, tempY + t ); // set control point to 2nd one of this command // "... the first control point is assumed to be // the reflection of the second control point on // the previous command relative to the current point." controlX = x + current[1]; controlY = y + current[2]; x = tempX; y = tempY; break; case 'S': // shorthand cubic bezierCurveTo, absolute tempX = current[3]; tempY = current[4]; // calculate reflection of previous control points controlX = 2 * x - controlX; controlY = 2 * y - controlY; ctx.bezierCurveTo( controlX + l, controlY + t, current[1] + l, current[2] + t, tempX + l, tempY + t ); x = tempX; y = tempY; // set control point to 2nd one of this command // "... the first control point is assumed to be // the reflection of the second control point on // the previous command relative to the current point." controlX = current[1]; controlY = current[2]; break; case 'q': // quadraticCurveTo, relative // transform to absolute x,y tempX = x + current[3]; tempY = y + current[4]; controlX = x + current[1]; controlY = y + current[2]; ctx.quadraticCurveTo( controlX + l, controlY + t, tempX + l, tempY + t ); x = tempX; y = tempY; break; case 'Q': // quadraticCurveTo, absolute tempX = current[3]; tempY = current[4]; ctx.quadraticCurveTo( current[1] + l, current[2] + t, tempX + l, tempY + t ); x = tempX; y = tempY; controlX = current[1]; controlY = current[2]; break; case 't': // shorthand quadraticCurveTo, relative // transform to absolute x,y tempX = x + current[1]; tempY = y + current[2]; if (previous[0].match(/[QqTt]/) === null) { // If there is no previous command or if the previous command was not a Q, q, T or t, // assume the control point is coincident with the current point controlX = x; controlY = y; } else if (previous[0] === 't') { // calculate reflection of previous control points for t controlX = 2 * x - tempControlX; controlY = 2 * y - tempControlY; } else if (previous[0] === 'q') { // calculate reflection of previous control points for q controlX = 2 * x - controlX; controlY = 2 * y - controlY; } tempControlX = controlX; tempControlY = controlY; ctx.quadraticCurveTo( controlX + l, controlY + t, tempX + l, tempY + t ); x = tempX; y = tempY; controlX = x + current[1]; controlY = y + current[2]; break; case 'T': tempX = current[1]; tempY = current[2]; // calculate reflection of previous control points controlX = 2 * x - controlX; controlY = 2 * y - controlY; ctx.quadraticCurveTo( controlX + l, controlY + t, tempX + l, tempY + t ); x = tempX; y = tempY; break; case 'a': // TODO: optimize this drawArc(ctx, x + l, y + t, [ current[1], current[2], current[3], current[4], current[5], current[6] + x + l, current[7] + y + t ]); x += current[6]; y += current[7]; break; case 'A': // TODO: optimize this drawArc(ctx, x + l, y + t, [ current[1], current[2], current[3], current[4], current[5], current[6] + l, current[7] + t ]); x = current[6]; y = current[7]; break; case 'z': case 'Z': x = subpathStartX; y = subpathStartY; ctx.closePath(); break; } previous = current; } this._renderFill(ctx); this._renderStroke(ctx); }, /** * Renders path on a specified context * @param {CanvasRenderingContext2D} ctx context to render path on * @param {Boolean} [noTransform] When true, context is not transformed */ render: function(ctx, noTransform) { // do not render if width/height are zeros or object is not visible if (!this.visible) { return; } ctx.save(); this._setupCompositeOperation(ctx); if (!noTransform) { this.transform(ctx); } this._setStrokeStyles(ctx); this._setFillStyles(ctx); if (this.group && this.group.type === 'path-group') { ctx.translate(-this.group.width / 2, -this.group.height / 2); } if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } this._setOpacity(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); this._restoreCompositeOperation(ctx); ctx.restore(); }, /** * Returns string representation of an instance * @return {String} string representation of an instance */ toString: function() { return '#'; }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { var o = extend(this.callSuper('toObject', propertiesToInclude), { path: this.path.map(function(item) { return item.slice() }), pathOffset: this.pathOffset }); if (this.sourcePath) { o.sourcePath = this.sourcePath; } if (this.transformMatrix) { o.transformMatrix = this.transformMatrix; } return o; }, /** * Returns dataless object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toDatalessObject: function(propertiesToInclude) { var o = this.toObject(propertiesToInclude); if (this.sourcePath) { o.path = this.sourcePath; } delete o.sourcePath; return o; }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var chunks = [], markup = this._createBaseSVGMarkup(), addTransform = ''; for (var i = 0, len = this.path.length; i < len; i++) { chunks.push(this.path[i].join(' ')); } var path = chunks.join(' '); if (!(this.group && this.group.type === 'path-group')) { addTransform = 'translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ')'; } markup.push( //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns number representation of an instance complexity * @return {Number} complexity of this instance */ complexity: function() { return this.path.length; }, /** * @private */ _parsePath: function() { var result = [ ], coords = [ ], currentPath, parsed, re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/ig, match, coordsStr; for (var i = 0, coordsParsed, len = this.path.length; i < len; i++) { currentPath = this.path[i]; coordsStr = currentPath.slice(1).trim(); coords.length = 0; while ((match = re.exec(coordsStr))) { coords.push(match[0]); } coordsParsed = [ currentPath.charAt(0) ]; for (var j = 0, jlen = coords.length; j < jlen; j++) { parsed = parseFloat(coords[j]); if (!isNaN(parsed)) { coordsParsed.push(parsed); } } var command = coordsParsed[0], commandLength = commandLengths[command.toLowerCase()], repeatedCommand = repeatedCommands[command] || command; if (coordsParsed.length - 1 > commandLength) { for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { result.push([ command ].concat(coordsParsed.slice(k, k + commandLength))); command = repeatedCommand; } } else { result.push(coordsParsed); } } return result; }, /** * @private */ _parseDimensions: function() { var aX = [], aY = [], current, // current instruction previous = null, subpathStartX = 0, subpathStartY = 0, x = 0, // current x y = 0, // current y controlX = 0, // current control point x controlY = 0, // current control point y tempX, tempY, tempControlX, tempControlY, bounds; for (var i = 0, len = this.path.length; i < len; ++i) { current = this.path[i]; switch (current[0]) { // first letter case 'l': // lineto, relative x += current[1]; y += current[2]; bounds = [ ]; break; case 'L': // lineto, absolute x = current[1]; y = current[2]; bounds = [ ]; break; case 'h': // horizontal lineto, relative x += current[1]; bounds = [ ]; break; case 'H': // horizontal lineto, absolute x = current[1]; bounds = [ ]; break; case 'v': // vertical lineto, relative y += current[1]; bounds = [ ]; break; case 'V': // verical lineto, absolute y = current[1]; bounds = [ ]; break; case 'm': // moveTo, relative x += current[1]; y += current[2]; subpathStartX = x; subpathStartY = y; bounds = [ ]; break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; subpathStartX = x; subpathStartY = y; bounds = [ ]; break; case 'c': // bezierCurveTo, relative tempX = x + current[5]; tempY = y + current[6]; controlX = x + current[3]; controlY = y + current[4]; bounds = fabric.util.getBoundsOfCurve(x, y, x + current[1], // x1 y + current[2], // y1 controlX, // x2 controlY, // y2 tempX, tempY ); x = tempX; y = tempY; break; case 'C': // bezierCurveTo, absolute x = current[5]; y = current[6]; controlX = current[3]; controlY = current[4]; bounds = fabric.util.getBoundsOfCurve(x, y, current[1], current[2], controlX, controlY, x, y ); break; case 's': // shorthand cubic bezierCurveTo, relative // transform to absolute x,y tempX = x + current[3]; tempY = y + current[4]; // calculate reflection of previous control points controlX = controlX ? (2 * x - controlX) : x; controlY = controlY ? (2 * y - controlY) : y; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, x + current[1], y + current[2], tempX, tempY ); // set control point to 2nd one of this command // "... the first control point is assumed to be // the reflection of the second control point on // the previous command relative to the current point." controlX = x + current[1]; controlY = y + current[2]; x = tempX; y = tempY; break; case 'S': // shorthand cubic bezierCurveTo, absolute tempX = current[3]; tempY = current[4]; // calculate reflection of previous control points controlX = 2 * x - controlX; controlY = 2 * y - controlY; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, current[1], current[2], tempX, tempY ); x = tempX; y = tempY; // set control point to 2nd one of this command // "... the first control point is assumed to be // the reflection of the second control point on // the previous command relative to the current point." controlX = current[1]; controlY = current[2]; break; case 'q': // quadraticCurveTo, relative // transform to absolute x,y tempX = x + current[3]; tempY = y + current[4]; controlX = x + current[1]; controlY = y + current[2]; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); x = tempX; y = tempY; break; case 'Q': // quadraticCurveTo, absolute controlX = current[1]; controlY = current[2]; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, current[3], current[4] ); x = current[3]; y = current[4]; break; case 't': // shorthand quadraticCurveTo, relative // transform to absolute x,y tempX = x + current[1]; tempY = y + current[2]; if (previous[0].match(/[QqTt]/) === null) { // If there is no previous command or if the previous command was not a Q, q, T or t, // assume the control point is coincident with the current point controlX = x; controlY = y; } else if (previous[0] === 't') { // calculate reflection of previous control points for t controlX = 2 * x - tempControlX; controlY = 2 * y - tempControlY; } else if (previous[0] === 'q') { // calculate reflection of previous control points for q controlX = 2 * x - controlX; controlY = 2 * y - controlY; } tempControlX = controlX; tempControlY = controlY; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); x = tempX; y = tempY; controlX = x + current[1]; controlY = y + current[2]; break; case 'T': tempX = current[1]; tempY = current[2]; // calculate reflection of previous control points controlX = 2 * x - controlX; controlY = 2 * y - controlY; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); x = tempX; y = tempY; break; case 'a': // TODO: optimize this bounds = fabric.util.getBoundsOfArc(x, y, current[1], current[2], current[3], current[4], current[5], current[6] + x, current[7] + y ); x += current[6]; y += current[7]; break; case 'A': // TODO: optimize this bounds = fabric.util.getBoundsOfArc(x, y, current[1], current[2], current[3], current[4], current[5], current[6], current[7] ); x = current[6]; y = current[7]; break; case 'z': case 'Z': x = subpathStartX; y = subpathStartY; break; } previous = current; bounds.forEach(function (point) { aX.push(point.x); aY.push(point.y); }); aX.push(x); aY.push(y); } var minX = min(aX), minY = min(aY), maxX = max(aX), maxY = max(aY), deltaX = maxX - minX, deltaY = maxY - minY, o = { left: minX, top: minY, width: deltaX, height: deltaY }; return o; } }); /** * Creates an instance of fabric.Path from an object * @static * @memberOf fabric.Path * @param {Object} object * @param {Function} callback Callback to invoke when an fabric.Path instance is created */ fabric.Path.fromObject = function(object, callback) { if (typeof object.path === 'string') { fabric.loadSVGFromURL(object.path, function (elements) { var path = elements[0], pathUrl = object.path; delete object.path; fabric.util.object.extend(path, object); path.setSourcePath(pathUrl); callback(path); }); } else { callback(new fabric.Path(object.path, object)); } }; /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) * @static * @memberOf fabric.Path * @see http://www.w3.org/TR/SVG/paths.html#PathElement */ fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); /** * Creates an instance of fabric.Path from an SVG element * @static * @memberOf fabric.Path * @param {SVGElement} element to parse * @param {Function} callback Callback to invoke when an fabric.Path instance is created * @param {Object} [options] Options object */ fabric.Path.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); callback && callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); }; /* _FROM_SVG_END_ */ /** * Indicates that instances of this type are async * @static * @memberOf fabric.Path * @type Boolean * @default */ fabric.Path.async = true; })(typeof exports !== 'undefined' ? exports : this);