├── public
└── preview.jpg
├── .gitignore
├── demo
├── demo.css
├── debug.html
├── index.html
├── debug.js
├── demo.js
├── paperjs-offset.min.js
└── paperjs-offset.js
├── tslint.json
├── src
├── bundle.ts
├── index.ts
└── offset_core.ts
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glenzli/paperjs-offset/HEAD/public/preview.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | node_modules
3 | .nyc_output
4 | .DS_Store
5 | *.log
6 | .vscode
7 | .idea
8 | dist
9 | compiled
10 | .awcache
11 | .rpt2_cache
12 | docs
13 | issues
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | html {
2 | height: 100%;
3 | width: 100%;
4 | }
5 |
6 | body {
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 |
14 | canvas {
15 | box-shadow: 0 0 1rem #ccc;
16 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "quotemark": [true, "single"],
13 | "interface-name": false,
14 | "ordered-imports": false,
15 | "max-line-length": [true, 180]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/bundle.ts:
--------------------------------------------------------------------------------
1 | import paper from 'paper';
2 | import ExtendPaperJs, { PaperOffset } from './index';
3 |
4 | ExtendPaperJs(paper);
5 |
6 | declare global {
7 | interface Window {
8 | PaperOffset: {
9 | offset: typeof PaperOffset.offset;
10 | offsetStroke: typeof PaperOffset.offsetStroke;
11 | }
12 | }
13 | }
14 |
15 | window.PaperOffset = {
16 | offset: PaperOffset.offset,
17 | offsetStroke: PaperOffset.offsetStroke,
18 | };
19 |
--------------------------------------------------------------------------------
/demo/debug.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Demo
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Demo
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "es5",
5 | "module":"es2015",
6 | "lib": ["es2019", "dom"],
7 | "strict": true,
8 | "sourceMap": true,
9 | "declaration": true,
10 | "allowSyntheticDefaultImports": true,
11 | "experimentalDecorators": true,
12 | "emitDecoratorMetadata": true,
13 | "declarationDir": "dist/types",
14 | "outDir": "dist/lib",
15 | "typeRoots": [
16 | "node_modules/@types"
17 | ]
18 | },
19 | "include": ["src"]
20 | }
--------------------------------------------------------------------------------
/demo/debug.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | function DebugCase() {
3 | let canvas = document.querySelector('canvas');
4 | paper.setup(canvas);
5 | paper.view.center = [0, 0];
6 |
7 | const c3 = new paper.Path.Circle({ center: [180, 260], radius: 50, strokeColor: 'black' });
8 | const c4 = new paper.Path.Circle({ center: [230, 260], radius: 40, strokeColor: 'black' });
9 | const c5 = new paper.Path.Circle({ center: [205, 200], radius: 40, strokeColor: 'black' });
10 | const cc1 = c3.unite(c4, { insert: true });
11 | const cc = cc1.unite(c5, { insert: true });
12 | c3.remove();
13 | c4.remove();
14 | c5.remove();
15 | cc1.remove();
16 | cc.bringToFront();
17 | cc.translate(new paper.Point(-100, -100));
18 |
19 | PaperOffset.offset(cc, 24);
20 |
21 | const c = new paper.Path({pathData: "M4,11L5,13", strokeColor: 'rgba(156, 104, 193, 0.5)', strokeWidth: 4});
22 | PaperOffset.offsetStroke(c, 10, {cap: "round", join: "round"});
23 | }
24 |
25 | window.onload = DebugCase;
26 | })();
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016-2019 luz-alphacode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "paperjs-offset",
3 | "version": "1.0.8",
4 | "description": "An offset function for paperjs path.",
5 | "main": "dist/index.umd.js",
6 | "module": "dist/index.es5.js",
7 | "typings": "dist/types/index.d.ts",
8 | "scripts": {
9 | "lint": "tslint -p tsconfig.json",
10 | "prebuild": "rimraf dist",
11 | "build": "tsc --module commonjs && node build.js"
12 | },
13 | "dependencies": {
14 | "paper": "^0.12.4"
15 | },
16 | "devDependencies": {
17 | "lodash.camelcase": "^4.3.0",
18 | "rimraf": "^2.6.3",
19 | "rollup": "^1.10.1",
20 | "rollup-plugin-commonjs": "^9.3.4",
21 | "rollup-plugin-json": "^4.0.0",
22 | "rollup-plugin-node-resolve": "^4.2.3",
23 | "rollup-plugin-sourcemaps": "^0.4.2",
24 | "rollup-plugin-typescript2": "^0.20.1",
25 | "rollup-plugin-uglify": "^6.0.2",
26 | "tslint": "^5.16.0",
27 | "typescript": "^3.4.4"
28 | },
29 | "author": "glenli ",
30 | "license": "MIT",
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/luz-alphacode/paperjs-offset.git"
34 | },
35 | "bugs": {
36 | "url": "https://github.com/luz-alphacode/paperjs-offset/issues"
37 | },
38 | "homepage": "https://github.com/luz-alphacode/paperjs-offset#readme",
39 | "keywords": [
40 | "paperjs",
41 | "offset"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import paper from 'paper';
2 | import { StrokeJoinType, PathType, StrokeCapType, offsetPath, offsetStroke } from './offset_core';
3 |
4 | export interface OffsetOptions {
5 | join?: StrokeJoinType;
6 | cap?: StrokeCapType;
7 | limit?: number;
8 | insert?: boolean;
9 | }
10 |
11 | export class PaperOffset {
12 | public static offset(path: PathType, offset: number, options?: OffsetOptions): PathType {
13 | options = options || {};
14 | const newPath = offsetPath(path, offset, options.join || 'miter', options.limit || 10);
15 | if (options.insert === undefined) {
16 | options.insert = true;
17 | }
18 | if (options.insert) {
19 | (path.parent || paper.project.activeLayer).addChild(newPath);
20 | }
21 | return newPath;
22 | }
23 |
24 | public static offsetStroke(path: PathType, offset: number, options?: OffsetOptions): PathType {
25 | options = options || {};
26 | const newPath = offsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10);
27 | if (options.insert === undefined) {
28 | options.insert = true;
29 | }
30 | if (options.insert) {
31 | (path.parent || paper.project.activeLayer).addChild(newPath);
32 | }
33 | return newPath;
34 | }
35 | }
36 |
37 | /**
38 | * @deprecated EXTEND existing paper module is not recommend anymore
39 | */
40 | export default function ExtendPaperJs(paperNs: any) {
41 | paperNs.Path.prototype.offset = function(offset: number, options?: OffsetOptions) {
42 | return PaperOffset.offset(this, offset, options);
43 | };
44 |
45 | paperNs.Path.prototype.offsetStroke = function(offset: number, options?: OffsetOptions) {
46 | return PaperOffset.offsetStroke(this, offset, options);
47 | };
48 |
49 | paperNs.CompoundPath.prototype.offset = function(offset: number, options?: OffsetOptions) {
50 | return PaperOffset.offset(this, offset, options);
51 | };
52 |
53 | paperNs.CompoundPath.prototype.offsetStroke = function(offset: number, options?: OffsetOptions) {
54 | return PaperOffset.offsetStroke(this, offset, options);
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Paperjs Offset
2 | 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.
3 |
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.
4 |
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.
5 |
6 | ## Usage
7 | For node development, use
8 | ```sh
9 | npm install paperjs-offset
10 | ```
11 | And then, in you project:
12 | ```javascript
13 | import paper from 'paper'
14 | import { PaperOffset } from 'paperjs-offset'
15 |
16 | // call offset
17 | PaperOffset.offset(path, offset, options)
18 |
19 | // call offset stroke
20 | PaperOffset.offsetStroke(path, offset, options)
21 | ```
22 |
23 | You may still use the old way to extend paperjs module, which is **deprecated** and will be removed in future version.
24 | ```typescript
25 | import ExtendPaperJs from 'paperjs-offset'
26 | // extend paper.Path, paper.CompoundPath with offset, offsetStroke method
27 | ExtendPaperJs(paper);
28 |
29 | // Warning: The library no longer include extended definitions for paper.Path & paper.CompoundPath, you may need your own declarations to use extension in typescript.
30 | (path as any).offset(10);
31 | ```
32 |
33 | Or for web development, include the **paperjs-offset.js** or **paperjs-offset.min.js** in demo folder.
34 |
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.
35 | ```javascript
36 | let path = new paper.Path(/* params */)
37 |
38 | PaperOffset.offset(path, 10, { join: 'round' })
39 | PaperOffset.offsetStroke(path, 10, { cap: 'round' })
40 |
41 | // deprecated
42 | path.offset(10, { join: 'round' })
43 | // deprecated
44 | path.offsetStroke(10, { cap: 'round' })
45 | ```
46 |
47 | Sample references:
48 | ```typescript
49 | offset(path: paper.Path | paper.CompoundPath, offset: number, options?: OffsetOptions): paper.Path | paper.CompoundPath
50 |
51 | offsetStroke(path: paper.Path | paper.CompoundPath, offset: number, options?: OffsetOptions): paper.Path | paper.CompoundPath
52 |
53 | interface OffsetOptions {
54 | // the join style of offset path, default is 'miter'
55 | join?: 'miter' | 'bevel' | 'round';
56 | // the cap style of offset (only validate for offsetStroke), default is 'butt', ('square' will be supported in future)
57 | cap?: 'butt' | 'round';
58 | // the limit for miter style (refer to the miterLimit definition in paper)
59 | limit?: number;
60 | // whether the result should be insert into the canvas, default is true
61 | insert?: boolean;
62 | }
63 | ```
64 |
65 | ## Preview
66 | 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.
67 | 
68 |
69 | You can use open demo folder for simple cases demonstration.
70 |
71 | ## License
72 | Distributed under the MIT license. See [LICENSE](https://github.com/glenzli/paperjs-offset/blob/master/LICENSE) for detail.
--------------------------------------------------------------------------------
/demo/demo.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | function RunDemo() {
3 | let canvas = document.querySelector('canvas')
4 | paper.setup(canvas)
5 | paper.view.center = [0, 0]
6 |
7 | // simple polygon
8 | let r = new paper.Path.Rectangle({ point: [-500, -300], size: [80, 80], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' })
9 | PaperOffset.offset(r, 10)
10 | r.bringToFront()
11 | PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(r, -10), -10), -10);
12 |
13 | // simple polygon + bevel
14 | let r11 = new paper.Path.Rectangle({ point: [-500, -150], size: [60, 60], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' })
15 | let r12 = PaperOffset.offset(r11, -10, { insert: false })
16 | let r1 = r11.subtract(r12, { insert: true })
17 | r11.remove()
18 | PaperOffset.offset(r1, 15, { join: 'bevel' })
19 | r1.bringToFront()
20 |
21 | // simple polygon + round
22 | let r21 = new paper.Path.Rectangle({ point: [-350, -150], size: [60, 60], fillColor: 'rgb(191, 91, 91, 0.5)', strokeColor: 'black' })
23 | let r22 = PaperOffset.offset(r21, -10, { insert: false })
24 | let r2 = r21.subtract(r22, { insert: true })
25 | r21.remove()
26 | PaperOffset.offset(r2, 15, { join: 'round' })
27 | r2.bringToFront()
28 |
29 | // simple polygon
30 | let s = new paper.Path.Star({ center: [-300, -260], points: 12, radius1: 40, radius2: 30, fillColor: 'rgba(234, 154, 100, 0.5)', strokeColor: 'black' })
31 | PaperOffset.offset(s, 10)
32 | s.bringToFront()
33 | PaperOffset.offset(PaperOffset.offset(s, -10), -10);
34 |
35 | // smooth
36 | let s2 = new paper.Path.Star({ center: [-150, -260], points: 7, radius1: 40, radius2: 30, fillColor: 'rgba(239, 209, 88, 0.5)', strokeColor: 'black' })
37 | s2.smooth()
38 | PaperOffset.offset(s2, 10);
39 | s2.bringToFront()
40 | PaperOffset.offset(PaperOffset.offset(s2, -10), -10)
41 |
42 | // complex
43 | let c1 = new paper.Path.Circle({ center: [-20, -260], radius: 40, fillColor: 'rgba(165, 193, 93, 0.5)', strokeColor: 'black' })
44 | let c2 = new paper.Path.Circle({ center: [50, -260], radius: 40, fillColor: 'rgba(165, 193, 93, 0.5)', strokeColor: 'black' })
45 | let c = c1.unite(c2, { insert: true })
46 | c1.remove()
47 | c2.remove()
48 | PaperOffset.offset(c, 10);
49 | c.bringToFront()
50 | PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(c, -10), -10), -10)
51 |
52 | let c3 = new paper.Path.Circle({ center: [180, -260], radius: 40, fillColor: 'rgba(117, 170, 173, 0.5)', strokeColor: 'black' })
53 | let c4 = new paper.Path.Circle({ center: [230, -260], radius: 40, fillColor: 'rgba(117, 170, 173, 0.5)', strokeColor: 'black' })
54 | let c5 = new paper.Path.Circle({ center: [205, -200], radius: 40, fillColor: 'rgba(117, 170, 173, 0.5)', strokeColor: 'black' })
55 | let cc1 = c3.unite(c4, { insert: true })
56 | let cc = cc1.unite(c5, { insert: true })
57 | c3.remove()
58 | c4.remove()
59 | c5.remove()
60 | cc1.remove()
61 | PaperOffset.offset(cc, 10)
62 | cc.bringToFront()
63 | PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(cc, -10), -10), -10), -5)
64 |
65 | // complex+
66 | let c6 = new paper.Path.Circle({ center: [380, -260], radius: 40, fillColor: 'rgba(156, 104, 193, 0.5)', strokeColor: 'black' })
67 | let c7 = new paper.Path.Circle({ center: [430, -260], radius: 40, fillColor: 'rgba(156, 104, 193, 0.5)', strokeColor: 'black' })
68 | let c8 = new paper.Path.Circle({ center: [405, -200], radius: 40, fillColor: 'rgba(156, 104, 193, 0.5)', strokeColor: 'black' })
69 | let ccc1 = c6.unite(c7, { insert: true })
70 | let ccc = ccc1.unite(c8, { insert: true })
71 | c6.remove()
72 | c7.remove()
73 | c8.remove()
74 | ccc1.remove()
75 | ccc.smooth()
76 | ccc.offset(10)
77 | ccc.bringToFront()
78 | PaperOffset.offset(PaperOffset.offset(ccc, -10), -10)
79 | PaperOffset.offset(PaperOffset.offset(ccc, -30), -5)
80 |
81 | // stroke
82 | let rs = new paper.Path.Rectangle({ point: [-200, -150], size: [80, 80], fillColor: null, strokeColor: 'rgb(191, 91, 91, 0.5)' })
83 | PaperOffset.offsetStroke(rs, 10)
84 | rs.bringToFront()
85 |
86 | // stroke
87 | let st1 = new paper.Path.Line({ from: [-50, -100], to: [0, -100], strokeColor: 'rgba(156, 104, 193, 0.5)', strokeWidth: 3 })
88 | PaperOffset.offsetStroke(st1, 20, { cap: 'round' })
89 | st1.bringToFront()
90 |
91 | // stroke complex
92 | let cs = c.clone()
93 | cs.strokeColor = cs.fillColor
94 | cs.strokeWidth = 3
95 | cs.fillColor = null
96 | cs.position = [150, -50]
97 | cs.closed = false
98 | PaperOffset.offsetStroke(cs, 20)
99 | cs.bringToFront()
100 | let cs2 = cs.clone()
101 | cs2.position = [400, -50]
102 | cs2.strokeColor = 'rgba(117, 170, 173, 0.5)'
103 | PaperOffset.offsetStroke(cs2, 25, { cap: 'round' })
104 | cs2.bringToFront()
105 |
106 | // edge cases
107 | 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)' })
108 | ec1.translate(-450, -250)
109 | ec1.scale(0.4)
110 | PaperOffset.offset(ec1, 10)
111 | PaperOffset.offset(PaperOffset.offset(PaperOffset.offset(ec1, -10), -10), -10)
112 |
113 | 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 })
114 | ec2.scale(0.4)
115 | ec2.translate(-350, 20)
116 | PaperOffset.offsetStroke(ec2, 10)
117 |
118 | 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)' })
119 | ec3.scale(0.4)
120 | ec3.translate(-100, -150)
121 | PaperOffset.offset(ec3, -10)
122 | }
123 |
124 | window.onload = RunDemo
125 | })()
--------------------------------------------------------------------------------
/demo/paperjs-offset.min.js:
--------------------------------------------------------------------------------
1 | !function(S){"use strict";function p(e,t,n,r){var o=e.curve===t,i=t.getNormalAtTime(o?0:1).multiply(r),a=e.point.add(i),s=new S.Segment(a),c=o?"handleOut":"handleIn";return s[c]=e[c].add(n.subtract(i).divide(2)),s}function T(e,t){return e.x*t.y-e.y*t.x}function m(e,t,n,r,o,i){var a,s,c,u,f,l,d,h,p,m,g,v,w,P,b=new S.Curve(e[0],e[1]),k=new S.Curve(t[0],t[1]),C=b.getIntersections(k),y=e[1].point.getDistance(t[0].point);if(n.isSmooth())t[0].handleOut=t[0].handleOut.project(n.handleOut),t[0].handleIn=e[1].handleIn.project(n.handleIn),t[0].point=e[1].point.add(t[0].point).divide(2),e.pop();else if(0===C.length)if(y>.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 MAX_RECURSION_TIME) {
40 | console.log('reach maximum recursion times')
41 | return [segment1, segment2];
42 | }
43 | // divide && re-offset
44 | const offsetCurve = new paper.Curve(segment1, segment2);
45 | // if the offset curve is not self intersected, divide it
46 | if (offsetCurve.getIntersections(offsetCurve).length === 0) {
47 | const threshold = Math.min(Math.abs(offset) / 10, 1);
48 | const midOffset = offsetCurve.getPointAtTime(0.5).getDistance(curve.getPointAtTime(0.5));
49 | if (Math.abs(midOffset - Math.abs(offset)) > threshold) {
50 | const subCurve = curve.divideAtTime(0.5);
51 | if (subCurve != null) {
52 | return [...adaptiveOffsetCurve(curve, offset, recursionTime + 1), ...adaptiveOffsetCurve(subCurve, offset, recursionTime + 1)];
53 | }
54 | }
55 | }
56 | return [segment1, segment2];
57 | }
58 |
59 | /**
60 | * Create a round join segment between two adjacent segments.
61 | */
62 | function makeRoundJoin(segment1: paper.Segment, segment2: paper.Segment, originPoint: paper.Point, radius: number) {
63 | const through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint))
64 | .normalize(Math.abs(radius)).add(originPoint);
65 | const arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through, insert: false });
66 | segment1.handleOut = arc.firstSegment.handleOut;
67 | segment2.handleIn = arc.lastSegment.handleIn;
68 | return arc.segments.length === 3 ? arc.segments[1] : null;
69 | }
70 |
71 | function det(p1: paper.Point, p2: paper.Point) {
72 | return p1.x * p2.y - p1.y * p2.x;
73 | }
74 |
75 | /**
76 | * Get the intersection point of point based lines
77 | */
78 | function getPointLineIntersections(p1: paper.Point, p2: paper.Point, p3: paper.Point, p4: paper.Point) {
79 | const l1 = p1.subtract(p2);
80 | const l2 = p3.subtract(p4);
81 | const dl1 = det(p1, p2);
82 | const dl2 = det(p3, p4);
83 | return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(det(l1, l2));
84 | }
85 |
86 | /**
87 | * Connect two adjacent bezier curve, each curve is represented by two segments,
88 | * create different types of joins or simply removal redundant segment.
89 | */
90 | function connectAdjacentBezier(segments1: paper.Segment[], segments2: paper.Segment[], origin: paper.Segment, joinType: StrokeJoinType, offset: number, limit: number) {
91 | const curve1 = new paper.Curve(segments1[0], segments1[1]);
92 | const curve2 = new paper.Curve(segments2[0], segments2[1]);
93 | const intersection = curve1.getIntersections(curve2);
94 | const distance = segments1[1].point.getDistance(segments2[0].point);
95 | if (origin.isSmooth()) {
96 | segments2[0].handleOut = segments2[0].handleOut!.project(origin.handleOut!);
97 | segments2[0].handleIn = segments1[1].handleIn!.project(origin.handleIn!);
98 | segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2);
99 | segments1.pop();
100 | } else {
101 | if (intersection.length === 0) {
102 | if (distance > Math.abs(offset) * 0.1) {
103 | // connect
104 | switch (joinType) {
105 | case 'miter':
106 | const join = getPointLineIntersections(curve1.point2, curve1.point2.add(curve1.getTangentAtTime(1)),
107 | curve2.point1, curve2.point1.add(curve2.getTangentAtTime(0)));
108 | // prevent sharp angle
109 | const joinOffset = Math.max(join.getDistance(curve1.point2), join.getDistance(curve2.point1));
110 | if (joinOffset < Math.abs(offset) * limit) {
111 | segments1.push(new paper.Segment(join));
112 | }
113 | break;
114 | case 'round':
115 | const mid = makeRoundJoin(segments1[1], segments2[0], origin.point, offset);
116 | if (mid) {
117 | segments1.push(mid);
118 | }
119 | break;
120 | default: break;
121 | }
122 | } else {
123 | segments2[0].handleIn = segments1[1].handleIn;
124 | segments1.pop();
125 | }
126 | } else {
127 | const second1 = curve1.divideAt(intersection[0]);
128 | if (second1) {
129 | const join = second1.segment1;
130 | const second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]);
131 | join.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut;
132 | segments1.pop();
133 | segments2[0] = join;
134 | } else {
135 | segments2[0].handleIn = segments1[1].handleIn;
136 | segments1.pop();
137 | }
138 | }
139 | }
140 | }
141 |
142 | /**
143 | * Connect all the segments together.
144 | */
145 | function connectBeziers(rawSegments: paper.Segment[][], join: StrokeJoinType, source: paper.Path, offset: number, limit: number) {
146 | const originSegments = source.segments;
147 | const first = rawSegments[0].slice();
148 | for (let i = 0; i < rawSegments.length - 1; ++i) {
149 | connectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit);
150 | }
151 | if (source.closed) {
152 | connectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit);
153 | rawSegments[0][0] = first[0];
154 | }
155 | return rawSegments;
156 | }
157 |
158 | function reduceSingleChildCompoundPath(path: PathType) {
159 | if (path.children.length === 1) {
160 | path = path.children[0] as paper.Path;
161 | path.remove(); // remove from parent, this is critical, or the style attributes will be ignored
162 | }
163 | return path;
164 | }
165 |
166 | /** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */
167 | function normalize(path: PathType, areaThreshold = 0.01) {
168 | if (path.closed) {
169 | const ignoreArea = Math.abs(path.area * areaThreshold);
170 | if (!path.clockwise) {
171 | path.reverse();
172 | }
173 | path = path.unite(path, { insert: false }) as PathType;
174 | if (path instanceof paper.CompoundPath) {
175 | path.children.filter((c) => Math.abs((c as PathType).area) < ignoreArea).forEach((c) => c.remove());
176 | if (path.children.length === 1) {
177 | return reduceSingleChildCompoundPath(path);
178 | }
179 | }
180 | }
181 | return path;
182 | }
183 |
184 | function isSameDirection(partialPath: paper.Path, fullPath: PathType) {
185 | const offset1 = partialPath.segments[0].location.offset;
186 | const offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset;
187 | const sampleOffset = (offset1 + offset2) / 3;
188 | const originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset;
189 | const originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset;
190 | return originOffset1 < originOffset2;
191 | }
192 |
193 | /** Remove self intersection when offset is negative by point direction dectection. */
194 | function removeIntersection(path: PathType) {
195 | if (path.closed) {
196 | const newPath = path.unite(path, { insert: false }) as PathType;
197 | if (newPath instanceof paper.CompoundPath) {
198 | (newPath.children as paper.Path[]).filter((c) => {
199 | if (c.segments.length > 1) {
200 | return !isSameDirection(c, path);
201 | } else {
202 | return true;
203 | }
204 | }).forEach((c) => c.remove());
205 | return reduceSingleChildCompoundPath(newPath);
206 | }
207 | }
208 | return path;
209 | }
210 |
211 | function getSegments(path: PathType) {
212 | if (path instanceof paper.CompoundPath) {
213 | return path.children.map((c) => (c as paper.Path).segments).flat();
214 | } else {
215 | return (path as paper.Path).segments;
216 | }
217 | }
218 |
219 | /**
220 | * Remove impossible segments in negative offset condition.
221 | */
222 | function removeOutsiders(newPath: PathType, path: PathType) {
223 | const segments = getSegments(newPath).slice();
224 | segments.forEach((segment) => {
225 | if (!path.contains(segment.point)) {
226 | segment.remove();
227 | }
228 | });
229 | }
230 |
231 | function preparePath(path: paper.Path, offset: number): [paper.Path, number] {
232 | const source = path.clone({ insert: false }) as paper.Path;
233 | source.reduce({});
234 | if (!path.clockwise) {
235 | source.reverse();
236 | offset = -offset;
237 | }
238 | return [source, offset];
239 | }
240 |
241 | function offsetSimpleShape(path: paper.Path, offset: number, join: StrokeJoinType, limit: number): PathType {
242 | let source: paper.Path;
243 | [source, offset] = preparePath(path, offset);
244 | const curves = source.curves.slice();
245 | const offsetCurves = curves.map((curve) => adaptiveOffsetCurve(curve, offset)).flat();
246 | const raws: paper.Segment[][] = [];
247 | for (let i = 0; i < offsetCurves.length; i += 2) {
248 | raws.push(offsetCurves.slice(i, i + 2));
249 | }
250 | const segments = connectBeziers(raws, join, source, offset, limit).flat();
251 | const newPath = removeIntersection(new paper.Path({ segments, insert: false, closed: path.closed }));
252 | newPath.reduce({});
253 | if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) {
254 | removeOutsiders(newPath, path);
255 | }
256 | // recovery path
257 | if (source.clockwise !== path.clockwise) {
258 | newPath.reverse();
259 | }
260 | return normalize(newPath);
261 | }
262 |
263 | function makeRoundCap(from: paper.Segment, to: paper.Segment, offset: number) {
264 | const origin = from.point.add(to.point).divide(2);
265 | const normal = to.point.subtract(from.point).rotate(-90, new paper.Point(0, 0)).normalize(offset);
266 | const through = origin.add(normal);
267 | const arc = new paper.Path.Arc({ from: from.point, to: to.point, through, insert: false });
268 | return arc.segments;
269 | }
270 |
271 | function connectSide(outer: PathType, inner: paper.Path, offset: number, cap: StrokeCapType): paper.Path {
272 | if (outer instanceof paper.CompoundPath) {
273 | let cs = outer.children.map((c) => ({ c, a: Math.abs((c as paper.Path).area) }));
274 | cs = cs.sort((c1, c2) => c2.a - c1.a);
275 | outer = cs[0].c as paper.Path;
276 | }
277 | const oSegments = (outer as paper.Path).segments.slice();
278 | const iSegments = inner.segments.slice();
279 | switch (cap) {
280 | case 'round':
281 | const heads = makeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset);
282 | const tails = makeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset);
283 | const result = new paper.Path({ segments: [...heads, ...oSegments, ...tails, ...iSegments], closed: true, insert: false });
284 | result.reduce({});
285 | return result;
286 | default: return new paper.Path({ segments: [...oSegments, ...iSegments], closed: true, insert: false });
287 | }
288 | }
289 |
290 | function offsetSimpleStroke(path: paper.Path, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number): PathType {
291 | offset = path.clockwise ? offset : -offset;
292 | const positiveOffset = offsetSimpleShape(path, offset, join, limit);
293 | const negativeOffset = offsetSimpleShape(path, -offset, join, limit);
294 | if (path.closed) {
295 | return positiveOffset.subtract(negativeOffset, { insert: false }) as PathType;
296 | } else {
297 | let inner = negativeOffset;
298 | let holes = new Array();
299 | if (negativeOffset instanceof paper.CompoundPath) {
300 | holes = negativeOffset.children.filter((c) => (c as paper.Path).closed) as paper.Path[];
301 | holes.forEach((h) => h.remove());
302 | inner = negativeOffset.children[0] as paper.Path;
303 | }
304 | inner.reverse();
305 | let final = connectSide(positiveOffset, inner as paper.Path, offset, cap) as PathType;
306 | if (holes.length > 0) {
307 | for (const hole of holes) {
308 | final = final.subtract(hole, { insert: false }) as PathType;
309 | }
310 | }
311 | return final;
312 | }
313 | }
314 |
315 | function getNonSelfItersectionPath(path: PathType) {
316 | if (path.closed) {
317 | return path.unite(path, { insert: false }) as PathType;
318 | }
319 | return path;
320 | }
321 |
322 | export function offsetPath(path: PathType, offset: number, join: StrokeJoinType, limit: number): PathType {
323 | const nonSIPath = getNonSelfItersectionPath(path);
324 | let result = nonSIPath;
325 | if (nonSIPath instanceof paper.Path) {
326 | result = offsetSimpleShape(nonSIPath, offset, join, limit);
327 | } else {
328 | const offsetParts = (nonSIPath.children as paper.Path[]).map((c) => {
329 | if (c.segments.length > 1) {
330 | if (!isSameDirection(c, path)) {
331 | c.reverse();
332 | }
333 | let offseted = offsetSimpleShape(c, offset, join, limit);
334 | offseted = normalize(offseted);
335 | if (offseted.clockwise !== c.clockwise) {
336 | offseted.reverse();
337 | }
338 | if (offseted instanceof paper.CompoundPath) {
339 | offseted.applyMatrix = true;
340 | return offseted.children;
341 | } else {
342 | return offseted;
343 | }
344 | } else {
345 | return null;
346 | }
347 | });
348 | const children = offsetParts.flat().filter((c) => !!c) as paper.Item[];
349 | result = new paper.CompoundPath({ children, insert: false });
350 | }
351 | result.copyAttributes(nonSIPath, false);
352 | result.remove();
353 | return result;
354 | }
355 |
356 | export function offsetStroke(path: PathType, offset: number, join: StrokeJoinType, cap: StrokeCapType, limit: number): PathType {
357 | const nonSIPath = getNonSelfItersectionPath(path);
358 | let result = nonSIPath;
359 | if (nonSIPath instanceof paper.Path) {
360 | result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit);
361 | } else {
362 | const children = (nonSIPath.children as paper.Path[]).flatMap((c) => {
363 | return offsetSimpleStroke(c, offset, join, cap, limit);
364 | });
365 | result = children.reduce((c1, c2) => c1.unite(c2, { insert: false }) as PathType);
366 | }
367 | result.strokeWidth = 0;
368 | result.fillColor = nonSIPath.strokeColor;
369 | result.shadowBlur = nonSIPath.shadowBlur;
370 | result.shadowColor = nonSIPath.shadowColor;
371 | result.shadowOffset = nonSIPath.shadowOffset;
372 | return result;
373 | }
374 |
--------------------------------------------------------------------------------
/demo/paperjs-offset.js:
--------------------------------------------------------------------------------
1 | (function (paper) {
2 | 'use strict';
3 |
4 | paper = paper && Object.prototype.hasOwnProperty.call(paper, 'default') ? paper['default'] : paper;
5 |
6 | /**
7 | * Offset the start/terminal segment of a bezier curve
8 | * @param segment segment to offset
9 | * @param curve curve to offset
10 | * @param handleNormal the normal of the the line formed of two handles
11 | * @param offset offset value
12 | */
13 | function offsetSegment(segment, curve, handleNormal, offset) {
14 | var isFirst = segment.curve === curve;
15 | // get offset vector
16 | var offsetVector = (curve.getNormalAtTime(isFirst ? 0 : 1)).multiply(offset);
17 | // get offset point
18 | var point = segment.point.add(offsetVector);
19 | var newSegment = new paper.Segment(point);
20 | // handleOut for start segment & handleIn for terminal segment
21 | var handle = (isFirst ? 'handleOut' : 'handleIn');
22 | newSegment[handle] = segment[handle].add(handleNormal.subtract(offsetVector).divide(2));
23 | return newSegment;
24 | }
25 | /**
26 | * Adaptive offset a curve by repeatly apply the approximation proposed by Tiller and Hanson.
27 | * @param curve curve to offset
28 | * @param offset offset value
29 | */
30 | function adaptiveOffsetCurve(curve, offset) {
31 | 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);
32 | var segment1 = offsetSegment(curve.segment1, curve, hNormal, offset);
33 | var segment2 = offsetSegment(curve.segment2, curve, hNormal, offset);
34 | // divide && re-offset
35 | var offsetCurve = new paper.Curve(segment1, segment2);
36 | // if the offset curve is not self intersected, divide it
37 | if (offsetCurve.getIntersections(offsetCurve).length === 0) {
38 | var threshold = Math.min(Math.abs(offset) / 10, 1);
39 | var midOffset = offsetCurve.getPointAtTime(0.5).getDistance(curve.getPointAtTime(0.5));
40 | if (Math.abs(midOffset - Math.abs(offset)) > threshold) {
41 | var subCurve = curve.divideAtTime(0.5);
42 | if (subCurve != null) {
43 | return adaptiveOffsetCurve(curve, offset).concat(adaptiveOffsetCurve(subCurve, offset));
44 | }
45 | }
46 | }
47 | return [segment1, segment2];
48 | }
49 | /**
50 | * Create a round join segment between two adjacent segments.
51 | */
52 | function makeRoundJoin(segment1, segment2, originPoint, radius) {
53 | var through = segment1.point.subtract(originPoint).add(segment2.point.subtract(originPoint))
54 | .normalize(Math.abs(radius)).add(originPoint);
55 | var arc = new paper.Path.Arc({ from: segment1.point, to: segment2.point, through: through, insert: false });
56 | segment1.handleOut = arc.firstSegment.handleOut;
57 | segment2.handleIn = arc.lastSegment.handleIn;
58 | return arc.segments.length === 3 ? arc.segments[1] : null;
59 | }
60 | function det(p1, p2) {
61 | return p1.x * p2.y - p1.y * p2.x;
62 | }
63 | /**
64 | * Get the intersection point of point based lines
65 | */
66 | function getPointLineIntersections(p1, p2, p3, p4) {
67 | var l1 = p1.subtract(p2);
68 | var l2 = p3.subtract(p4);
69 | var dl1 = det(p1, p2);
70 | var dl2 = det(p3, p4);
71 | return new paper.Point(dl1 * l2.x - l1.x * dl2, dl1 * l2.y - l1.y * dl2).divide(det(l1, l2));
72 | }
73 | /**
74 | * Connect two adjacent bezier curve, each curve is represented by two segments,
75 | * create different types of joins or simply removal redundant segment.
76 | */
77 | function connectAdjacentBezier(segments1, segments2, origin, joinType, offset, limit) {
78 | var curve1 = new paper.Curve(segments1[0], segments1[1]);
79 | var curve2 = new paper.Curve(segments2[0], segments2[1]);
80 | var intersection = curve1.getIntersections(curve2);
81 | var distance = segments1[1].point.getDistance(segments2[0].point);
82 | if (origin.isSmooth()) {
83 | segments2[0].handleOut = segments2[0].handleOut.project(origin.handleOut);
84 | segments2[0].handleIn = segments1[1].handleIn.project(origin.handleIn);
85 | segments2[0].point = segments1[1].point.add(segments2[0].point).divide(2);
86 | segments1.pop();
87 | }
88 | else {
89 | if (intersection.length === 0) {
90 | if (distance > Math.abs(offset) * 0.1) {
91 | // connect
92 | switch (joinType) {
93 | case 'miter':
94 | var join = getPointLineIntersections(curve1.point2, curve1.point2.add(curve1.getTangentAtTime(1)), curve2.point1, curve2.point1.add(curve2.getTangentAtTime(0)));
95 | // prevent sharp angle
96 | var joinOffset = Math.max(join.getDistance(curve1.point2), join.getDistance(curve2.point1));
97 | if (joinOffset < Math.abs(offset) * limit) {
98 | segments1.push(new paper.Segment(join));
99 | }
100 | break;
101 | case 'round':
102 | var mid = makeRoundJoin(segments1[1], segments2[0], origin.point, offset);
103 | if (mid) {
104 | segments1.push(mid);
105 | }
106 | break;
107 | }
108 | }
109 | else {
110 | segments2[0].handleIn = segments1[1].handleIn;
111 | segments1.pop();
112 | }
113 | }
114 | else {
115 | var second1 = curve1.divideAt(intersection[0]);
116 | if (second1) {
117 | var join = second1.segment1;
118 | var second2 = curve2.divideAt(curve2.getIntersections(curve1)[0]);
119 | join.handleOut = second2 ? second2.segment1.handleOut : segments2[0].handleOut;
120 | segments1.pop();
121 | segments2[0] = join;
122 | }
123 | else {
124 | segments2[0].handleIn = segments1[1].handleIn;
125 | segments1.pop();
126 | }
127 | }
128 | }
129 | }
130 | /**
131 | * Connect all the segments together.
132 | */
133 | function connectBeziers(rawSegments, join, source, offset, limit) {
134 | var originSegments = source.segments;
135 | var first = rawSegments[0].slice();
136 | for (var i = 0; i < rawSegments.length - 1; ++i) {
137 | connectAdjacentBezier(rawSegments[i], rawSegments[i + 1], originSegments[i + 1], join, offset, limit);
138 | }
139 | if (source.closed) {
140 | connectAdjacentBezier(rawSegments[rawSegments.length - 1], first, originSegments[0], join, offset, limit);
141 | rawSegments[0][0] = first[0];
142 | }
143 | return rawSegments;
144 | }
145 | function reduceSingleChildCompoundPath(path) {
146 | if (path.children.length === 1) {
147 | path = path.children[0];
148 | path.remove(); // remove from parent, this is critical, or the style attributes will be ignored
149 | }
150 | return path;
151 | }
152 | /** Normalize a path, always clockwise, non-self-intersection, ignore really small components, and no one-component compound path. */
153 | function normalize(path, areaThreshold) {
154 | if (areaThreshold === void 0) { areaThreshold = 0.01; }
155 | if (path.closed) {
156 | var ignoreArea_1 = Math.abs(path.area * areaThreshold);
157 | if (!path.clockwise) {
158 | path.reverse();
159 | }
160 | path = path.unite(path, { insert: false });
161 | if (path instanceof paper.CompoundPath) {
162 | path.children.filter(function (c) { return Math.abs(c.area) < ignoreArea_1; }).forEach(function (c) { return c.remove(); });
163 | if (path.children.length === 1) {
164 | return reduceSingleChildCompoundPath(path);
165 | }
166 | }
167 | }
168 | return path;
169 | }
170 | function isSameDirection(partialPath, fullPath) {
171 | var offset1 = partialPath.segments[0].location.offset;
172 | var offset2 = partialPath.segments[Math.max(1, Math.floor(partialPath.segments.length / 2))].location.offset;
173 | var sampleOffset = (offset1 + offset2) / 3;
174 | var originOffset1 = fullPath.getNearestLocation(partialPath.getPointAt(sampleOffset)).offset;
175 | var originOffset2 = fullPath.getNearestLocation(partialPath.getPointAt(2 * sampleOffset)).offset;
176 | return originOffset1 < originOffset2;
177 | }
178 | /** Remove self intersection when offset is negative by point direction dectection. */
179 | function removeIntersection(path) {
180 | if (path.closed) {
181 | var newPath = path.unite(path, { insert: false });
182 | if (newPath instanceof paper.CompoundPath) {
183 | newPath.children.filter(function (c) {
184 | if (c.segments.length > 1) {
185 | return !isSameDirection(c, path);
186 | }
187 | else {
188 | return true;
189 | }
190 | }).forEach(function (c) { return c.remove(); });
191 | return reduceSingleChildCompoundPath(newPath);
192 | }
193 | }
194 | return path;
195 | }
196 | function getSegments(path) {
197 | if (path instanceof paper.CompoundPath) {
198 | return path.children.map(function (c) { return c.segments; }).flat();
199 | }
200 | else {
201 | return path.segments;
202 | }
203 | }
204 | /**
205 | * Remove impossible segments in negative offset condition.
206 | */
207 | function removeOutsiders(newPath, path) {
208 | var segments = getSegments(newPath).slice();
209 | segments.forEach(function (segment) {
210 | if (!path.contains(segment.point)) {
211 | segment.remove();
212 | }
213 | });
214 | }
215 | function preparePath(path, offset) {
216 | var source = path.clone({ insert: false });
217 | source.reduce({});
218 | if (!path.clockwise) {
219 | source.reverse();
220 | offset = -offset;
221 | }
222 | return [source, offset];
223 | }
224 | function offsetSimpleShape(path, offset, join, limit) {
225 | var _a;
226 | var source;
227 | _a = preparePath(path, offset), source = _a[0], offset = _a[1];
228 | var curves = source.curves.slice();
229 | var offsetCurves = curves.map(function (curve) { return adaptiveOffsetCurve(curve, offset); }).flat();
230 | var raws = [];
231 | for (var i = 0; i < offsetCurves.length; i += 2) {
232 | raws.push(offsetCurves.slice(i, i + 2));
233 | }
234 | var segments = connectBeziers(raws, join, source, offset, limit).flat();
235 | var newPath = removeIntersection(new paper.Path({ segments: segments, insert: false, closed: path.closed }));
236 | newPath.reduce({});
237 | if (source.closed && ((source.clockwise && offset < 0) || (!source.clockwise && offset > 0))) {
238 | removeOutsiders(newPath, path);
239 | }
240 | // recovery path
241 | if (source.clockwise !== path.clockwise) {
242 | newPath.reverse();
243 | }
244 | return normalize(newPath);
245 | }
246 | function makeRoundCap(from, to, offset) {
247 | var origin = from.point.add(to.point).divide(2);
248 | var normal = to.point.subtract(from.point).rotate(-90, new paper.Point(0, 0)).normalize(offset);
249 | var through = origin.add(normal);
250 | var arc = new paper.Path.Arc({ from: from.point, to: to.point, through: through, insert: false });
251 | return arc.segments;
252 | }
253 | function connectSide(outer, inner, offset, cap) {
254 | if (outer instanceof paper.CompoundPath) {
255 | var cs = outer.children.map(function (c) { return ({ c: c, a: Math.abs(c.area) }); });
256 | cs = cs.sort(function (c1, c2) { return c2.a - c1.a; });
257 | outer = cs[0].c;
258 | }
259 | var oSegments = outer.segments.slice();
260 | var iSegments = inner.segments.slice();
261 | switch (cap) {
262 | case 'round':
263 | var heads = makeRoundCap(iSegments[iSegments.length - 1], oSegments[0], offset);
264 | var tails = makeRoundCap(oSegments[oSegments.length - 1], iSegments[0], offset);
265 | var result = new paper.Path({ segments: heads.concat(oSegments, tails, iSegments), closed: true, insert: false });
266 | result.reduce({});
267 | return result;
268 | default: return new paper.Path({ segments: oSegments.concat(iSegments), closed: true, insert: false });
269 | }
270 | }
271 | function offsetSimpleStroke(path, offset, join, cap, limit) {
272 | offset = path.clockwise ? offset : -offset;
273 | var positiveOffset = offsetSimpleShape(path, offset, join, limit);
274 | var negativeOffset = offsetSimpleShape(path, -offset, join, limit);
275 | if (path.closed) {
276 | return positiveOffset.subtract(negativeOffset, { insert: false });
277 | }
278 | else {
279 | var inner = negativeOffset;
280 | var holes = new Array();
281 | if (negativeOffset instanceof paper.CompoundPath) {
282 | holes = negativeOffset.children.filter(function (c) { return c.closed; });
283 | holes.forEach(function (h) { return h.remove(); });
284 | inner = negativeOffset.children[0];
285 | }
286 | inner.reverse();
287 | var final = connectSide(positiveOffset, inner, offset, cap);
288 | if (holes.length > 0) {
289 | for (var _i = 0, holes_1 = holes; _i < holes_1.length; _i++) {
290 | var hole = holes_1[_i];
291 | final = final.subtract(hole, { insert: false });
292 | }
293 | }
294 | return final;
295 | }
296 | }
297 | function getNonSelfItersectionPath(path) {
298 | if (path.closed) {
299 | return path.unite(path, { insert: false });
300 | }
301 | return path;
302 | }
303 | function offsetPath(path, offset, join, limit) {
304 | var nonSIPath = getNonSelfItersectionPath(path);
305 | var result = nonSIPath;
306 | if (nonSIPath instanceof paper.Path) {
307 | result = offsetSimpleShape(nonSIPath, offset, join, limit);
308 | }
309 | else {
310 | var offsetParts = nonSIPath.children.map(function (c) {
311 | if (c.segments.length > 1) {
312 | if (!isSameDirection(c, path)) {
313 | c.reverse();
314 | }
315 | var offseted = offsetSimpleShape(c, offset, join, limit);
316 | offseted = normalize(offseted);
317 | if (offseted.clockwise !== c.clockwise) {
318 | offseted.reverse();
319 | }
320 | if (offseted instanceof paper.CompoundPath) {
321 | offseted.applyMatrix = true;
322 | return offseted.children;
323 | }
324 | else {
325 | return offseted;
326 | }
327 | }
328 | else {
329 | return null;
330 | }
331 | });
332 | var children = offsetParts.flat().filter(function (c) { return !!c; });
333 | result = new paper.CompoundPath({ children: children, insert: false });
334 | }
335 | result.copyAttributes(nonSIPath, false);
336 | result.remove();
337 | return result;
338 | }
339 | function offsetStroke(path, offset, join, cap, limit) {
340 | var nonSIPath = getNonSelfItersectionPath(path);
341 | var result = nonSIPath;
342 | if (nonSIPath instanceof paper.Path) {
343 | result = offsetSimpleStroke(nonSIPath, offset, join, cap, limit);
344 | }
345 | else {
346 | var children = nonSIPath.children.flatMap(function (c) {
347 | return offsetSimpleStroke(c, offset, join, cap, limit);
348 | });
349 | result = children.reduce(function (c1, c2) { return c1.unite(c2, { insert: false }); });
350 | }
351 | result.strokeWidth = 0;
352 | result.fillColor = nonSIPath.strokeColor;
353 | result.shadowBlur = nonSIPath.shadowBlur;
354 | result.shadowColor = nonSIPath.shadowColor;
355 | result.shadowOffset = nonSIPath.shadowOffset;
356 | return result;
357 | }
358 |
359 | var PaperOffset = /** @class */ (function () {
360 | function PaperOffset() {
361 | }
362 | PaperOffset.offset = function (path, offset, options) {
363 | options = options || {};
364 | var newPath = offsetPath(path, offset, options.join || 'miter', options.limit || 10);
365 | if (options.insert === undefined) {
366 | options.insert = true;
367 | }
368 | if (options.insert) {
369 | (path.parent || paper.project.activeLayer).addChild(newPath);
370 | }
371 | return newPath;
372 | };
373 | PaperOffset.offsetStroke = function (path, offset, options) {
374 | options = options || {};
375 | var newPath = offsetStroke(path, offset, options.join || 'miter', options.cap || 'butt', options.limit || 10);
376 | if (options.insert === undefined) {
377 | options.insert = true;
378 | }
379 | if (options.insert) {
380 | (path.parent || paper.project.activeLayer).addChild(newPath);
381 | }
382 | return newPath;
383 | };
384 | return PaperOffset;
385 | }());
386 | /**
387 | * @deprecated EXTEND existing paper module is not recommend anymore
388 | */
389 | function ExtendPaperJs(paperNs) {
390 | paperNs.Path.prototype.offset = function (offset, options) {
391 | return PaperOffset.offset(this, offset, options);
392 | };
393 | paperNs.Path.prototype.offsetStroke = function (offset, options) {
394 | return PaperOffset.offsetStroke(this, offset, options);
395 | };
396 | paperNs.CompoundPath.prototype.offset = function (offset, options) {
397 | return PaperOffset.offset(this, offset, options);
398 | };
399 | paperNs.CompoundPath.prototype.offsetStroke = function (offset, options) {
400 | return PaperOffset.offsetStroke(this, offset, options);
401 | };
402 | }
403 |
404 | ExtendPaperJs(paper);
405 | window.PaperOffset = {
406 | offset: PaperOffset.offset,
407 | offsetStroke: PaperOffset.offsetStroke,
408 | };
409 |
410 | }(paper));
411 |
--------------------------------------------------------------------------------