diff --git a/build.js b/build.js index 0a3bf06..4588637 100644 --- a/build.js +++ b/build.js @@ -22,7 +22,7 @@ let tasks = [] tasks.push(rollup.rollup({ input: 'src/index.ts', - external: ['arrayex', 'paper'], + external: ['paper'], plugins: defaultPlugins }).then(bundle => { bundle.write({ format: 'umd', file: pkg.main, name: camelCase(libraryName), sourcemap: true }) diff --git a/demo/debug.html b/demo/debug.html new file mode 100644 index 0000000..49c34b8 --- /dev/null +++ b/demo/debug.html @@ -0,0 +1,12 @@ + + + Demo + + + + + + + + + \ No newline at end of file diff --git a/demo/debug.js b/demo/debug.js new file mode 100644 index 0000000..a9d0f36 --- /dev/null +++ b/demo/debug.js @@ -0,0 +1,23 @@ +(function() { + function DebugCase() { + let canvas = document.querySelector('canvas'); + paper.setup(canvas); + paper.view.center = [0, 0]; + + const c3 = new paper.Path.Circle({ center: [180, 260], radius: 50, strokeColor: 'black' }); + const c4 = new paper.Path.Circle({ center: [230, 260], radius: 40, strokeColor: 'black' }); + const c5 = new paper.Path.Circle({ center: [205, 200], radius: 40, strokeColor: 'black' }); + const cc1 = c3.unite(c4, { insert: true }); + const cc = cc1.unite(c5, { insert: true }); + c3.remove(); + c4.remove(); + c5.remove(); + cc1.remove(); + cc.bringToFront(); + cc.translate(new paper.Point(-100, -100)); + + PaperOffset.offset(cc, 24); + } + + window.onload = DebugCase; +})(); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 0ba69a1..0a2ec01 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,7 +2,7 @@ Demo - + diff --git a/demo/paperjs-offset.js b/demo/paperjs-offset.js index 43c3ba4..bea5a93 100644 --- a/demo/paperjs-offset.js +++ b/demo/paperjs-offset.js @@ -1,660 +1,402 @@ (function (paper) { - 'use strict'; + 'use strict'; - paper = paper && Object.prototype.hasOwnProperty.call(paper, 'default') ? paper['default'] : paper; + paper = paper && Object.prototype.hasOwnProperty.call(paper, 'default') ? paper['default'] : paper; - var Arrayex; - (function (Arrayex) { - function Delete(array, item, deleteAll) { - if (deleteAll === void 0) { deleteAll = false; } - if (deleteAll) { - var predicate = (typeof item === 'function') ? (function (v, i, a) { return !item(v, i, a); }) : (function (v) { return v !== item; }); - var afterDeleted = array.filter(predicate); - if (afterDeleted.length < array.length) { - array.splice.apply(array, [0, array.length].concat(afterDeleted)); - return true; - } - } - else { - var index = (typeof item === 'function') ? array.findIndex(item) : array.indexOf(item); - if (index > -1) { - array.splice(index, 1); - return true; - } - } - return false; - } - Arrayex.Delete = Delete; - function BatchDelete(array, items, deleteAll) { - if (deleteAll === void 0) { deleteAll = false; } - items.forEach(function (item) { Delete(array, item, deleteAll); }); - } - Arrayex.BatchDelete = BatchDelete; - function Replace(array, item, newItem, replaceAll) { - if (replaceAll === void 0) { replaceAll = false; } - if (array != null) { - var replaced = false; - do { - var index = (typeof item === 'function') ? array.findIndex(item) : array.indexOf(item); - if (index > -1) { - array[index] = newItem; - replaced = true; - } - else { - break; - } - } while (replaceAll); - return replaced; - } - return false; - } - Arrayex.Replace = Replace; - function Find(array, predicate) { - var index = array.findIndex(predicate); - return array[index]; - } - Arrayex.Find = Find; - /** - * IncludeSome() determines if at least one element in items is included in array. - */ - function IncludeSome(array, items) { - return items.some(function (item) { return array.includes(item); }); - } - Arrayex.IncludeSome = IncludeSome; - function OrderedInsert(orderedArray, item, compare, unique) { - if (unique === void 0) { unique = false; } - var index = orderedArray.findIndex(function (val) { return compare(val, item) >= 0; }); - if (index > -1) { - var exist = compare(orderedArray[index], item) === 0; - orderedArray.splice(index, (exist && unique) ? 1 : 0, item); - return index; - } - else { - orderedArray.push(item); - return orderedArray.length - 1; - } - } - Arrayex.OrderedInsert = OrderedInsert; - /** - * InsertBefore() inserts an item into array before nextItem. insert to the head of array if nextItem doesn't exist. - */ - function InsertBefore(array, item, nextItem) { - var index = array.indexOf(nextItem); - array.splice(Math.max(index, 0), 0, item); - } - Arrayex.InsertBefore = InsertBefore; - /** - * InsertAfter() inserts an item into array after prevItem. insert to the tail of array if prevItem doesn't exist. - */ - function InsertAfter(array, item, prevItem) { - var index = array.indexOf(prevItem); - if (index > -1) { - array.splice(index + 1, 0, item); - } - else { - array.push(prevItem); - } - } - Arrayex.InsertAfter = InsertAfter; - function ApproximateIndex(array, value, epsilon) { - if (epsilon === void 0) { epsilon = 0; } - return array.findIndex(function (val) { return Math.abs(val - value) <= epsilon; }); - } - Arrayex.ApproximateIndex = ApproximateIndex; - function Approximate(array, value, epsilon) { - if (epsilon === void 0) { epsilon = 0; } - var index = array.findIndex(function (val) { return Math.abs(val - value) <= epsilon; }); - return array[index]; - } - Arrayex.Approximate = Approximate; - function Empty(n) { - return n > 0 ? new Array(n).fill(null) : []; - } - Arrayex.Empty = Empty; - function Create(n, creator) { - return n > 0 ? new Array(n).fill(null).map(function (_, index) { return creator(index, n); }) : []; - } - Arrayex.Create = Create; - /** - * Repeat() repeats each element in sequence n times. e.g. Repeat(2, 1, 2) => [1, 1, 2, 2] - */ - function Repeat(n) { - var sequence = []; - for (var _i = 1; _i < arguments.length; _i++) { - sequence[_i - 1] = arguments[_i]; - } - return Create(n * sequence.length, function (index) { return sequence[Math.floor(index / n)]; }); - } - Arrayex.Repeat = Repeat; - /** - * RepeatSequence() repeats full sequence n tims. e.g. Repeat(2, 1, 2) => [1, 2, 1, 2] - */ - function RepeatSequence(n) { - var sequence = []; - for (var _i = 1; _i < arguments.length; _i++) { - sequence[_i - 1] = arguments[_i]; - } - return Create(n * sequence.length, function (index) { return sequence[index % sequence.length]; }); - } - Arrayex.RepeatSequence = RepeatSequence; - /** - * AnalyzePeriod() returns the minimum preriod of give array. - */ - function AnalyzePeriod(array) { - var threshold = Math.ceil(Math.sqrt(array.length)); - for (var length_1 = 1; length_1 <= threshold; ++length_1) { - if (array.length % length_1 === 0) { - var sample = array.slice(0, length_1); - var validate = true; - for (var i = length_1; i < array.length; ++i) { - if (array[i] !== sample[i % length_1]) { - validate = false; - break; - } - } - if (validate) { - return sample; - } - } - } - return array; - } - Arrayex.AnalyzePeriod = AnalyzePeriod; - /** - * Flat() flats the array inside an array. - */ - function Flat(array, keepNull) { - if (keepNull === void 0) { keepNull = false; } - var flat = Array.prototype.concat.apply([], array); - return keepNull ? flat : flat.filter(function (v) { return v != null; }); - } - Arrayex.Flat = Flat; - /** - * Divide() divides a given array into partitions, each partition contains count element except the last one. - */ - function Divide(array, count) { - var divides = []; - for (var i = 0; i < array.length; i += count) { - divides.push(array.slice(i, i + count)); - } - return divides; - } - Arrayex.Divide = Divide; - /** - * Group() groups items in array into a directMap by a groupBy function. - */ - function Group(array, groupBy) { - var groups = {}; - array.forEach(function (item, index) { - var group = groupBy(item, index, array); - if (!groups[group]) { - groups[group] = []; - } - groups[group].push(item); - }); - return groups; - } - Arrayex.Group = Group; - /** - * Sample() samples a subarray of count in array. - */ - function Sample(array, count, random) { - if (random === void 0) { random = false; } - if (count >= array.length) { - return array; - } - else { - if (!random) { - var interval_1 = array.length / count; - return Create(count, function (index) { return array[Math.round(interval_1 * index)]; }); - } - else { - var result = []; - var copy = array.slice(); - for (var i = 0; i < count; ++i) { - var item = copy.splice(Math.round(copy.length * Math.random()), 1)[0]; - result.push(item); - } - return result; - } - } - } - Arrayex.Sample = Sample; - function Union(arrays, unique) { - if (unique === void 0) { unique = false; } - var union = Array.prototype.concat.apply([], arrays); - return unique ? Array.from(new Set(union)) : union; - } - Arrayex.Union = Union; - function Intersect(arrays) { - if (arrays.length > 1) { - var intersectMap_1 = new Map(); - arrays[0].forEach(function (v) { return intersectMap_1.set(v, 1); }); - for (var i = 1; i < arrays.length; ++i) { - arrays[i].forEach(function (item) { - if (intersectMap_1.has(item)) { - intersectMap_1.set(item, intersectMap_1.get(item) + 1); - } - }); - } - var intersect_1 = new Array(); - intersectMap_1.forEach(function (value, item) { - if (value === arrays.length) { - intersect_1.push(item); - } - }); - return intersect_1; - } - else { - return arrays[0] || []; - } - } - Arrayex.Intersect = Intersect; - function Subtract(array, arrays) { - if (arrays.length > 1) { - var union_1 = new Set(Array.prototype.concat.apply([], arrays)); - return array.filter(function (item) { return !union_1.has(item); }); - } - else { - return array || []; - } - } - Arrayex.Subtract = Subtract; - function NonNull(array) { - return array.filter(function (i) { return i != null; }); - } - Arrayex.NonNull = NonNull; - })(Arrayex || (Arrayex = {})); + /** + * 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 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(); + } + 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 { + 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 { + segments2[0].handleIn = segments1[1].handleIn; + segments1.pop(); + } + } + } + } + /** + * 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(); + } + 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 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); + } + else { + return true; + } + }).forEach(function (c) { return c.remove(); }); + return reduceSingleChildCompoundPath(newPath); + } + return path; + } + function getSegments(path) { + if (path instanceof paper.CompoundPath) { + return path.children.map(function (c) { return c.segments; }).flat(); + } + else { + return path.segments; + } + } + /** + * 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 offsetCurves = curves.map(function (curve) { return adaptiveOffsetCurve(curve, offset); }).flat(); + var raws = []; + for (var i = 0; i < offsetCurves.length; i += 2) { + raws.push(offsetCurves.slice(i, i + 2)); + } + var segments = connectBeziers(raws, join, source, offset, limit).flat(); + 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 }); + } + } + return final; + } + } + function offsetPath(path, offset, join, limit) { + var nonSIPath = path.unite(path, { insert: false }); + var result = nonSIPath; + if (nonSIPath instanceof paper.Path) { + result = offsetSimpleShape(nonSIPath, offset, join, limit); + } + else { + var offsetParts = nonSIPath.children.map(function (c) { + if (c.segments.length > 1) { + if (!isSameDirection(c, path)) { + c.reverse(); + } + var offseted = offsetSimpleShape(c, offset, join, limit); + offseted = normalize(offseted); + if (offseted.clockwise !== c.clockwise) { + offseted.reverse(); + } + if (offseted instanceof paper.CompoundPath) { + offseted.applyMatrix = true; + return offseted.children; + } + else { + return offseted; + } + } + else { + return null; + } + }); + var children = offsetParts.flat().filter(function (c) { return !!c; }); + result = new paper.CompoundPath({ children: children, insert: false }); + } + result.copyAttributes(nonSIPath, false); + result.remove(); + return result; + } + function offsetStroke(path, offset, join, cap, limit) { + var nonSIPath = path.unite(path, { insert: false }); + var result = nonSIPath; + if (nonSIPath instanceof paper.Path) { + result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit); + } + else { + var children = nonSIPath.children.flatMap(function (c) { + return offsetSimpleStroke(c, offset, join, cap, limit); + }); + result = children.reduce(function (c1, c2) { return c1.unite(c2, { insert: false }); }); + } + result.strokeWidth = 0; + result.fillColor = nonSIPath.strokeColor; + result.shadowBlur = nonSIPath.shadowBlur; + result.shadowColor = nonSIPath.shadowColor; + result.shadowOffset = nonSIPath.shadowOffset; + return result; + } - /** - * 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 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(); - } - 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 { - 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 { - segments2[0].handleIn = segments1[1].handleIn; - segments1.pop(); - } - } - } - } - /** - * 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(); - } - 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 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); - } - else { - return true; - } - }).forEach(function (c) { return c.remove(); }); - return reduceSingleChildCompoundPath(newPath); - } - return path; - } - function getSegments(path) { - if (path instanceof paper.CompoundPath) { - return Arrayex.Flat(path.children.map(function (c) { return c.segments; })); - } - else { - return path.segments; - } - } - /** - * 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 }); - } - } - return final; - } - } - function offsetPath(path, offset, join, limit) { - var nonSIPath = path.unite(path, { insert: false }); - var result = nonSIPath; - if (nonSIPath instanceof paper.Path) { - result = offsetSimpleShape(nonSIPath, offset, join, limit); - } - else { - var children = Arrayex.Flat(nonSIPath.children.map(function (c) { - if (c.segments.length > 1) { - if (!isSameDirection(c, path)) { - c.reverse(); - } - var offseted = offsetSimpleShape(c, offset, join, limit); - offseted = normalize(offseted); - if (offseted.clockwise !== c.clockwise) { - offseted.reverse(); - } - if (offseted instanceof paper.CompoundPath) { - offseted.applyMatrix = true; - return offseted.children; - } - else { - return offseted; - } - } - else { - return null; - } - }), false); - result = new paper.CompoundPath({ children: children, insert: false }); - } - result.copyAttributes(nonSIPath, false); - result.remove(); - return result; - } - function offsetStroke(path, offset, join, cap, limit) { - var nonSIPath = path.unite(path, { insert: false }); - var result = nonSIPath; - if (nonSIPath instanceof paper.Path) { - result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit); - } - else { - var children = Arrayex.Flat(nonSIPath.children.map(function (c) { - return offsetSimpleStroke(c, offset, join, cap, limit); - })); - result = children.reduce(function (c1, c2) { return c1.unite(c2, { insert: false }); }); - } - result.strokeWidth = 0; - result.fillColor = nonSIPath.strokeColor; - result.shadowBlur = nonSIPath.shadowBlur; - result.shadowColor = nonSIPath.shadowColor; - result.shadowOffset = nonSIPath.shadowOffset; - return result; - } + var PaperOffset = /** @class */ (function () { + function PaperOffset() { + } + 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 newPath = 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(newPath); + } + return newPath; + }; + 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); + }; + paperNs.Path.prototype.offsetStroke = function (offset, options) { + return PaperOffset.offsetStroke(this, offset, options); + }; + paperNs.CompoundPath.prototype.offset = function (offset, options) { + return PaperOffset.offset(this, offset, options); + }; + paperNs.CompoundPath.prototype.offsetStroke = function (offset, options) { + return PaperOffset.offsetStroke(this, offset, options); + }; + } - var PaperOffset = /** @class */ (function () { - function PaperOffset() { - } - 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); - }; - paperNs.Path.prototype.offsetStroke = function (offset, options) { - return PaperOffset.offsetStroke(this, offset, options); - }; - paperNs.CompoundPath.prototype.offset = function (offset, options) { - return PaperOffset.offset(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, - }; + 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 1fe5e2a..0cf9172 100644 --- a/demo/paperjs-offset.min.js +++ b/demo/paperjs-offset.min.js @@ -1 +1 @@ -!function(O){"use strict";var v,t;function r(t,r,e){if(void 0===e&&(e=!1),e){var n="function"==typeof r?function(t,e,n){return!r(t,e,n)}:function(t){return t!==r},o=t.filter(n);if(o.length.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.1*Math.abs(o))switch(r){case"miter":var A=(d=b.point2,h=b.point2.add(b.getTangentAtTime(1)),p=k.point1,m=k.point1.add(k.getTangentAtTime(0)),g=d.subtract(h),v=p.subtract(m),w=T(d,h),P=T(p,m),new S.Point(w*v.x-g.x*P,w*v.y-g.y*P).divide(T(g,v)));Math.max(A.getDistance(b.point2),A.getDistance(k.point1))s){var u=t.divideAtTime(.5);if(null!=u)return e(t,n).concat(e(u,n))}}return[o,i]}(e,t)}).flat(),f=[],l=0;l(path.children.map((c) => (c as paper.Path).segments)); + return path.children.map((c) => (c as paper.Path).segments).flat(); } else { return (path as paper.Path).segments; } @@ -237,8 +236,12 @@ function offsetSimpleShape(path: paper.Path, offset: number, join: StrokeJoinTyp 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 offsetCurves = curves.map((curve) => adaptiveOffsetCurve(curve, offset)).flat(); + const raws: paper.Segment[][] = []; + for (let i = 0; i < offsetCurves.length; i += 2) { + raws.push(offsetCurves.slice(i, i + 2)); + } + const segments = connectBeziers(raws, join, source, offset, limit).flat(); 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))) { @@ -309,7 +312,7 @@ export function offsetPath(path: PathType, offset: number, join: StrokeJoinType, if (nonSIPath instanceof paper.Path) { result = offsetSimpleShape(nonSIPath, offset, join, limit); } else { - const children = Arrayex.Flat((nonSIPath.children as paper.Path[]).map((c) => { + const offsetParts = (nonSIPath.children as paper.Path[]).map((c) => { if (c.segments.length > 1) { if (!isSameDirection(c, path)) { c.reverse(); @@ -328,7 +331,8 @@ export function offsetPath(path: PathType, offset: number, join: StrokeJoinType, } else { return null; } - }), false); + }); + const children = offsetParts.flat().filter((c) => !!c) as paper.Item[]; result = new paper.CompoundPath({ children, insert: false }); } result.copyAttributes(nonSIPath, false); @@ -342,9 +346,9 @@ export function offsetStroke(path: PathType, offset: number, join: StrokeJoinTyp if (nonSIPath instanceof paper.Path) { result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit); } else { - const children = Arrayex.Flat((nonSIPath.children as paper.Path[]).map((c) => { + const children = (nonSIPath.children as paper.Path[]).flatMap((c) => { return offsetSimpleStroke(c, offset, join, cap, limit); - })); + }); result = children.reduce((c1, c2) => c1.unite(c2, { insert: false }) as PathType); } result.strokeWidth = 0; diff --git a/src/index.ts b/src/index.ts index 4ae3332..dcc5933 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,14 +23,14 @@ export class PaperOffset { 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); + const newPath = 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); + (path.parent || paper.project.activeLayer).addChild(newPath); } - return newStroke; + return newPath; } } diff --git a/tsconfig.json b/tsconfig.json index 8d969b4..7a9ef7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "moduleResolution": "node", "target": "es5", "module":"es2015", - "lib": ["es2015", "es2016", "es2017", "dom"], + "lib": ["es2019", "dom"], "strict": true, "sourceMap": true, "declaration": true,