diff --git a/README.md b/README.md index 8650d17..30ae126 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Paperjs Offset The dicussion to implement a offset function in paper.js started years ago, yet the author have not decided to put a offset feature into the library. So I implement an extension of my own. +
As far as I know, the author has promised recently to implement a native offset functionality in near feature, the library will be closed once the native implement is published.
This library implement both path offset and stroke offset, you may offset a path or expand a stroke like what you did in Adobe illustrator. Offset complicate path may cause unwanted self intersections, this library already take care some cases but bugs still exists. Please let me notice the false conditions in the issue pannel so I can correct it. ## Usage @@ -10,18 +11,37 @@ npm install paperjs-offset And then, in you project: ```javascript import paper from 'paper' -import ExtendPaperJs from 'paperjs-offset' +import { PaperOffset } from 'paperjs-offset' + +// call offset +PaperOffset.offset(path, offset, options) + +// call offset stroke +PaperOffset.offsetStroke(path, offset, options) +``` +You may still use the old way to extend paperjs module, which is **deprecated** and will be removed in future version. +```javascript +import ExtendPaperJs from 'paperjs-offset' +// extend paper.Path, paper.CompoundPath with offset, offsetStroke method ExtendPaperJs(paper) ``` + Or for web development, include the **paperjs-offset.js** or **paperjs-offset.min.js** in demo folder. -
The library extends **paper.Path** and **paper.CompoundPath** object, with offset/offsetStroke functions. +
The library now exposes a global variable **PaperOffset**, again, the extension of **paper.Path** and **paper.CompoundPath** with offset/offsetStroke functions is still available, but no longer recommended. ```javascript let path = new paper.Path(/* params */) + +PaperOffset.offset(path, 10, { join: 'round' }) +PaperOffset.offsetStroke(path, 10, { cap: 'round' }) + +// deprecated path.offset(10, { join: 'round' }) +// deprecated path.offsetStroke(10, { cap: 'round' }) ``` -Both offset/offsetStroke take the form of **f(offset: number, options?: {})**, the options have following parameters: + +Both offset/offsetStroke take the form of **f(path: Path | CompoundPath, offset: number, options?: {})**, the options have following parameters:
  **join**: the join style of offset path, you can choose **'miter'**, **'bevel'** or **'round'**, default is **'miter'**.
  **limit**: the limit for miter style (refer the miterLimit's definition in paper.js).
  **cap**: the cap style of offset (only available in offsetStroke), you can choose **'butt'** and **'round'** (**'square'** is not supported yet), default is **'butt'** @@ -31,13 +51,7 @@ Both offset/offsetStroke take the form of **f(offset: number, options?: {})**, t There are some cases that the library may return weird result or failed silently, please let me noticed in the project issues. And in some cases the library will yeild an ok result than a perfect one. Currently the library should give good results for closed shapes, but may fail in some open curve cases, I'm still working on it. ![Preview](/public/preview.jpg) -## Development -```sh -# build es5/umd/iife packages -npm run build -``` You can use open demo folder for simple cases demonstration. -
*I've noticed there is a react adaption of paper.js but no vue adaption, so I create a vue adaption of paper.js, you can visit this [repo](https://github.com/luz-alphacode/paper-vueify) if you are interested.* ## License -Distributed under the MIT license. See [LICENSE](https://github.com/luz-alphacode/paperjs-offset/blob/master/LICENSE) for detail. \ No newline at end of file +Distributed under the MIT license. See [LICENSE](https://github.com/glenzli/paperjs-offset/blob/master/LICENSE) for detail. \ No newline at end of file diff --git a/demo/demo.js b/demo/demo.js index a5a4ef9..e5d8e8a 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -6,38 +6,38 @@ // simple polygon let r = new paper.Path.Rectangle({ point: [-500, -300], size: [80, 80], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' }) - r.offset(10) + PaperOffset.offset(r, 10) r.bringToFront() - r.offset(-10).offset(-10).offset(-10) + PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(r, -10), -10), -10); // simple polygon + bevel let r11 = new paper.Path.Rectangle({ point: [-500, -150], size: [60, 60], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' }) - let r12 = r11.offset(-10, { insert: false }) + let r12 = PaperOffset.offset(r11, -10, { insert: false }) let r1 = r11.subtract(r12, { insert: true }) r11.remove() - r1.offset(15, { join: 'bevel' }) + PaperOffset.offset(r1, 15, { join: 'bevel' }) r1.bringToFront() // simple polygon + round let r21 = new paper.Path.Rectangle({ point: [-350, -150], size: [60, 60], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' }) - let r22 = r21.offset(-10, { insert: false }) + let r22 = PaperOffset.offset(r21, -10, { insert: false }) let r2 = r21.subtract(r22, { insert: true }) r21.remove() - r2.offset(15, { join: 'round' }) + PaperOffset.offset(r2, 15, { join: 'round' }) r2.bringToFront() // simple polygon let s = new paper.Path.Star({ center: [-300, -260], points: 12, radius1: 40, radius2: 30, fillColor: 'rgba(234, 154, 100, 0.5)', strokeColor: 'black' }) - s.offset(10) + PaperOffset.offset(s, 10) s.bringToFront() - s.offset(-10).offset(-10) + PaperOffset.offset(PaperOffset.offset(s, -10), -10); // smooth let s2 = new paper.Path.Star({ center: [-150, -260], points: 7, radius1: 40, radius2: 30, fillColor: 'rgba(239, 209, 88, 0.5)', strokeColor: 'black' }) s2.smooth() - s2.offset(10) + PaperOffset.offset(s2, 10); s2.bringToFront() - s2.offset(-10).offset(-10) + PaperOffset.offset(PaperOffset.offset(s2, -10), -10) // complex let c1 = new paper.Path.Circle({ center: [-20, -260], radius: 40, fillColor: 'rgba(165, 193, 93, 0.5)', strokeColor: 'black' }) @@ -45,9 +45,9 @@ let c = c1.unite(c2, { insert: true }) c1.remove() c2.remove() - c.offset(10) + PaperOffset.offset(c, 10); c.bringToFront() - c.offset(-10).offset(-10).offset(-10) + PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(c, -10), -10), -10) let c3 = new paper.Path.Circle({ center: [180, -260], radius: 40, fillColor: 'rgba(117, 170, 173, 0.5)', strokeColor: 'black' }) let c4 = new paper.Path.Circle({ center: [230, -260], radius: 40, fillColor: 'rgba(117, 170, 173, 0.5)', strokeColor: 'black' }) @@ -58,9 +58,9 @@ c4.remove() c5.remove() cc1.remove() - cc.offset(10) + PaperOffset.offset(cc, 10) cc.bringToFront() - cc.offset(-10).offset(-10).offset(-10).offset(-5) + PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(cc, -10), -10), -10), -5) // complex+ let c6 = new paper.Path.Circle({ center: [380, -260], radius: 40, fillColor: 'rgba(156, 104, 193, 0.5)', strokeColor: 'black' }) @@ -75,17 +75,17 @@ ccc.smooth() ccc.offset(10) ccc.bringToFront() - ccc.offset(-10).offset(-10) - ccc.offset(-30).offset(-5) + PaperOffset.offset(PaperOffset.offset(ccc, -10), -10) + PaperOffset.offset(PaperOffset.offset(ccc, -30), -5) // stroke let rs = new paper.Path.Rectangle({ point: [-200, -150], size: [80, 80], fillColor: null, strokeColor: 'rgb(191, 91, 91, 0.5)' }) - rs.offsetStroke(10) + PaperOffset.offsetStroke(rs, 10) rs.bringToFront() // stroke let st1 = new paper.Path.Line({ from: [-50, -100], to: [0, -100], strokeColor: 'rgba(156, 104, 193, 0.5)', strokeWidth: 3 }) - st1.offsetStroke(20, { cap: 'round' }) + PaperOffset.offsetStroke(st1, 20, { cap: 'round' }) st1.bringToFront() // stroke complex @@ -95,30 +95,31 @@ cs.fillColor = null cs.position = [150, -50] cs.closed = false - cs.offsetStroke(20) + PaperOffset.offsetStroke(cs, 20) cs.bringToFront() let cs2 = cs.clone() cs2.position = [400, -50] cs2.strokeColor = 'rgba(117, 170, 173, 0.5)' - cs2.offsetStroke(25, { cap: 'round' }) + PaperOffset.offsetStroke(cs2, 25, { cap: 'round' }) cs2.bringToFront() // edge cases let ec1 = new paper.Path({ pathData: 'M466,467c0,0 -105,-235 0,0c-376.816,-119.63846 -469.06596,-146.09389 -650.61329,-266.59735c-282.68388,-230.49081 300.86045,-10.26825 452.77726,121.52815z', fillColor: 'rgba(156, 104, 193, 0.5)' }) ec1.translate(-450, -250) ec1.scale(0.4) - ec1.offset(10) - ec1.offset(-10).offset(-10).offset(-10) + PaperOffset.offset(ec1, 10) + PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(ec1, -10), -10), -10) let ec2 = new paper.Path({ pathData: 'M466,467c-65,-34 136,64 0,0c-391,-270 62,-670 62,-670l-463,370z', strokeColor: 'rgba(239, 209, 88, 0.5)', strokeWidth: 3 }) ec2.scale(0.4) ec2.translate(-350, 20) - ec2.offsetStroke(10) + PaperOffset.offsetStroke(ec2, 10) let ec3 = new paper.Path({ pathData: 'M466,467c-65,-34 136,64 0,0c-391,-270 520,-471 522,-137c-214,-144 -1489,123 -923,-163z', fillColor: 'rgb(191, 91, 91, 0.5)' }) ec3.scale(0.4) ec3.translate(-100, -150) - ec3.offset(-10) + PaperOffset.offset(ec3, -10) } + window.onload = RunDemo })() \ No newline at end of file diff --git a/demo/paperjs-offset.js b/demo/paperjs-offset.js index adbf32e..43c3ba4 100644 --- a/demo/paperjs-offset.js +++ b/demo/paperjs-offset.js @@ -1,7 +1,7 @@ (function (paper) { 'use strict'; - paper = paper && paper.hasOwnProperty('default') ? paper['default'] : paper; + paper = paper && Object.prototype.hasOwnProperty.call(paper, 'default') ? paper['default'] : paper; var Arrayex; (function (Arrayex) { @@ -266,313 +266,305 @@ Arrayex.NonNull = NonNull; })(Arrayex || (Arrayex = {})); - var Offsets; - (function (Offsets) { - /** - * Offset the start/terminal segment of a bezier curve - * @param segment segment to offset - * @param curve curve to offset - * @param handleNormal the normal of the the line formed of two handles - * @param offset offset value - */ - function OffsetSegment(segment, curve, handleNormal, offset) { - var isFirst = segment.curve === curve; - // get offset vector - var offsetVector = (curve.getNormalAt(isFirst ? 0 : 1, true)).multiply(offset); - // get offset point - var point = segment.point.add(offsetVector); - var newSegment = new paper.Segment(point); - // handleOut for start segment & handleIn for terminal segment - var handle = (isFirst ? 'handleOut' : 'handleIn'); - newSegment[handle] = segment[handle].add(handleNormal.subtract(offsetVector).divide(2)); - return newSegment; - } - /** - * Adaptive offset a curve by repeatly apply the approximation proposed by Tiller and Hanson. - * @param curve curve to offset - * @param offset offset value - */ - function AdaptiveOffsetCurve(curve, offset) { - var hNormal = (new paper.Curve(curve.segment1.handleOut.add(curve.segment1.point), new paper.Point(0, 0), new paper.Point(0, 0), curve.segment2.handleIn.add(curve.segment2.point))).getNormalAt(0.5, true).multiply(offset); - var segment1 = OffsetSegment(curve.segment1, curve, hNormal, offset); - var segment2 = OffsetSegment(curve.segment2, curve, hNormal, offset); - // divide && re-offset - var offsetCurve = new paper.Curve(segment1, segment2); - // if the offset curve is not self intersected, divide it - if (offsetCurve.getIntersections(offsetCurve).length === 0) { - var threshold = Math.min(Math.abs(offset) / 10, 1); - var midOffset = offsetCurve.getPointAt(0.5, true).getDistance(curve.getPointAt(0.5, true)); - if (Math.abs(midOffset - Math.abs(offset)) > threshold) { - var subCurve = curve.divideAtTime(0.5); - if (subCurve != null) { - return AdaptiveOffsetCurve(curve, offset).concat(AdaptiveOffsetCurve(subCurve, offset)); - } + /** + * Offset the start/terminal segment of a bezier curve + * @param segment segment to offset + * @param curve curve to offset + * @param handleNormal the normal of the the line formed of two handles + * @param offset offset value + */ + function offsetSegment(segment, curve, handleNormal, offset) { + var isFirst = segment.curve === curve; + // get offset vector + var offsetVector = (curve.getNormalAtTime(isFirst ? 0 : 1)).multiply(offset); + // get offset point + var point = segment.point.add(offsetVector); + var newSegment = new paper.Segment(point); + // handleOut for start segment & handleIn for terminal segment + var handle = (isFirst ? 'handleOut' : 'handleIn'); + newSegment[handle] = segment[handle].add(handleNormal.subtract(offsetVector).divide(2)); + return newSegment; + } + /** + * Adaptive offset a curve by repeatly apply the approximation proposed by Tiller and Hanson. + * @param curve curve to offset + * @param offset offset value + */ + function adaptiveOffsetCurve(curve, offset) { + var hNormal = (new paper.Curve(curve.segment1.handleOut.add(curve.segment1.point), new paper.Point(0, 0), new paper.Point(0, 0), curve.segment2.handleIn.add(curve.segment2.point))).getNormalAtTime(0.5).multiply(offset); + var segment1 = offsetSegment(curve.segment1, curve, hNormal, offset); + var segment2 = offsetSegment(curve.segment2, curve, hNormal, offset); + // divide && re-offset + var offsetCurve = new paper.Curve(segment1, segment2); + // if the offset curve is not self intersected, divide it + if (offsetCurve.getIntersections(offsetCurve).length === 0) { + var threshold = Math.min(Math.abs(offset) / 10, 1); + var midOffset = offsetCurve.getPointAtTime(0.5).getDistance(curve.getPointAtTime(0.5)); + if (Math.abs(midOffset - Math.abs(offset)) > threshold) { + var subCurve = curve.divideAtTime(0.5); + if (subCurve != null) { + return adaptiveOffsetCurve(curve, offset).concat(adaptiveOffsetCurve(subCurve, offset)); } } - return [segment1, segment2]; - } - /** - * Create a round join segment between two adjacent segments. - */ - function MakeRoundJoin(segment1, segment2, originPoint, radius) { - var through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint)).normalize(Math.abs(radius)).add(originPoint); - var arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through: through, insert: false }); - segment1.handleOut = arc.firstSegment.handleOut; - segment2.handleIn = arc.lastSegment.handleIn; - return arc.segments.length === 3 ? arc.segments[1] : null; - } - function Det(p1, p2) { - return p1.x * p2.y - p1.y * p2.x; } - /** - * Get the intersection point of two point - */ - function Intersection(p1, p2, p3, p4) { - var l1 = p1.subtract(p2); - var l2 = p3.subtract(p4); - var dl1 = Det(p1, p2); - var dl2 = Det(p3, p4); - return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(Det(l1, l2)); + return [segment1, segment2]; + } + /** + * Create a round join segment between two adjacent segments. + */ + function makeRoundJoin(segment1, segment2, originPoint, radius) { + var through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint)) + .normalize(Math.abs(radius)).add(originPoint); + var arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through: through, insert: false }); + segment1.handleOut = arc.firstSegment.handleOut; + segment2.handleIn = arc.lastSegment.handleIn; + return arc.segments.length === 3 ? arc.segments[1] : null; + } + function det(p1, p2) { + return p1.x * p2.y - p1.y * p2.x; + } + /** + * Get the intersection point of point based lines + */ + function getPointLineIntersections(p1, p2, p3, p4) { + var l1 = p1.subtract(p2); + var l2 = p3.subtract(p4); + var dl1 = det(p1, p2); + var dl2 = det(p3, p4); + return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(det(l1, l2)); + } + /** + * Connect two adjacent bezier curve, each curve is represented by two segments, + * create different types of joins or simply removal redundant segment. + */ + function connectAdjacentBezier(segments1, segments2, origin, joinType, offset, limit) { + var curve1 = new paper.Curve(segments1[0], segments1[1]); + var curve2 = new paper.Curve(segments2[0], segments2[1]); + var intersection = curve1.getIntersections(curve2); + var distance = segments1[1].point.getDistance(segments2[0].point); + if (origin.isSmooth()) { + segments2[0].handleOut = segments2[0].handleOut.project(origin.handleOut); + segments2[0].handleIn = segments1[1].handleIn.project(origin.handleIn); + segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2); + segments1.pop(); } - /** - * Connect two adjacent bezier curve, each curve is represented by two segments, create different types of joins or simply removal redundant segment. - */ - function ConnectAdjacentBezier(segments1, segments2, origin, join, offset, limit) { - var curve1 = new paper.Curve(segments1[0], segments1[1]); - var curve2 = new paper.Curve(segments2[0], segments2[1]); - var intersection = curve1.getIntersections(curve2); - var distance = segments1[1].point.getDistance(segments2[0].point); - if (origin.isSmooth()) { - segments2[0].handleOut = segments2[0].handleOut.project(origin.handleOut); - segments2[0].handleIn = segments1[1].handleIn.project(origin.handleIn); - segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2); - segments1.pop(); + else { + if (intersection.length === 0) { + if (distance > Math.abs(offset) * 0.1) { + // connect + switch (joinType) { + case 'miter': + var join = getPointLineIntersections(curve1.point2, curve1.point2.add(curve1.getTangentAtTime(1)), curve2.point1, curve2.point1.add(curve2.getTangentAtTime(0))); + // prevent sharp angle + var joinOffset = Math.max(join.getDistance(curve1.point2), join.getDistance(curve2.point1)); + if (joinOffset < Math.abs(offset) * limit) { + segments1.push(new paper.Segment(join)); + } + break; + case 'round': + var mid = makeRoundJoin(segments1[1], segments2[0], origin.point, offset); + if (mid) { + segments1.push(mid); + } + break; + } + } + else { + segments2[0].handleIn = segments1[1].handleIn; + segments1.pop(); + } } else { - if (intersection.length === 0) { - if (distance > Math.abs(offset) * 0.1) { - // connect - switch (join) { - case 'miter': - var join_1 = Intersection(curve1.point2, curve1.point2.add(curve1.getTangentAt(1, true)), curve2.point1, curve2.point1.add(curve2.getTangentAt(0, true))); - // prevent sharp angle - var joinOffset = Math.max(join_1.getDistance(curve1.point2), join_1.getDistance(curve2.point1)); - if (joinOffset < Math.abs(offset) * limit) { - segments1.push(new paper.Segment(join_1)); - } - break; - case 'round': - var mid = MakeRoundJoin(segments1[1], segments2[0], origin.point, offset); - if (mid) { - segments1.push(mid); - } - break; - default: break; - } - } - else { - segments2[0].handleIn = segments1[1].handleIn; - segments1.pop(); - } + var second1 = curve1.divideAt(intersection[0]); + if (second1) { + var join = second1.segment1; + var second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]); + join.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut; + segments1.pop(); + segments2[0] = join; } else { - var second1 = curve1.divideAt(intersection[0]); - if (second1) { - var join_2 = second1.segment1; - var second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]); - join_2.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut; - segments1.pop(); - segments2[0] = join_2; - } - else { - segments2[0].handleIn = segments1[1].handleIn; - segments1.pop(); - } + segments2[0].handleIn = segments1[1].handleIn; + segments1.pop(); } } } - Offsets.ConnectAdjacentBezier = ConnectAdjacentBezier; - /** - * Connect all the segments together. - */ - function ConnectBeziers(rawSegments, join, source, offset, limit) { - var originSegments = source.segments; - var first = rawSegments[0].slice(); - for (var i = 0; i < rawSegments.length - 1; ++i) { - ConnectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit); + } + /** + * Connect all the segments together. + */ + function connectBeziers(rawSegments, join, source, offset, limit) { + var originSegments = source.segments; + var first = rawSegments[0].slice(); + for (var i = 0; i < rawSegments.length - 1; ++i) { + connectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit); + } + if (source.closed) { + connectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit); + rawSegments[0][0] = first[0]; + } + return rawSegments; + } + function reduceSingleChildCompoundPath(path) { + if (path.children.length === 1) { + path = path.children[0]; + path.remove(); // remove from parent, this is critical, or the style attributes will be ignored + } + return path; + } + /** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */ + function normalize(path, areaThreshold) { + if (areaThreshold === void 0) { areaThreshold = 0.01; } + if (path.closed) { + var ignoreArea_1 = Math.abs(path.area * areaThreshold); + if (!path.clockwise) { + path.reverse(); } - if (source.closed) { - ConnectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit); - rawSegments[0][0] = first[0]; + path = path.unite(path, { insert: false }); + if (path instanceof paper.CompoundPath) { + path.children.filter(function (c) { return Math.abs(c.area) < ignoreArea_1; }).forEach(function (c) { return c.remove(); }); + if (path.children.length === 1) { + return reduceSingleChildCompoundPath(path); + } } - return rawSegments; } - function Decompound(path) { - if (path.children.length === 1) { - path = path.children[0]; - path.remove(); // remove from parent, this is critical, or the style attributes will be ignored - } - return path; - } - /** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */ - function Normalize(path, areaThreshold) { - if (areaThreshold === void 0) { areaThreshold = 0.01; } - if (path.closed) { - var ignoreArea_1 = Math.abs(path.area * areaThreshold); - if (!path.clockwise) { - path.reverse(); + return path; + } + function isSameDirection(partialPath, fullPath) { + var offset1 = partialPath.segments[0].location.offset; + var offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset; + var sampleOffset = (offset1 + offset2) / 3; + var originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset; + var originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset; + return originOffset1 < originOffset2; + } + /** Remove self intersection when offset is negative by point direction dectection. */ + function removeIntersection(path) { + var newPath = path.unite(path, { insert: false }); + if (newPath instanceof paper.CompoundPath) { + newPath.children.filter(function (c) { + if (c.segments.length > 1) { + return !isSameDirection(c, path); } - path = path.unite(path, { insert: false }); - if (path instanceof paper.CompoundPath) { - path.children.filter(function (c) { return Math.abs(c.area) < ignoreArea_1; }).forEach(function (c) { return c.remove(); }); - if (path.children.length === 1) { - return Decompound(path); - } + else { + return true; } - } - return path; - } - Offsets.Normalize = Normalize; - function IsSameDirection(partialPath, fullPath) { - var offset1 = partialPath.segments[0].location.offset; - var offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset; - var sampleOffset = (offset1 + offset2) / 3; - var originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset; - var originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset; - return originOffset1 < originOffset2; - } - Offsets.IsSameDirection = IsSameDirection; - /** Remove self intersection when offset is negative by point direction dectection. */ - function RemoveIntersection(path) { - var newPath = path.unite(path, { insert: false }); - if (newPath instanceof paper.CompoundPath) { - newPath.children.filter(function (c) { - if (c.segments.length > 1) { - return !IsSameDirection(c, path); - } - else { - return true; - } - }).forEach(function (c) { return c.remove(); }); - return Decompound(newPath); - } - return path; + }).forEach(function (c) { return c.remove(); }); + return reduceSingleChildCompoundPath(newPath); } - Offsets.RemoveIntersection = RemoveIntersection; - function Segments(path) { - if (path instanceof paper.CompoundPath) { - return Arrayex.Flat(path.children.map(function (c) { return c.segments; })); - } - else { - return path.segments; - } + return path; + } + function getSegments(path) { + if (path instanceof paper.CompoundPath) { + return Arrayex.Flat(path.children.map(function (c) { return c.segments; })); } - /** - * Remove impossible segments in negative offset condition. - */ - function RemoveOutsiders(offsetPath, path) { - var segments = Segments(offsetPath).slice(); - segments.forEach(function (segment) { - if (!path.contains(segment.point)) { - segment.remove(); - } - }); + else { + return path.segments; } - function PreparePath(path, offset) { - var source = path.clone({ insert: false }); - source.reduce(); - if (!path.clockwise) { - source.reverse(); - offset = -offset; - } - return [source, offset]; - } - function OffsetSimpleShape(path, offset, join, limit) { - var _a; - var source; - _a = PreparePath(path, offset), source = _a[0], offset = _a[1]; - var curves = source.curves.slice(); - var raws = Arrayex.Divide(Arrayex.Flat(curves.map(function (curve) { return AdaptiveOffsetCurve(curve, offset); })), 2); - var segments = Arrayex.Flat(ConnectBeziers(raws, join, source, offset, limit)); - var offsetPath = RemoveIntersection(new paper.Path({ segments: segments, insert: false, closed: path.closed })); - offsetPath.reduce(); - if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) { - RemoveOutsiders(offsetPath, path); - } - // recovery path - if (source.clockwise !== path.clockwise) { - offsetPath.reverse(); - } - return Normalize(offsetPath); - } - Offsets.OffsetSimpleShape = OffsetSimpleShape; - function MakeRoundCap(from, to, offset) { - var origin = from.point.add(to.point).divide(2); - var normal = to.point.subtract(from.point).rotate(-90).normalize(offset); - var through = origin.add(normal); - var arc = new paper.Path.Arc({ from: from.point, to: to.point, through: through, insert: false }); - return arc.segments; - } - function ConnectSide(outer, inner, offset, cap) { - if (outer instanceof paper.CompoundPath) { - var cs = outer.children.map(function (c) { return ({ c: c, a: Math.abs(c.area) }); }); - cs = cs.sort(function (c1, c2) { return c2.a - c1.a; }); - outer = cs[0].c; - } - var oSegments = outer.segments.slice(); - var iSegments = inner.segments.slice(); - switch (cap) { - case 'round': - var heads = MakeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset); - var tails = MakeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset); - var result = new paper.Path({ segments: heads.concat(oSegments, tails, iSegments), closed: true, insert: false }); - result.reduce(); - return result; - default: return new paper.Path({ segments: oSegments.concat(iSegments), closed: true, insert: false }); - } + } + /** + * Remove impossible segments in negative offset condition. + */ + function removeOutsiders(newPath, path) { + var segments = getSegments(newPath).slice(); + segments.forEach(function (segment) { + if (!path.contains(segment.point)) { + segment.remove(); + } + }); + } + function preparePath(path, offset) { + var source = path.clone({ insert: false }); + source.reduce({}); + if (!path.clockwise) { + source.reverse(); + offset = -offset; + } + return [source, offset]; + } + function offsetSimpleShape(path, offset, join, limit) { + var _a; + var source; + _a = preparePath(path, offset), source = _a[0], offset = _a[1]; + var curves = source.curves.slice(); + var raws = Arrayex.Divide(Arrayex.Flat(curves.map(function (curve) { return adaptiveOffsetCurve(curve, offset); })), 2); + var segments = Arrayex.Flat(connectBeziers(raws, join, source, offset, limit)); + var newPath = removeIntersection(new paper.Path({ segments: segments, insert: false, closed: path.closed })); + newPath.reduce({}); + if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) { + removeOutsiders(newPath, path); + } + // recovery path + if (source.clockwise !== path.clockwise) { + newPath.reverse(); + } + return normalize(newPath); + } + function makeRoundCap(from, to, offset) { + var origin = from.point.add(to.point).divide(2); + var normal = to.point.subtract(from.point).rotate(-90, new paper.Point(0, 0)).normalize(offset); + var through = origin.add(normal); + var arc = new paper.Path.Arc({ from: from.point, to: to.point, through: through, insert: false }); + return arc.segments; + } + function connectSide(outer, inner, offset, cap) { + if (outer instanceof paper.CompoundPath) { + var cs = outer.children.map(function (c) { return ({ c: c, a: Math.abs(c.area) }); }); + cs = cs.sort(function (c1, c2) { return c2.a - c1.a; }); + outer = cs[0].c; + } + var oSegments = outer.segments.slice(); + var iSegments = inner.segments.slice(); + switch (cap) { + case 'round': + var heads = makeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset); + var tails = makeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset); + var result = new paper.Path({ segments: heads.concat(oSegments, tails, iSegments), closed: true, insert: false }); + result.reduce({}); + return result; + default: return new paper.Path({ segments: oSegments.concat(iSegments), closed: true, insert: false }); } - function OffsetSimpleStroke(path, offset, join, cap, limit) { - offset = path.clockwise ? offset : -offset; - var positiveOffset = OffsetSimpleShape(path, offset, join, limit); - var negativeOffset = OffsetSimpleShape(path, -offset, join, limit); - if (path.closed) { - return positiveOffset.subtract(negativeOffset, { insert: false }); - } - else { - var inner = negativeOffset; - var holes = new Array(); - if (negativeOffset instanceof paper.CompoundPath) { - holes = negativeOffset.children.filter(function (c) { return c.closed; }); - holes.forEach(function (h) { return h.remove(); }); - inner = negativeOffset.children[0]; - } - inner.reverse(); - var final = ConnectSide(positiveOffset, inner, offset, cap); - if (holes.length > 0) { - for (var _i = 0, holes_1 = holes; _i < holes_1.length; _i++) { - var hole = holes_1[_i]; - final = final.subtract(hole, { insert: false }); - } + } + function offsetSimpleStroke(path, offset, join, cap, limit) { + offset = path.clockwise ? offset : -offset; + var positiveOffset = offsetSimpleShape(path, offset, join, limit); + var negativeOffset = offsetSimpleShape(path, -offset, join, limit); + if (path.closed) { + return positiveOffset.subtract(negativeOffset, { insert: false }); + } + else { + var inner = negativeOffset; + var holes = new Array(); + if (negativeOffset instanceof paper.CompoundPath) { + holes = negativeOffset.children.filter(function (c) { return c.closed; }); + holes.forEach(function (h) { return h.remove(); }); + inner = negativeOffset.children[0]; + } + inner.reverse(); + var final = connectSide(positiveOffset, inner, offset, cap); + if (holes.length > 0) { + for (var _i = 0, holes_1 = holes; _i < holes_1.length; _i++) { + var hole = holes_1[_i]; + final = final.subtract(hole, { insert: false }); } - return final; } + return final; } - Offsets.OffsetSimpleStroke = OffsetSimpleStroke; - })(Offsets || (Offsets = {})); - function OffsetPath(path, offset, join, limit) { + } + function offsetPath(path, offset, join, limit) { var nonSIPath = path.unite(path, { insert: false }); var result = nonSIPath; if (nonSIPath instanceof paper.Path) { - result = Offsets.OffsetSimpleShape(nonSIPath, offset, join, limit); + result = offsetSimpleShape(nonSIPath, offset, join, limit); } else { var children = Arrayex.Flat(nonSIPath.children.map(function (c) { if (c.segments.length > 1) { - if (!Offsets.IsSameDirection(c, path)) { + if (!isSameDirection(c, path)) { c.reverse(); } - var offseted = Offsets.OffsetSimpleShape(c, offset, join, limit); - offseted = Offsets.Normalize(offseted); + var offseted = offsetSimpleShape(c, offset, join, limit); + offseted = normalize(offseted); if (offseted.clockwise !== c.clockwise) { offseted.reverse(); } @@ -594,15 +586,15 @@ result.remove(); return result; } - function OffsetStroke(path, offset, join, cap, limit) { + function offsetStroke(path, offset, join, cap, limit) { var nonSIPath = path.unite(path, { insert: false }); var result = nonSIPath; if (nonSIPath instanceof paper.Path) { - result = Offsets.OffsetSimpleStroke(nonSIPath, offset, join, cap, limit); + result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit); } else { var children = Arrayex.Flat(nonSIPath.children.map(function (c) { - return Offsets.OffsetSimpleStroke(c, offset, join, cap, limit); + return offsetSimpleStroke(c, offset, join, cap, limit); })); result = children.reduce(function (c1, c2) { return c1.unite(c2, { insert: false }); }); } @@ -614,43 +606,55 @@ return result; } - function PrototypedOffset(path, offset, options) { - options = options || {}; - var offsetPath = OffsetPath(path, offset, options.join || 'miter', options.limit || 10); - if (options.insert === undefined) { - options.insert = true; + var PaperOffset = /** @class */ (function () { + function PaperOffset() { } - if (options.insert) { - (path.parent || paper.project.activeLayer).addChild(offsetPath); - } - return offsetPath; - } - function PrototypedOffsetStroke(path, offset, options) { - options = options || {}; - var offsetPath = OffsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10); - if (options.insert === undefined) { - options.insert = true; - } - if (options.insert) { - (path.parent || paper.project.activeLayer).addChild(offsetPath); - } - return offsetPath; - } - function ExtendPaperJs(paper) { - paper.Path.prototype.offset = function (offset, options) { - return PrototypedOffset(this, offset, options); + PaperOffset.offset = function (path, offset, options) { + options = options || {}; + var newPath = offsetPath(path, offset, options.join || 'miter', options.limit || 10); + if (options.insert === undefined) { + options.insert = true; + } + if (options.insert) { + (path.parent || paper.project.activeLayer).addChild(newPath); + } + return newPath; + }; + PaperOffset.offsetStroke = function (path, offset, options) { + options = options || {}; + var newStroke = offsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10); + if (options.insert === undefined) { + options.insert = true; + } + if (options.insert) { + (path.parent || paper.project.activeLayer).addChild(newStroke); + } + return newStroke; + }; + return PaperOffset; + }()); + /** + * @deprecated EXTEND existing paper module is not recommend anymore + */ + function ExtendPaperJs(paperNs) { + paperNs.Path.prototype.offset = function (offset, options) { + return PaperOffset.offset(this, offset, options); }; - paper.Path.prototype.offsetStroke = function (offset, options) { - return PrototypedOffsetStroke(this, offset, options); + paperNs.Path.prototype.offsetStroke = function (offset, options) { + return PaperOffset.offsetStroke(this, offset, options); }; - paper.CompoundPath.prototype.offset = function (offset, options) { - return PrototypedOffset(this, offset, options); + paperNs.CompoundPath.prototype.offset = function (offset, options) { + return PaperOffset.offset(this, offset, options); }; - paper.CompoundPath.prototype.offsetStroke = function (offset, options) { - return PrototypedOffsetStroke(this, offset, options); + paperNs.CompoundPath.prototype.offsetStroke = function (offset, options) { + return PaperOffset.offsetStroke(this, offset, options); }; } ExtendPaperJs(paper); + window.PaperOffset = { + offset: PaperOffset.offset, + offsetStroke: PaperOffset.offsetStroke, + }; }(paper)); diff --git a/demo/paperjs-offset.min.js b/demo/paperjs-offset.min.js index 00bf908..1fe5e2a 100644 --- a/demo/paperjs-offset.min.js +++ b/demo/paperjs-offset.min.js @@ -1 +1 @@ -!function(w){"use strict";var y,u,n;function e(n,t,e){var r=function(e,r,i,o){var n=e.unite(e,{insert:!1}),t=n;if(n instanceof w.Path)t=u.OffsetSimpleShape(n,r,i,o);else{var a=y.Flat(n.children.map(function(n){if(1=t.length)return t;if(e){for(var r=[],i=t.slice(),o=0;o.1*Math.abs(i))switch(r){case"miter":var f=function(n,t,e,r){var i=n.subtract(t),o=e.subtract(r),a=v(n,t),u=v(e,r);return new w.Point(a*o.x-i.x*u,a*o.y-i.y*u).divide(v(i,o))}(a.point2,a.point2.add(a.getTangentAt(1,!0)),u.point1,u.point1.add(u.getTangentAt(0,!0)));Math.max(f.getDistance(a.point2),f.getDistance(u.point1))u){var s=t.divideAtTime(.5);if(null!=s)return n(t,e).concat(n(s,e))}}return[i,o]}(n,t)})),2),c=y.Flat(function(n,t,e,r,i){for(var o=e.segments,a=n[0].slice(),u=0;u.1*Math.abs(o))switch(r){case"miter":var M=(d=P.point2,h=P.point2.add(P.getTangentAtTime(1)),p=b.point1,v=b.point1.add(b.getTangentAtTime(0)),g=d.subtract(h),m=p.subtract(v),w=S(d,h),y=S(p,v),new O.Point(w*m.x-g.x*y,w*m.y-g.y*y).divide(S(g,m)));Math.max(M.getDistance(P.point2),M.getDistance(b.point1))u){var s=e.divideAtTime(.5);if(null!=s)return t(e,n).concat(t(s,n))}}return[o,i]}(t,e)})),2),h=v.Flat(function(t,e,n,r,o){for(var i=n.segments,a=t[0].slice(),u=0;u=e.length)return e;if(n){for(var r=[],o=e.slice(),i=0;i", + "author": "glenli ", "license": "MIT", "repository": { "type": "git", diff --git a/src/Offset.ts b/src/Offset.ts index 472c2c5..116d456 100644 --- a/src/Offset.ts +++ b/src/Offset.ts @@ -1,354 +1,356 @@ -import paper, { Point } from 'paper' -import { Arrayex } from 'arrayex' +import paper from 'paper'; +import { Arrayex } from 'arrayex'; -type HandleType = 'handleIn' | 'handleOut' -export type StrokeJoinType = 'miter' | 'bevel' | 'round' -export type StrokeCapType = 'round' | 'butt' -export type PathType = paper.Path | paper.CompoundPath +export type StrokeJoinType = 'miter' | 'bevel' | 'round'; +export type StrokeCapType = 'round' | 'butt'; +export type PathType = paper.Path | paper.CompoundPath; -namespace Offsets { - /** - * Offset the start/terminal segment of a bezier curve - * @param segment segment to offset - * @param curve curve to offset - * @param handleNormal the normal of the the line formed of two handles - * @param offset offset value - */ - function OffsetSegment(segment: paper.Segment, curve: paper.Curve, handleNormal: paper.Point, offset: number) { - let isFirst = segment.curve === curve - // get offset vector - let offsetVector = (curve.getNormalAt(isFirst ? 0 : 1, true)).multiply(offset) - // get offset point - let point = segment.point.add(offsetVector) - let newSegment = new paper.Segment(point) - // handleOut for start segment & handleIn for terminal segment - let handle = (isFirst ? 'handleOut' : 'handleIn') as HandleType - newSegment[handle] = segment[handle]!.add(handleNormal.subtract(offsetVector).divide(2)) - return newSegment - } +type HandleType = 'handleIn' | 'handleOut'; - /** - * Adaptive offset a curve by repeatly apply the approximation proposed by Tiller and Hanson. - * @param curve curve to offset - * @param offset offset value - */ - function AdaptiveOffsetCurve(curve: paper.Curve, offset: number): Array { - offset - let hNormal = (new paper.Curve(curve.segment1.handleOut!.add(curve.segment1.point), new paper.Point(0, 0), new paper.Point(0, 0), curve.segment2.handleIn!.add(curve.segment2.point))).getNormalAt(0.5, true).multiply(offset) - let segment1 = OffsetSegment(curve.segment1, curve, hNormal, offset) - let segment2 = OffsetSegment(curve.segment2, curve, hNormal, offset) - // divide && re-offset - let offsetCurve = new paper.Curve(segment1, segment2) - // if the offset curve is not self intersected, divide it - if (offsetCurve.getIntersections(offsetCurve).length === 0) { - let threshold = Math.min(Math.abs(offset) / 10, 1) - let midOffset = offsetCurve.getPointAt(0.5, true).getDistance(curve.getPointAt(0.5, true)) - if (Math.abs(midOffset - Math.abs(offset)) > threshold) { - let subCurve = curve.divideAtTime(0.5) - if (subCurve != null) { - return [...AdaptiveOffsetCurve(curve, offset), ...AdaptiveOffsetCurve(subCurve, offset)] - } +/** + * Offset the start/terminal segment of a bezier curve + * @param segment segment to offset + * @param curve curve to offset + * @param handleNormal the normal of the the line formed of two handles + * @param offset offset value + */ +function offsetSegment(segment: paper.Segment, curve: paper.Curve, handleNormal: paper.Point, offset: number) { + const isFirst = segment.curve === curve; + // get offset vector + const offsetVector = (curve.getNormalAtTime(isFirst ? 0 : 1)).multiply(offset); + // get offset point + const point = segment.point.add(offsetVector); + const newSegment = new paper.Segment(point); + // handleOut for start segment & handleIn for terminal segment + const handle = (isFirst ? 'handleOut' : 'handleIn') as HandleType; + newSegment[handle] = segment[handle]!.add(handleNormal.subtract(offsetVector).divide(2)); + return newSegment; +} + +/** + * Adaptive offset a curve by repeatly apply the approximation proposed by Tiller and Hanson. + * @param curve curve to offset + * @param offset offset value + */ +function adaptiveOffsetCurve(curve: paper.Curve, offset: number): paper.Segment[] { + const hNormal = (new paper.Curve(curve.segment1.handleOut!.add(curve.segment1.point), new paper.Point(0, 0), + new paper.Point(0, 0), curve.segment2.handleIn!.add(curve.segment2.point))).getNormalAtTime(0.5).multiply(offset); + const segment1 = offsetSegment(curve.segment1, curve, hNormal, offset); + const segment2 = offsetSegment(curve.segment2, curve, hNormal, offset); + // divide && re-offset + const offsetCurve = new paper.Curve(segment1, segment2); + // if the offset curve is not self intersected, divide it + if (offsetCurve.getIntersections(offsetCurve).length === 0) { + const threshold = Math.min(Math.abs(offset) / 10, 1); + const midOffset = offsetCurve.getPointAtTime(0.5).getDistance(curve.getPointAtTime(0.5)); + if (Math.abs(midOffset - Math.abs(offset)) > threshold) { + const subCurve = curve.divideAtTime(0.5); + if (subCurve != null) { + return [...adaptiveOffsetCurve(curve, offset), ...adaptiveOffsetCurve(subCurve, offset)]; } } - return [segment1, segment2] } + return [segment1, segment2]; +} - /** - * Create a round join segment between two adjacent segments. - */ - function MakeRoundJoin(segment1: paper.Segment, segment2: paper.Segment, originPoint: paper.Point, radius: number) { - let through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint)).normalize(Math.abs(radius)).add(originPoint) - let arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through, insert: false }) - segment1.handleOut = arc.firstSegment.handleOut - segment2.handleIn = arc.lastSegment.handleIn - return arc.segments.length === 3 ? arc.segments[1] : null - } +/** + * Create a round join segment between two adjacent segments. + */ +function makeRoundJoin(segment1: paper.Segment, segment2: paper.Segment, originPoint: paper.Point, radius: number) { + const through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint)) + .normalize(Math.abs(radius)).add(originPoint); + const arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through, insert: false }); + segment1.handleOut = arc.firstSegment.handleOut; + segment2.handleIn = arc.lastSegment.handleIn; + return arc.segments.length === 3 ? arc.segments[1] : null; +} - function Det(p1: paper.Point, p2: paper.Point) { - return p1.x * p2.y - p1.y * p2.x - } +function det(p1: paper.Point, p2: paper.Point) { + return p1.x * p2.y - p1.y * p2.x; +} - /** - * Get the intersection point of two point - */ - function Intersection(p1: paper.Point, p2: paper.Point, p3: paper.Point, p4: paper.Point) { - let l1 = p1.subtract(p2) - let l2 = p3.subtract(p4) - let dl1 = Det(p1, p2) - let dl2 = Det(p3, p4) - return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(Det(l1, l2)) - } +/** + * Get the intersection point of point based lines + */ +function getPointLineIntersections(p1: paper.Point, p2: paper.Point, p3: paper.Point, p4: paper.Point) { + const l1 = p1.subtract(p2); + const l2 = p3.subtract(p4); + const dl1 = det(p1, p2); + const dl2 = det(p3, p4); + return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(det(l1, l2)); +} - /** - * Connect two adjacent bezier curve, each curve is represented by two segments, create different types of joins or simply removal redundant segment. - */ - export function ConnectAdjacentBezier(segments1: Array, segments2: Array, origin: paper.Segment, join: StrokeJoinType, offset: number, limit: number) { - let curve1 = new paper.Curve(segments1[0], segments1[1]) - let curve2 = new paper.Curve(segments2[0], segments2[1]) - let intersection = curve1.getIntersections(curve2) - let distance = segments1[1].point.getDistance(segments2[0].point) - if (origin.isSmooth()) { - segments2[0].handleOut = segments2[0].handleOut!.project(origin.handleOut!) - segments2[0].handleIn = segments1[1].handleIn!.project(origin.handleIn!) - segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2) - segments1.pop() - } else { - if (intersection.length === 0) { - if (distance > Math.abs(offset) * 0.1) { - // connect - switch (join) { - case 'miter': - let join = Intersection(curve1.point2, curve1.point2.add(curve1.getTangentAt(1, true)), curve2.point1, curve2.point1.add(curve2.getTangentAt(0, true))) - // prevent sharp angle - let joinOffset = Math.max(join.getDistance(curve1.point2), join.getDistance(curve2.point1)) - if (joinOffset < Math.abs(offset) * limit) { - segments1.push(new paper.Segment(join)) - } - break - case 'round': - let mid = MakeRoundJoin(segments1[1], segments2[0], origin.point, offset) - if (mid) { - segments1.push(mid) - } - break - default: break - } - } else { - segments2[0].handleIn = segments1[1].handleIn - segments1.pop() +/** + * Connect two adjacent bezier curve, each curve is represented by two segments, + * create different types of joins or simply removal redundant segment. + */ +function connectAdjacentBezier(segments1: paper.Segment[], segments2: paper.Segment[], origin: paper.Segment, joinType: StrokeJoinType, offset: number, limit: number) { + const curve1 = new paper.Curve(segments1[0], segments1[1]); + const curve2 = new paper.Curve(segments2[0], segments2[1]); + const intersection = curve1.getIntersections(curve2); + const distance = segments1[1].point.getDistance(segments2[0].point); + if (origin.isSmooth()) { + segments2[0].handleOut = segments2[0].handleOut!.project(origin.handleOut!); + segments2[0].handleIn = segments1[1].handleIn!.project(origin.handleIn!); + segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2); + segments1.pop(); + } else { + if (intersection.length === 0) { + if (distance > Math.abs(offset) * 0.1) { + // connect + switch (joinType) { + case 'miter': + const join = getPointLineIntersections(curve1.point2, curve1.point2.add(curve1.getTangentAtTime(1)), + curve2.point1, curve2.point1.add(curve2.getTangentAtTime(0))); + // prevent sharp angle + const joinOffset = Math.max(join.getDistance(curve1.point2), join.getDistance(curve2.point1)); + if (joinOffset < Math.abs(offset) * limit) { + segments1.push(new paper.Segment(join)); + } + break; + case 'round': + const mid = makeRoundJoin(segments1[1], segments2[0], origin.point, offset); + if (mid) { + segments1.push(mid); + } + break; + default: break; } } else { - let second1 = curve1.divideAt(intersection[0]) - if (second1) { - let join = second1.segment1 - let second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]) - join.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut - segments1.pop() - segments2[0] = join - } else { - segments2[0].handleIn = segments1[1].handleIn - segments1.pop() - } + segments2[0].handleIn = segments1[1].handleIn; + segments1.pop(); + } + } else { + const second1 = curve1.divideAt(intersection[0]); + if (second1) { + const join = second1.segment1; + const second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]); + join.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut; + segments1.pop(); + segments2[0] = join; + } else { + segments2[0].handleIn = segments1[1].handleIn; + segments1.pop(); } } } +} - /** - * Connect all the segments together. - */ - function ConnectBeziers(rawSegments: Array>, join: StrokeJoinType, source: paper.Path, offset: number, limit: number) { - let originSegments = source.segments - let first = rawSegments[0].slice() - for (let i = 0; i < rawSegments.length - 1; ++i) { - ConnectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit) - } - if (source.closed) { - ConnectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit) - rawSegments[0][0] = first[0] - } - return rawSegments - } - - function Decompound(path: PathType) { - if (path.children.length === 1) { - path = path.children[0] as paper.Path - path.remove() // remove from parent, this is critical, or the style attributes will be ignored - } - return path +/** + * Connect all the segments together. + */ +function connectBeziers(rawSegments: paper.Segment[][], join: StrokeJoinType, source: paper.Path, offset: number, limit: number) { + const originSegments = source.segments; + const first = rawSegments[0].slice(); + for (let i = 0; i < rawSegments.length - 1; ++i) { + connectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit); } - - /** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */ - export function Normalize(path: PathType, areaThreshold = 0.01) { - if (path.closed) { - let ignoreArea = Math.abs(path.area * areaThreshold) - if (!path.clockwise) { - path.reverse() - } - path = path.unite(path, { insert: false }) as PathType - if (path instanceof paper.CompoundPath) { - path.children.filter(c => Math.abs((c as PathType).area) < ignoreArea).forEach(c => c.remove()) - if (path.children.length === 1) { - return Decompound(path) - } - } - } - return path + if (source.closed) { + connectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit); + rawSegments[0][0] = first[0]; } + return rawSegments; +} - export function IsSameDirection(partialPath: paper.Path, fullPath: PathType) { - let offset1 = partialPath.segments[0].location.offset - let offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset - let sampleOffset = (offset1 + offset2) / 3 - let originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset - let originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset - return originOffset1 < originOffset2 +function reduceSingleChildCompoundPath(path: PathType) { + if (path.children.length === 1) { + path = path.children[0] as paper.Path; + path.remove(); // remove from parent, this is critical, or the style attributes will be ignored } + return path; +} - /** Remove self intersection when offset is negative by point direction dectection. */ - export function RemoveIntersection(path: PathType) { - let newPath = path.unite(path, { insert: false }) as PathType - if (newPath instanceof paper.CompoundPath) { - (newPath.children as Array).filter(c => { - if (c.segments.length > 1) { - return !IsSameDirection(c, path) - } else { - return true - } - }).forEach(c => c.remove()) - return Decompound(newPath) +/** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */ +function normalize(path: PathType, areaThreshold = 0.01) { + if (path.closed) { + const ignoreArea = Math.abs(path.area * areaThreshold); + if (!path.clockwise) { + path.reverse(); } - return path - } - - function Segments(path: PathType) { + path = path.unite(path, { insert: false }) as PathType; if (path instanceof paper.CompoundPath) { - return Arrayex.Flat(path.children.map(c => (c as paper.Path).segments)) - } else { - return (path as paper.Path).segments + path.children.filter((c) => Math.abs((c as PathType).area) < ignoreArea).forEach((c) => c.remove()); + if (path.children.length === 1) { + return reduceSingleChildCompoundPath(path); + } } } + return path; +} + +function isSameDirection(partialPath: paper.Path, fullPath: PathType) { + const offset1 = partialPath.segments[0].location.offset; + const offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset; + const sampleOffset = (offset1 + offset2) / 3; + const originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset; + const originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset; + return originOffset1 < originOffset2; +} - /** - * Remove impossible segments in negative offset condition. - */ - function RemoveOutsiders(offsetPath: PathType, path: PathType) { - let segments = Segments(offsetPath).slice() - segments.forEach(segment => { - if (!path.contains(segment.point)) { - segment.remove() +/** Remove self intersection when offset is negative by point direction dectection. */ +function removeIntersection(path: PathType) { + const newPath = path.unite(path, { insert: false }) as PathType; + if (newPath instanceof paper.CompoundPath) { + (newPath.children as paper.Path[]).filter((c) => { + if (c.segments.length > 1) { + return !isSameDirection(c, path); + } else { + return true; } - }) + }).forEach((c) => c.remove()); + return reduceSingleChildCompoundPath(newPath); } + return path; +} - function PreparePath(path: paper.Path, offset: number): [paper.Path, number] { - let source = path.clone({ insert: false }) as paper.Path - source.reduce() - if (!path.clockwise) { - source.reverse() - offset = -offset - } - return [source, offset] +function getSegments(path: PathType) { + if (path instanceof paper.CompoundPath) { + return Arrayex.Flat(path.children.map((c) => (c as paper.Path).segments)); + } else { + return (path as paper.Path).segments; } +} - export function OffsetSimpleShape(path: paper.Path, offset: number, join: StrokeJoinType, limit: number): PathType { - let source: paper.Path - [source, offset] = PreparePath(path, offset) - let curves = source.curves.slice() - let raws = Arrayex.Divide(Arrayex.Flat(curves.map(curve => AdaptiveOffsetCurve(curve, offset))), 2) - let segments = Arrayex.Flat(ConnectBeziers(raws, join, source, offset, limit)) - let offsetPath = RemoveIntersection(new paper.Path({ segments, insert: false, closed: path.closed })) - offsetPath.reduce() - if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) { - RemoveOutsiders(offsetPath, path) - } - // recovery path - if (source.clockwise !== path.clockwise) { - offsetPath.reverse() +/** + * Remove impossible segments in negative offset condition. + */ +function removeOutsiders(newPath: PathType, path: PathType) { + const segments = getSegments(newPath).slice(); + segments.forEach((segment) => { + if (!path.contains(segment.point)) { + segment.remove(); } - return Normalize(offsetPath) + }); +} + +function preparePath(path: paper.Path, offset: number): [paper.Path, number] { + const source = path.clone({ insert: false }) as paper.Path; + source.reduce({}); + if (!path.clockwise) { + source.reverse(); + offset = -offset; } + return [source, offset]; +} - function MakeRoundCap(from: paper.Segment, to: paper.Segment, offset: number) { - let origin = from.point.add(to.point).divide(2) - let normal = to.point.subtract(from.point).rotate(-90).normalize(offset) - let through = origin.add(normal) - let arc = new paper.Path.Arc({ from: from.point, to: to.point, through, insert: false }) - return arc.segments +function offsetSimpleShape(path: paper.Path, offset: number, join: StrokeJoinType, limit: number): PathType { + let source: paper.Path; + [source, offset] = preparePath(path, offset); + const curves = source.curves.slice(); + const raws = Arrayex.Divide(Arrayex.Flat(curves.map((curve) => adaptiveOffsetCurve(curve, offset))), 2); + const segments = Arrayex.Flat(connectBeziers(raws, join, source, offset, limit)); + const newPath = removeIntersection(new paper.Path({ segments, insert: false, closed: path.closed })); + newPath.reduce({}); + if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) { + removeOutsiders(newPath, path); + } + // recovery path + if (source.clockwise !== path.clockwise) { + newPath.reverse(); } + return normalize(newPath); +} - function ConnectSide(outer: PathType, inner: paper.Path, offset: number, cap: StrokeCapType): paper.Path { - if (outer instanceof paper.CompoundPath) { - let cs = outer.children.map(c => ({ c, a: Math.abs((c as paper.Path).area) })) - cs = cs.sort((c1, c2) => c2.a - c1.a) - outer = cs[0].c as paper.Path - } - let oSegments = (outer as paper.Path).segments.slice() - let iSegments = inner.segments.slice() - switch (cap) { - case 'round': - let heads = MakeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset) - let tails = MakeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset) - let result = new paper.Path({ segments: [...heads, ...oSegments, ...tails, ...iSegments], closed: true, insert: false }) - result.reduce() - return result - default: return new paper.Path({ segments: [...oSegments, ...iSegments], closed: true, insert: false }) - } +function makeRoundCap(from: paper.Segment, to: paper.Segment, offset: number) { + const origin = from.point.add(to.point).divide(2); + const normal = to.point.subtract(from.point).rotate(-90, new paper.Point(0, 0)).normalize(offset); + const through = origin.add(normal); + const arc = new paper.Path.Arc({ from: from.point, to: to.point, through, insert: false }); + return arc.segments; +} + +function connectSide(outer: PathType, inner: paper.Path, offset: number, cap: StrokeCapType): paper.Path { + if (outer instanceof paper.CompoundPath) { + let cs = outer.children.map((c) => ({ c, a: Math.abs((c as paper.Path).area) })); + cs = cs.sort((c1, c2) => c2.a - c1.a); + outer = cs[0].c as paper.Path; + } + const oSegments = (outer as paper.Path).segments.slice(); + const iSegments = inner.segments.slice(); + switch (cap) { + case 'round': + const heads = makeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset); + const tails = makeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset); + const result = new paper.Path({ segments: [...heads, ...oSegments, ...tails, ...iSegments], closed: true, insert: false }); + result.reduce({}); + return result; + default: return new paper.Path({ segments: [...oSegments, ...iSegments], closed: true, insert: false }); } +} - export function OffsetSimpleStroke(path: paper.Path, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number): PathType { - offset = path.clockwise ? offset : -offset - let positiveOffset = OffsetSimpleShape(path, offset, join, limit) - let negativeOffset = OffsetSimpleShape(path, -offset, join, limit) - if (path.closed) { - return positiveOffset.subtract(negativeOffset, { insert: false }) as PathType - } else { - let inner = negativeOffset - let holes = new Array() - if (negativeOffset instanceof paper.CompoundPath) { - holes = negativeOffset.children.filter(c => (c as paper.Path).closed) as Array - holes.forEach(h => h.remove()) - inner = negativeOffset.children[0] as paper.Path - } - inner.reverse() - let final = ConnectSide(positiveOffset, inner as paper.Path, offset, cap) as PathType - if (holes.length > 0) { - for(let hole of holes) { - final = final.subtract(hole, { insert: false }) as PathType - } +function offsetSimpleStroke(path: paper.Path, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number): PathType { + offset = path.clockwise ? offset : -offset; + const positiveOffset = offsetSimpleShape(path, offset, join, limit); + const negativeOffset = offsetSimpleShape(path, -offset, join, limit); + if (path.closed) { + return positiveOffset.subtract(negativeOffset, { insert: false }) as PathType; + } else { + let inner = negativeOffset; + let holes = new Array(); + if (negativeOffset instanceof paper.CompoundPath) { + holes = negativeOffset.children.filter((c) => (c as paper.Path).closed) as paper.Path[]; + holes.forEach((h) => h.remove()); + inner = negativeOffset.children[0] as paper.Path; + } + inner.reverse(); + let final = connectSide(positiveOffset, inner as paper.Path, offset, cap) as PathType; + if (holes.length > 0) { + for (const hole of holes) { + final = final.subtract(hole, { insert: false }) as PathType; } - return final } + return final; } } -export function OffsetPath(path: PathType, offset: number, join: StrokeJoinType, limit: number) { - let nonSIPath = path.unite(path, { insert: false }) as PathType - let result = nonSIPath +export function offsetPath(path: PathType, offset: number, join: StrokeJoinType, limit: number): PathType { + const nonSIPath = path.unite(path, { insert: false }) as PathType; + let result = nonSIPath; if (nonSIPath instanceof paper.Path) { - result = Offsets.OffsetSimpleShape(nonSIPath, offset, join, limit) + result = offsetSimpleShape(nonSIPath, offset, join, limit); } else { - let children = Arrayex.Flat((nonSIPath.children as Array).map(c => { + const children = Arrayex.Flat((nonSIPath.children as paper.Path[]).map((c) => { if (c.segments.length > 1) { - if (!Offsets.IsSameDirection(c, path)) { - c.reverse() + if (!isSameDirection(c, path)) { + c.reverse(); } - let offseted = Offsets.OffsetSimpleShape(c, offset, join, limit) - offseted = Offsets.Normalize(offseted) + let offseted = offsetSimpleShape(c, offset, join, limit); + offseted = normalize(offseted); if (offseted.clockwise !== c.clockwise) { - offseted.reverse() + offseted.reverse(); } if (offseted instanceof paper.CompoundPath) { - offseted.applyMatrix = true - return offseted.children + offseted.applyMatrix = true; + return offseted.children; } else { - return offseted + return offseted; } } else { - return null + return null; } - }), false) - result = new paper.CompoundPath({ children, insert: false }) + }), false); + result = new paper.CompoundPath({ children, insert: false }); } - result.copyAttributes(nonSIPath, false) - result.remove() - return result + result.copyAttributes(nonSIPath, false); + result.remove(); + return result; } -export function OffsetStroke(path: PathType, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number) { - let nonSIPath = path.unite(path, { insert: false }) as PathType - let result = nonSIPath as PathType +export function offsetStroke(path: PathType, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number): PathType { + const nonSIPath = path.unite(path, { insert: false }) as PathType; + let result = nonSIPath as PathType; if (nonSIPath instanceof paper.Path) { - result = Offsets.OffsetSimpleStroke(nonSIPath, offset, join, cap, limit) + result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit); } else { - let children = Arrayex.Flat((nonSIPath.children as Array).map(c => { - return Offsets.OffsetSimpleStroke(c, offset, join, cap, limit) - })) - result = children.reduce((c1, c2) => c1.unite(c2, { insert: false }) as PathType) + const children = Arrayex.Flat((nonSIPath.children as paper.Path[]).map((c) => { + return offsetSimpleStroke(c, offset, join, cap, limit); + })); + result = children.reduce((c1, c2) => c1.unite(c2, { insert: false }) as PathType); } - result.strokeWidth = 0 - result.fillColor = nonSIPath.strokeColor - result.shadowBlur = nonSIPath.shadowBlur - result.shadowColor = nonSIPath.shadowColor - result.shadowOffset = nonSIPath.shadowOffset - return result + result.strokeWidth = 0; + result.fillColor = nonSIPath.strokeColor; + result.shadowBlur = nonSIPath.shadowBlur; + result.shadowColor = nonSIPath.shadowColor; + result.shadowOffset = nonSIPath.shadowOffset; + return result; } diff --git a/src/bundle.ts b/src/bundle.ts index bc19f47..36275da 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -1,4 +1,18 @@ -import paper from 'paper' -import ExtendPaperJs from './index' +import paper from 'paper'; +import ExtendPaperJs, { PaperOffset } from './index'; -ExtendPaperJs(paper) \ No newline at end of file +ExtendPaperJs(paper); + +declare global { + interface Window { + PaperOffset: { + offset: typeof PaperOffset.offset; + offsetStroke: typeof PaperOffset.offsetStroke; + } + } +} + +window.PaperOffset = { + offset: PaperOffset.offset, + offsetStroke: PaperOffset.offsetStroke, +}; diff --git a/src/index.ts b/src/index.ts index 42fd102..4ae3332 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,63 +1,56 @@ -import paper from 'paper' -import { OffsetPath, StrokeJoinType, PathType, StrokeCapType, OffsetStroke } from './Offset' +import paper from 'paper'; +import { StrokeJoinType, PathType, StrokeCapType, offsetPath, offsetStroke } from './offset'; export interface OffsetOptions { - join?: StrokeJoinType, - cap?: StrokeCapType, - limit?: number, - insert?: boolean, + join?: StrokeJoinType; + cap?: StrokeCapType; + limit?: number; + insert?: boolean; } -declare module 'paper' { - interface Path { - offset(offset: number, options?: OffsetOptions): PathType - offsetStroke(offset: number, options?: OffsetOptions): PathType | paper.Group - } - - interface CompoundPath { - offset(offset: number, options?: OffsetOptions): PathType - offsetStroke(offset: number, options?: OffsetOptions): PathType | paper.Group +export class PaperOffset { + public static offset(path: PathType, offset: number, options?: OffsetOptions): PathType { + options = options || {}; + const newPath = offsetPath(path, offset, options.join || 'miter', options.limit || 10); + if (options.insert === undefined) { + options.insert = true; + } + if (options.insert) { + (path.parent || paper.project.activeLayer).addChild(newPath); + } + return newPath; + } + + public static offsetStroke(path: PathType, offset: number, options?: OffsetOptions): PathType { + options = options || {}; + const newStroke = offsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10); + if (options.insert === undefined) { + options.insert = true; + } + if (options.insert) { + (path.parent || paper.project.activeLayer).addChild(newStroke); + } + return newStroke; } } -function PrototypedOffset(path: PathType, offset: number, options?: OffsetOptions) { - options = options || {} - let offsetPath = OffsetPath(path, offset, options.join || 'miter', options.limit || 10) - if (options.insert === undefined) { - options.insert = true - } - if (options.insert) { - (path.parent || paper.project.activeLayer).addChild(offsetPath) - } - return offsetPath -} - -function PrototypedOffsetStroke(path: PathType, offset: number, options?: OffsetOptions) { - options = options || {} - let offsetPath = OffsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10) - if (options.insert === undefined) { - options.insert = true - } - if (options.insert) { - (path.parent || paper.project.activeLayer).addChild(offsetPath) - } - return offsetPath -} - -export default function ExtendPaperJs(paper: any) { - paper.Path.prototype.offset = function(offset: number, options?: OffsetOptions) { - return PrototypedOffset(this, offset, options) - } - - paper.Path.prototype.offsetStroke = function (offset: number, options?: OffsetOptions) { - return PrototypedOffsetStroke(this, offset, options) - } - - paper.CompoundPath.prototype.offset = function(offset: number, options?: OffsetOptions) { - return PrototypedOffset(this, offset, options) - } - - paper.CompoundPath.prototype.offsetStroke = function (offset: number, options?: OffsetOptions) { - return PrototypedOffsetStroke(this, offset, options) - } +/** + * @deprecated EXTEND existing paper module is not recommend anymore + */ +export default function ExtendPaperJs(paperNs: any) { + paperNs.Path.prototype.offset = function(offset: number, options?: OffsetOptions) { + return PaperOffset.offset(this, offset, options); + }; + + paperNs.Path.prototype.offsetStroke = function(offset: number, options?: OffsetOptions) { + return PaperOffset.offsetStroke(this, offset, options); + }; + + paperNs.CompoundPath.prototype.offset = function(offset: number, options?: OffsetOptions) { + return PaperOffset.offset(this, offset, options); + }; + + paperNs.CompoundPath.prototype.offsetStroke = function(offset: number, options?: OffsetOptions) { + return PaperOffset.offsetStroke(this, offset, options); + }; } diff --git a/tslint.json b/tslint.json index cb6b60e..bffe99b 100644 --- a/tslint.json +++ b/tslint.json @@ -10,25 +10,8 @@ }, "rules": { "quotemark": [true, "single"], - "indent": [true, "spaces", 2], - "semicolon": [true, "never"], "interface-name": false, "ordered-imports": false, - "object-literal-sort-keys": false, - "no-consecutive-blank-lines": false, - "prefer-const": false, - "no-console": false, - "no-namespace": false, - "arrow-parens": false, - "array-type": [true, "generic"], - "variable-name": [true, "check-format", "allow-leading-underscore", "allow-pascal-case"], - "max-line-length": [true, 280], - "member-access": [true, "no-public"], - "no-empty": [true, "allow-empty-catch", "allow-empty-functions"], - "no-shadowed-variable": false, - "interface-over-type-literal": false, - "member-ordering": false, - "max-classes-per-file": false, - "no-bitwise": false + "max-line-length": [true, 180] } }