diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 2f0b8be5..22c3b7c7 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -112,9 +112,10 @@ var CompoundPath = this.CompoundPath = PathItem.extend(/** @lends CompoundPath# * @bean */ getCurves: function() { - var curves = []; - for (var i = 0, l = this._children.length; i < l; i++) - curves = curves.concat(this._children[i].getCurves()); + var children = this._children, + curves = []; + for (var i = 0, l = children.length; i < l; i++) + curves = curves.concat(children[i].getCurves()); return curves; }, @@ -140,6 +141,14 @@ var CompoundPath = this.CompoundPath = PathItem.extend(/** @lends CompoundPath# return last && last.getFirstCurve(); }, + getPathData: function(/* precision */) { + var children = this._children, + paths = []; + for (var i = 0, l = children.length; i < l; i++) + paths.push(children[i].getPathData(arguments[0])); + return paths.join(' '); + }, + /** * A private method to help with both #contains() and #_hitTest(). */ diff --git a/src/path/Path.js b/src/path/Path.js index b3719a1b..4db2cf5c 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -184,6 +184,53 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{ return curves[curves.length - 1]; }, + /** + * The segments contained within the path, described as SVG style path data. + * + * @type String + * @bean + */ + getPathData: function(/* precision */) { + var segments = this._segments, + style = this._style, + format = Format.point, + precision = arguments[0], + parts = []; + + // TODO: Add support for H/V and/or relative commands, where appropriate + // and resulting in shorter strings + function addCurve(seg1, seg2, skipLine) { + var point1 = seg1._point, + point2 = seg2._point, + handle1 = seg1._handleOut, + handle2 = seg2._handleIn; + if (handle1.isZero() && handle2.isZero()) { + if (!skipLine) { + // L = absolute lineto: moving to a point with drawing + parts.push('L' + format(point2)); + } + } else { + // c = relative curveto: handle1, handle2 + end - start, + // end - start + var end = point2.subtract(point1); + parts.push('c' + format(handle1) + + ' ' + format(end.add(handle2)) + + ' ' + format(end)); + } + } + + parts.push('M' + format(segments[0]._point)); + for (i = 0, l = segments.length - 1; i < l; i++) + addCurve(segments[i], segments[i + 1], false); + // We only need to draw the connecting curve if it is not a line, and if + // the path is cosed and has a stroke color, or if it is filled. + if (this._closed && style._strokeColor || style._fillColor) + addCurve(segments[segments.length - 1], segments[0], true); + if (this._closed) + parts.push('z'); + return parts.join(''); + }, + /** * Specifies whether the path is closed. If it is closed, Paper.js connects * the first and last segments. diff --git a/src/path/PathItem.js b/src/path/PathItem.js index a967a132..72a666fa 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -49,7 +49,87 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ Curve._addIntersections(values1, values2[j], curve, locations); } return locations; + }, + + setPathData: function(data) { + // This is a very compact SVG Path Data parser that works both for Path + // and CompoundPath. + + var parts = data.match(/[a-z][^a-z]*/ig), + coords, + relative = false, + control, + current = new Point(); // the current position + + function getCoord(index, coord, update) { + var val = parseFloat(coords[index]); + if (relative) + val += current[coord]; + if (update) + current[coord] = val; + return val; + } + + function getPoint(index, update) { + return new Point( + getCoord(index, 'x', update), + getCoord(index + 1, 'y', update) + ); + } + + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i].trim(); + cmd = part[0], + lower = cmd.toLowerCase(); + // Split at white-space, commas but also before signs. + // Use positive ookahead to include signs. + coords = part.slice(1).split(/[\s,]+|(?=[+-])/); + relative = cmd === lower; + switch (lower) { + case 'm': + case 'l': + for (var j = 0; j < coords.length; j += 2) + this[j === 0 && lower === 'm' ? 'moveTo' : 'lineTo']( + getPoint(j, true)); + break; + case 'h': + case 'v': + var coord = lower == 'h' ? 'x' : 'y'; + for (var j = 0; j < coords.length; j++) + getCoord(j, coord, true); + this.lineTo(current); + break; + case 'c': + control = getPoint(2); + this.cubicCurveTo(getPoint(0), control, getPoint(4, true)); + break; + case 's': // Shorthand cubic bezierCurveTo, absolute + // Calculate reflection of previous control points + var handle = current.multiply(2).subtract(control); + control = getPoint(0); + this.cubicCurveTo(handle, control, getPoint(2, true)); + break; + case 'q': + control = getPoint(0); + this.quadraticCurveTo(control, getPoint(2, true)); + break; + case 't': + for (var j = 0; j < coords.length; j += 2) { + // Calculate reflection of previous control points + control = current.multiply(2).subtract(control); + path.quadraticCurveTo(control, getPoint(j, true)); + } + break; + case 'a': + // TODO: Implement Arcs! + break; + case 'z': + this.closePath(); + break; + } + } } + /** * Smooth bezier curves without changing the amount of segments or their * points, by only smoothing and adjusting their handle points, for both diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index c8dc8111..fbf279c1 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -84,42 +84,6 @@ new function() { return attrs; } - function getPath(path) { - var segments = path._segments, - style = path._style, - parts = []; - - function addCurve(seg1, seg2, skipLine) { - var point1 = seg1._point, - point2 = seg2._point, - handle1 = seg1._handleOut, - handle2 = seg2._handleIn; - if (handle1.isZero() && handle2.isZero()) { - if (!skipLine) { - // L = lineto: moving to a point with drawing - parts.push('L' + formatPoint(point2)); - } - } else { - // c = relative curveto: handle1, handle2 + end - start, end - start - var end = point2.subtract(point1); - parts.push('c' + formatPoint(handle1), - formatPoint(end.add(handle2)), - formatPoint(end)); - } - } - - parts.push('M' + formatPoint(segments[0]._point)); - for (i = 0, l = segments.length - 1; i < l; i++) - addCurve(segments[i], segments[i + 1], false); - // We only need to draw the connecting curve if it is not a line, and if - // the path is cosed and has a stroke color, or if it is filled. - if (path._closed && style._strokeColor || style._fillColor) - addCurve(segments[segments.length - 1], segments[0], true); - if (path._closed) - parts.push('z'); - return parts.join(' '); - } - function determineAngle(path, segments, type, center) { // If the object is a circle, ellipse, rectangle, or rounded rectangle, // see if it is placed at an angle, by figuring out its topCenter point @@ -253,7 +217,7 @@ new function() { return null; case 'path': attrs = { - d: getPath(item) + d: item.getPathData() }; break; case 'polyline': @@ -342,12 +306,8 @@ new function() { } function exportCompoundPath(item) { - var attrs = getTransform(item, true), - children = item._children, - paths = []; - for (var i = 0, l = children.length; i < l; i++) - paths.push(children[i].getPathData()); - attrs.d = paths.join(' '); + var attrs = getTransform(item, true); + attrs.d = item.getPathData(); return createElement('path', attrs); } diff --git a/src/svg/SvgImport.js b/src/svg/SvgImport.js index be8a1aaf..469ad0e1 100644 --- a/src/svg/SvgImport.js +++ b/src/svg/SvgImport.js @@ -121,106 +121,14 @@ new function() { } function importPath(node) { - var path = new Path(), - list = node.pathSegList, - compoundPath, lastPoint; - for (var i = 0, l = list.numberOfItems; i < l; i++) { - var segment = list.getItem(i), - segType = segment.pathSegType, - isRelative = segType % 2 == 1; - if (segType === /*#=*/ SVGPathSeg.PATHSEG_UNKNOWN) - continue; - if (!path.isEmpty()) - lastPoint = path.getLastSegment().getPoint(); - var relative = isRelative && !path.isEmpty() - ? lastPoint - : Point.create(0, 0); - // Horizontal or vertical lineto commands, so fill in the - // missing x or y value: - var coord = (segType == /*#=*/ SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS - || segType == /*#=*/ SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL) && 'y' - || (segType == /*#=*/ SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS - || segType == /*#=*/ SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL) && 'x'; - if (coord) - segment[coord] = isRelative ? 0 : lastPoint[coord]; - var point = Point.create(segment.x, segment.y).add(relative); - switch (segType) { - case /*#=*/ SVGPathSeg.PATHSEG_CLOSEPATH: - path.closePath(); - break; - case /*#=*/ SVGPathSeg.PATHSEG_MOVETO_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_MOVETO_REL: - if (!path.isEmpty() && !compoundPath) { - compoundPath = new CompoundPath([path]); - } - if (compoundPath) { - path = new Path(); - compoundPath.addChild(path); - } - path.moveTo(point); - break; - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_REL: - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL: - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL: - path.lineTo(point); - break; - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL: - path.cubicCurveTo( - relative.add(segment.x1, segment.y1), - relative.add(segment.x2, segment.y2), - point - ); - break; - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL: - path.quadraticCurveTo( - relative.add(segment.x1, segment.y1), - point - ); - break; - // TODO: Implement Arcs: ttp://www.w3.org/TR/SVG/implnote.html - // case /*#=*/ SVGPathSeg.PATHSEG_ARC_ABS: - // case /*#=*/ SVGPathSeg.PATHSEG_ARC_REL: - // break; - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL: - var prev = list.getItem(i - 1), - control = lastPoint.add(lastPoint.subtract( - Point.create(prev.x2, prev.y2) - .subtract(prev.x, prev.y) - .add(lastPoint))); - path.cubicCurveTo( - control, - relative.add(segment.x2, segment.y2), - point); - break; - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS: - case /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL: - var control, - j = i; - for (; j >= 0; j--) { - var prev = list.getItem(j); - if (prev.pathSegType === /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS || - prev.pathSegType === /*#=*/ SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL) { - control = Point.create(prev.x1, prev.y1) - .subtract(prev.x, prev.y) - .add(path._segments[j].getPoint()); - break; - } - } - for (; j < i; ++j) { - var anchor = path._segments[j].getPoint(); - control = anchor.add(anchor.subtract(control)); - } - path.quadraticCurveTo(control, point); - break; - } - } - return compoundPath || path; + // Get the path data, and determine wether it is a compound path or a + // normal path based on the amount of moveTo commands inside it. + var data = node.getAttribute('d'), + path = data.match(/m/gi).length > 1 + ? new CompoundPath() + : new Path(); + path.setPathData(data); + return path; } function importGradient(node, type) {