forked from williamsokol/FoldOverBoxGenerator
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpaperjs-offset.js
More file actions
410 lines (405 loc) · 16.9 KB
/
paperjs-offset.js
File metadata and controls
410 lines (405 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
(function (paper) {
'use strict';
paper = paper && Object.prototype.hasOwnProperty.call(paper, 'default') ? paper['default'] : paper;
/**
* 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) {
if (path.closed) {
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 getNonSelfItersectionPath(path) {
if (path.closed) {
return path.unite(path, { insert: false });
}
return path;
}
function offsetPath(path, offset, join, limit) {
var nonSIPath = getNonSelfItersectionPath(path);
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 = getNonSelfItersectionPath(path);
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;
}
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);
};
}
ExtendPaperJs(paper);
window.PaperOffset = {
offset: PaperOffset.offset,
offsetStroke: PaperOffset.offsetStroke,
};
}(paper));