paperjs-offset 1.0.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,72 +1,699 @@
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
- <br/>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
- <br/>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.
1
+ # paperjs-offset
2
+
3
+ [![paperjs-offset preview](https://raw.githubusercontent.com/glenzli/paperjs-offset/master/public/preview.jpg)](https://glenzli.github.io/paperjs-offset/)
4
+
5
+ Language: [English](#english) | [中文](#中文)
6
+
7
+ <a id="中文"></a>
8
+
9
+ ## 中文
10
+
11
+ ### 简介
12
+
13
+ `paperjs-offset` 是 [Paper.js](https://paperjs.org/) 的路径偏移辅助库,用于生成闭合路径偏移、描边轮廓,以及分析偏移结果质量。
14
+
15
+ [打开在线 Demo](https://glenzli.github.io/paperjs-offset/) · [反馈问题](https://github.com/glenzli/paperjs-offset/issues)
16
+
17
+ 本地运行预览:
18
+
19
+ ```sh
20
+ npm run demo:serve
21
+ ```
22
+
23
+ 本地服务会输出并监听:
24
+
25
+ ```txt
26
+ http://localhost:4173/
27
+ ```
28
+
29
+ 新代码推荐直接使用命名函数:
30
+
31
+ ```ts
32
+ import paper from 'paper';
33
+ import { analyze, offset, offsetStroke } from 'paperjs-offset';
34
+
35
+ paper.setup(new paper.Size(400, 300));
36
+ ```
37
+
38
+ ### 推荐 API
39
+
40
+ ```ts
41
+ function offset(
42
+ path: paper.Path | paper.CompoundPath,
43
+ distance: number,
44
+ options?: OffsetOptions
45
+ ): paper.Path | paper.CompoundPath;
46
+
47
+ function offsetStroke(
48
+ path: paper.Path | paper.CompoundPath,
49
+ distance: number,
50
+ options?: OffsetOptions
51
+ ): paper.Path | paper.CompoundPath;
52
+
53
+ function analyze(
54
+ source: paper.Path | paper.CompoundPath,
55
+ result: paper.Path | paper.CompoundPath,
56
+ distance: number,
57
+ options?: { stroke?: boolean }
58
+ ): OffsetQuality;
59
+ ```
60
+
61
+ - `offset(path, distance, options)` 用于扩张或收缩路径。
62
+ - `offsetStroke(path, distance, options)` 将描边中心线转换成可填充的闭合轮廓。`distance` 是轮廓到中心线的距离,因此效果类似描边宽度的一半。
63
+ - `analyze(source, result, distance, options)` 对生成结果做质量评分,并为困难几何场景输出警告。
64
+
65
+ 这三个函数都接受 `paper.Path` 和 `paper.CompoundPath`。默认情况下,结果会插入当前 Paper.js project;如果希望自行管理返回对象,请传入 `insert: false`。
66
+
67
+ ### 安装
68
+
69
+ ```sh
70
+ npm install paper paperjs-offset
71
+ ```
72
+
73
+ 使用这个附加库的应用需要自行安装 `paper`。`paperjs-offset` 不声明运行时依赖;包内提供 CommonJS、ESM、TypeScript 类型声明和浏览器 bundle。
74
+
75
+ ### 快速开始
76
+
77
+ ```ts
78
+ import paper from 'paper';
79
+ import { offset, offsetStroke } from 'paperjs-offset';
80
+
81
+ paper.setup(new paper.Size(400, 300));
82
+
83
+ const source = new paper.Path.Star({
84
+ center: [160, 120],
85
+ points: 8,
86
+ radius1: 70,
87
+ radius2: 36,
88
+ insert: false
89
+ });
90
+
91
+ const expanded = offset(source, 14, {
92
+ join: 'round',
93
+ insert: false
94
+ });
95
+
96
+ const outline = offsetStroke(source, 8, {
97
+ join: 'round',
98
+ cap: 'round',
99
+ insert: false
100
+ });
101
+ ```
102
+
103
+ ### API 使用模式
104
+
105
+ #### 1. 偏移闭合图形
106
+
107
+ 正数距离会扩张闭合图形,负数距离会向内收缩。
108
+
109
+ ```ts
110
+ const expanded = offset(source, 18, {
111
+ join: 'round',
112
+ insert: false
113
+ });
114
+
115
+ const contracted = offset(source, -18, {
116
+ join: 'round',
117
+ insert: false
118
+ });
119
+ ```
120
+
121
+ #### 2. 生成可填充的描边轮廓
122
+
123
+ 当你需要一个可填充的轮廓,而不是仅由渲染层绘制的 stroke 时,可以对开放或闭合路径使用 `offsetStroke`。
124
+
125
+ ```ts
126
+ const openPath = new paper.Path({
127
+ segments: [[20, 80], [90, 30], [160, 80]],
128
+ insert: false
129
+ });
130
+
131
+ const outline = offsetStroke(openPath, 10, {
132
+ join: 'round',
133
+ cap: 'round',
134
+ insert: false
135
+ });
136
+ ```
137
+
138
+ 对于开放路径,`cap` 控制端点样式;对于闭合路径,结果仍然是闭合的可填充轮廓。
139
+
140
+ #### 3. 检查结果质量
141
+
142
+ Bezier 几何的偏移是近似问题。当图形包含紧凑曲线、凹角、自交,或使用较激进的负向偏移时,可以使用 `analyze` 检查结果质量。
143
+
144
+ ```ts
145
+ const result = offset(source, -12, {
146
+ join: 'round',
147
+ algorithm: 'auto',
148
+ insert: false
149
+ });
150
+
151
+ const quality = analyze(source, result, -12);
152
+
153
+ console.log(quality.score, quality.warnings);
154
+ ```
155
+
156
+ 检查 `offsetStroke` 结果时,请传入 `stroke: true`:
157
+
158
+ ```ts
159
+ const outline = offsetStroke(openPath, 10, {
160
+ join: 'round',
161
+ cap: 'round',
162
+ insert: false
163
+ });
164
+
165
+ const quality = analyze(openPath, outline, 10, { stroke: true });
166
+ ```
167
+
168
+ ### 选项
169
+
170
+ ```ts
171
+ interface OffsetOptions {
172
+ join?: 'miter' | 'bevel' | 'round';
173
+ cap?: 'butt' | 'round';
174
+ limit?: number;
175
+ insert?: boolean;
176
+ algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
177
+ }
178
+ ```
179
+
180
+ | 选项 | 默认值 | 适用于 | 说明 |
181
+ | --- | --- | --- | --- |
182
+ | `join` | `'miter'` | `offset`, `offsetStroke` | 角点重建样式。 |
183
+ | `cap` | `'butt'` | `offsetStroke` | 开放路径端点样式。 |
184
+ | `limit` | `10` | `offset`, `offsetStroke` | 尖角的 miter 限制。 |
185
+ | `insert` | `true` | `offset`, `offsetStroke` | 将结果插入源对象父级或当前 active layer。 |
186
+ | `algorithm` | `'auto'` | `offset`, `offsetStroke` | 策略选择模式。 |
187
+
188
+ ### 算法模式
189
+
190
+ `algorithm: 'auto'` 是推荐默认值。它会尝试可用策略,使用与 `analyze` 相同的质量函数对候选结果评分,并返回评分最低的结果。
191
+
192
+ `adaptive` 有意保持保守。当严格结果看起来像 miter 尖刺,或负向偏移过度侵蚀时,它可能选择更安全的连接方式或更小的向内 fallback 距离。如果必须严格使用请求的距离,请使用 `robust`、`split` 或 `legacy`。
193
+
194
+ | 模式 | 使用场景 |
195
+ | --- | --- |
196
+ | `auto` | 推荐的应用默认值,会选择评分最佳的结果。 |
197
+ | `adaptive` | 先尝试严格 robust 结果,再为 miter 尖刺和激进内缩提供更安全的 fallback。 |
198
+ | `robust` | 对凹角和自交描边轮廓做额外清理。 |
199
+ | `split` | 拆分自交曲线,清理程度低于 `robust`。 |
200
+ | `legacy` | 尽量接近旧版本行为,适合需要保持历史输出的场景。 |
201
+
202
+ `OffsetQuality` 包含:
203
+
204
+ ```ts
205
+ interface OffsetQuality {
206
+ algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
207
+ score: number;
208
+ warnings: string[];
209
+ selfIntersections: number;
210
+ containmentErrors: number;
211
+ distanceErrors: number;
212
+ segmentCount: number;
213
+ area: number;
214
+ }
215
+ ```
216
+
217
+ 警告可能包括空结果、非有限 bounds、自交、非预期面积变化、包含关系错误、中心偏移、负向偏移坍缩和距离坍缩。
218
+
219
+ ### 浏览器用法
220
+
221
+ 在浏览器页面中,先加载 Paper.js,再加载浏览器版本的 `paperjs-offset`:
222
+
223
+ ```html
224
+ <script src="https://cdn.jsdelivr.net/npm/paper@0.12.18/dist/paper-full.min.js"></script>
225
+ <script src="https://cdn.jsdelivr.net/npm/paperjs-offset@1.0.8/dist/index.umd.min.js"></script>
226
+ ```
227
+
228
+ 使用 `paperjsOffset` 全局对象访问当前 API:
229
+
230
+ ```js
231
+ const expanded = paperjsOffset.offset(path, 12, {
232
+ join: 'round'
233
+ });
234
+
235
+ const outline = paperjsOffset.offsetStroke(path, 8, {
236
+ join: 'round',
237
+ cap: 'round'
238
+ });
239
+ ```
240
+
241
+ ### Demo
242
+
243
+ [打开在线 Demo](https://glenzli.github.io/paperjs-offset/) 可以查看偏移方向、连接样式、描边轮廓、复合路径、策略选择和质量评分。Gallery 已拆成 basic、advanced、boundary 和 edge-case 页面,以减少单页需要渲染的确定性 case 数量。
244
+
245
+ 本地运行 demo:
246
+
247
+ ```sh
248
+ npm run demo:serve
249
+ ```
250
+
251
+ 根目录 `index.html` 会重定向到 `demo/index.html`。`npm run build` 会将浏览器 bundle 写入 `dist/`,并同步生成文件到 `demo/`,因此本地 demo 和发布产物使用的是同一套构建结果。
252
+
253
+ 线上 demo 由 GitHub Pages 部署。仓库需要在 GitHub 的 Settings -> Pages 中把 Source 设为 GitHub Actions;之后 `.github/workflows/pages.yml` 会在 `master` 分支推送或手动触发时运行:
254
+
255
+ ```sh
256
+ npm ci
257
+ npm run pages:build
258
+ ```
259
+
260
+ `pages:build` 会重新构建包,把 `index.html`、`demo/` 和 `public/` 复制到 `.pages/`,并把锁定版本的 Paper.js 复制到 `.pages/demo/vendor/`。GitHub Pages workflow 只部署 demo 站点,不负责 npm 发布。
261
+
262
+ ### 兼容 API
263
+
264
+ 旧入口仍然可用,但新代码应优先使用上面的命名函数。
265
+
266
+ #### 静态类
267
+
268
+ ```ts
269
+ import { PaperOffset } from 'paperjs-offset';
270
+
271
+ const expanded = PaperOffset.offset(path, 12, { join: 'round' });
272
+ const outline = PaperOffset.offsetStroke(path, 8, { cap: 'round' });
273
+ const quality = PaperOffset.analyze(path, expanded, 12);
274
+ ```
275
+
276
+ 浏览器 bundle 也会暴露 `window.PaperOffset`,作为旧脚本的兼容别名。
277
+
278
+ #### 原型扩展
279
+
280
+ ```ts
281
+ import paper from 'paper';
282
+ import extendPaperJs from 'paperjs-offset';
283
+
284
+ extendPaperJs(paper);
285
+
286
+ const expanded = path.offset(12, { join: 'round' });
287
+ const outline = path.offsetStroke(8, { cap: 'round' });
288
+ ```
289
+
290
+ 这会修改 `paper.Path.prototype` 和 `paper.CompoundPath.prototype`。它仍然保留用于兼容,但命名函数更容易类型标注、测试和迁移。
291
+
292
+ ### 限制和问题反馈
293
+
294
+ 路径偏移本质上是几何近似问题,尤其是在 Bezier 连接、自交、复合路径和大幅负向偏移附近。复杂图形仍然可能暴露边界情况。
295
+
296
+ 反馈问题时,最好包含:
297
+
298
+ - `pathData`
299
+ - 距离
300
+ - 选项
301
+ - 使用的是 `offset` 还是 `offsetStroke`
302
+ - 预期图形或截图
303
+ - 如果方便,附上 `analyze(...)` 输出
304
+
305
+ 请在 <https://github.com/glenzli/paperjs-offset/issues> 反馈问题。
306
+
307
+ ### 开发
308
+
309
+ ```sh
310
+ npm ci
311
+ npm run lint
312
+ npm test
313
+ npm run test:stress
314
+ npm run test:stress:quick
315
+ npm run test:stress:boundary
316
+ ```
317
+
318
+ 常用构建命令:
319
+
320
+ ```sh
321
+ npm run build
322
+ npm run build:ts
323
+ npm run build:bundles
324
+ ```
325
+
326
+ `npm test` 会重新构建包、运行 smoke tests,然后运行完整生成式 stress corpus。普通 gallery 分组可使用 `test:stress:quick`,更激进的 boundary 分组可使用 `test:stress:boundary`。
327
+
328
+ ### 发布
329
+
330
+ 发布流程刻意保持在本地触发;当前没有 GitHub Action 自动发布到 npm。准备发大版本时,先提交普通代码改动,然后运行:
331
+
332
+ ```sh
333
+ npm run release:major
334
+ ```
335
+
336
+ 也可以用 `npm run release:minor` 或 `npm run release:patch`。`release:prepare` 要求 git 工作区干净,会更新 `package.json` 和 `package-lock.json`、运行 `release:check`、提交 `chore: release vX.Y.Z`,并创建 `vX.Y.Z` tag。
337
+
338
+ 确认版本提交和 tag 无误后,再执行真正的发布:
339
+
340
+ ```sh
341
+ npm run release:ship
342
+ ```
343
+
344
+ `release:ship` 要求当前提交带有匹配当前版本号的 tag,会先检查 `npm whoami` 和 `gh auth status`,再重新运行 `release:check`,然后 `git push origin HEAD --follow-tags`、`npm publish`,最后通过 GitHub CLI 执行 `gh release create --generate-notes`。如果 npm 发布需要 2FA,可以运行 `npm run release:ship -- --otp=123456`。
345
+
346
+ `release:check` 会运行 typecheck、测试、`npm audit` 和 `npm pack --dry-run --ignore-scripts`。`npm test` 已经先完成构建,因此 pack 校验不依赖 npm lifecycle。直接执行 `npm publish` 时,`prepublishOnly` 也会运行同一套 release check。
347
+
348
+ ### 许可证
349
+
350
+ MIT。详见 [LICENSE](LICENSE)。
351
+
352
+ [回到语言切换](#paperjs-offset)
353
+
354
+ <a id="english"></a>
355
+
356
+ ## English
357
+
358
+ ### Overview
359
+
360
+ `paperjs-offset` provides offset geometry helpers for [Paper.js](https://paperjs.org/). It generates closed path offsets, filled stroke outlines, and quality reports for generated offset geometry.
361
+
362
+ [Open the live demo](https://glenzli.github.io/paperjs-offset/) · [Report an issue](https://github.com/glenzli/paperjs-offset/issues)
363
+
364
+ Run the live preview locally:
5
365
 
6
- ## Usage
7
- For node development, use
8
366
  ```sh
9
- npm install paperjs-offset
367
+ npm run demo:serve
10
368
  ```
11
- And then, in you project:
12
- ```javascript
13
- import paper from 'paper'
14
- import { PaperOffset } from 'paperjs-offset'
15
369
 
16
- // call offset
17
- PaperOffset.offset(path, offset, options)
370
+ The local server prints and serves:
371
+
372
+ ```txt
373
+ http://localhost:4173/
374
+ ```
375
+
376
+ Use the named functions for new code:
377
+
378
+ ```ts
379
+ import paper from 'paper';
380
+ import { analyze, offset, offsetStroke } from 'paperjs-offset';
18
381
 
19
- // call offset stroke
20
- PaperOffset.offsetStroke(path, offset, options)
382
+ paper.setup(new paper.Size(400, 300));
21
383
  ```
22
384
 
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);
385
+ ### Recommended API
28
386
 
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);
387
+ ```ts
388
+ function offset(
389
+ path: paper.Path | paper.CompoundPath,
390
+ distance: number,
391
+ options?: OffsetOptions
392
+ ): paper.Path | paper.CompoundPath;
393
+
394
+ function offsetStroke(
395
+ path: paper.Path | paper.CompoundPath,
396
+ distance: number,
397
+ options?: OffsetOptions
398
+ ): paper.Path | paper.CompoundPath;
399
+
400
+ function analyze(
401
+ source: paper.Path | paper.CompoundPath,
402
+ result: paper.Path | paper.CompoundPath,
403
+ distance: number,
404
+ options?: { stroke?: boolean }
405
+ ): OffsetQuality;
31
406
  ```
32
407
 
33
- Or for web development, include the **paperjs-offset.js** or **paperjs-offset.min.js** in demo folder.
34
- <br/>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 */)
408
+ - `offset(path, distance, options)` expands or contracts a path.
409
+ - `offsetStroke(path, distance, options)` converts a stroked centerline into a closed filled outline. The distance is the outline distance from the centerline, so it behaves like half of a stroke width.
410
+ - `analyze(source, result, distance, options)` scores generated geometry and reports warnings for hard cases.
37
411
 
38
- PaperOffset.offset(path, 10, { join: 'round' })
39
- PaperOffset.offsetStroke(path, 10, { cap: 'round' })
412
+ All three functions accept `paper.Path` and `paper.CompoundPath`. Results are inserted into the active Paper.js project by default; pass `insert: false` when you want to manage the returned item yourself.
40
413
 
41
- // deprecated
42
- path.offset(10, { join: 'round' })
43
- // deprecated
44
- path.offsetStroke(10, { cap: 'round' })
414
+ ### Install
415
+
416
+ ```sh
417
+ npm install paper paperjs-offset
45
418
  ```
46
419
 
47
- Sample references:
48
- ```typescript
49
- offset(path: paper.Path | paper.CompoundPath, offset: number, options?: OffsetOptions): paper.Path | paper.CompoundPath
420
+ Install `paper` in applications that use this addon. `paperjs-offset` does not declare runtime dependencies; the package ships CommonJS, ESM, TypeScript declarations, and browser bundles.
421
+
422
+ ### Quick Start
423
+
424
+ ```ts
425
+ import paper from 'paper';
426
+ import { offset, offsetStroke } from 'paperjs-offset';
427
+
428
+ paper.setup(new paper.Size(400, 300));
429
+
430
+ const source = new paper.Path.Star({
431
+ center: [160, 120],
432
+ points: 8,
433
+ radius1: 70,
434
+ radius2: 36,
435
+ insert: false
436
+ });
50
437
 
51
- offsetStroke(path: paper.Path | paper.CompoundPath, offset: number, options?: OffsetOptions): paper.Path | paper.CompoundPath
438
+ const expanded = offset(source, 14, {
439
+ join: 'round',
440
+ insert: false
441
+ });
52
442
 
443
+ const outline = offsetStroke(source, 8, {
444
+ join: 'round',
445
+ cap: 'round',
446
+ insert: false
447
+ });
448
+ ```
449
+
450
+ ### API Patterns
451
+
452
+ #### 1. Offset a Closed Shape
453
+
454
+ Positive distances expand closed shapes. Negative distances contract them.
455
+
456
+ ```ts
457
+ const expanded = offset(source, 18, {
458
+ join: 'round',
459
+ insert: false
460
+ });
461
+
462
+ const contracted = offset(source, -18, {
463
+ join: 'round',
464
+ insert: false
465
+ });
466
+ ```
467
+
468
+ #### 2. Build a Filled Stroke Outline
469
+
470
+ Use `offsetStroke` for open or closed paths when you need a fillable outline instead of a rendered stroke.
471
+
472
+ ```ts
473
+ const openPath = new paper.Path({
474
+ segments: [[20, 80], [90, 30], [160, 80]],
475
+ insert: false
476
+ });
477
+
478
+ const outline = offsetStroke(openPath, 10, {
479
+ join: 'round',
480
+ cap: 'round',
481
+ insert: false
482
+ });
483
+ ```
484
+
485
+ For open paths, `cap` controls the terminals. For closed paths, the result is still a closed filled outline.
486
+
487
+ #### 3. Inspect Quality
488
+
489
+ Offsetting Bezier geometry is approximate. Use `analyze` when a shape may contain tight curves, concave joins, self-intersections, or aggressive negative offsets.
490
+
491
+ ```ts
492
+ const result = offset(source, -12, {
493
+ join: 'round',
494
+ algorithm: 'auto',
495
+ insert: false
496
+ });
497
+
498
+ const quality = analyze(source, result, -12);
499
+
500
+ console.log(quality.score, quality.warnings);
501
+ ```
502
+
503
+ When inspecting `offsetStroke` output, pass `stroke: true`:
504
+
505
+ ```ts
506
+ const outline = offsetStroke(openPath, 10, {
507
+ join: 'round',
508
+ cap: 'round',
509
+ insert: false
510
+ });
511
+
512
+ const quality = analyze(openPath, outline, 10, { stroke: true });
513
+ ```
514
+
515
+ ### Options
516
+
517
+ ```ts
53
518
  interface OffsetOptions {
54
- // the join style of offset path, default is 'miter'
55
519
  join?: 'miter' | 'bevel' | 'round';
56
- // the cap style of offset (only validate for offsetStroke), default is 'butt', ('square' will be supported in future)
57
520
  cap?: 'butt' | 'round';
58
- // the limit for miter style (refer to the miterLimit definition in paper)
59
521
  limit?: number;
60
- // whether the result should be insert into the canvas, default is true
61
522
  insert?: boolean;
523
+ algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
524
+ }
525
+ ```
526
+
527
+ | Option | Default | Applies to | Notes |
528
+ | --- | --- | --- | --- |
529
+ | `join` | `'miter'` | `offset`, `offsetStroke` | Corner reconstruction style. |
530
+ | `cap` | `'butt'` | `offsetStroke` | Terminal style for open paths. |
531
+ | `limit` | `10` | `offset`, `offsetStroke` | Miter limit for sharp joins. |
532
+ | `insert` | `true` | `offset`, `offsetStroke` | Insert the result into the source parent or active layer. |
533
+ | `algorithm` | `'auto'` | `offset`, `offsetStroke` | Strategy selection mode. |
534
+
535
+ ### Algorithms
536
+
537
+ `algorithm: 'auto'` is the recommended default. It tries the available strategies, scores each candidate with the same quality function exposed by `analyze`, and returns the lowest-scoring result.
538
+
539
+ `adaptive` is intentionally conservative. When a strict result looks like a miter spike or an over-eroded inward offset, it may choose a safer join or a smaller inward fallback distance. Use `robust`, `split`, or `legacy` when the requested distance must be treated strictly.
540
+
541
+ | Mode | Use case |
542
+ | --- | --- |
543
+ | `auto` | Recommended application default. Chooses the best-scored result. |
544
+ | `adaptive` | Tries strict robust output first, then safer fallbacks for miter spikes and aggressive inward collapse. |
545
+ | `robust` | Extra cleanup for concave joins and self-intersecting stroke outlines. |
546
+ | `split` | Splits self-intersecting curves with less cleanup than `robust`. |
547
+ | `legacy` | Closest behavior to older releases when preserving historical output matters. |
548
+
549
+ `OffsetQuality` includes:
550
+
551
+ ```ts
552
+ interface OffsetQuality {
553
+ algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
554
+ score: number;
555
+ warnings: string[];
556
+ selfIntersections: number;
557
+ containmentErrors: number;
558
+ distanceErrors: number;
559
+ segmentCount: number;
560
+ area: number;
62
561
  }
63
562
  ```
64
563
 
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
- ![Preview](/public/preview.jpg)
564
+ Warnings can include empty results, non-finite bounds, self-intersections, unexpected area changes, containment errors, center shift, negative offset collapse, and distance collapse.
565
+
566
+ ### Browser Usage
567
+
568
+ For browser pages, load Paper.js first and then the browser build:
569
+
570
+ ```html
571
+ <script src="https://cdn.jsdelivr.net/npm/paper@0.12.18/dist/paper-full.min.js"></script>
572
+ <script src="https://cdn.jsdelivr.net/npm/paperjs-offset@1.0.8/dist/index.umd.min.js"></script>
573
+ ```
574
+
575
+ Use the `paperjsOffset` global for the current API:
576
+
577
+ ```js
578
+ const expanded = paperjsOffset.offset(path, 12, {
579
+ join: 'round'
580
+ });
581
+
582
+ const outline = paperjsOffset.offsetStroke(path, 8, {
583
+ join: 'round',
584
+ cap: 'round'
585
+ });
586
+ ```
587
+
588
+ ### Demo
589
+
590
+ [Open the live demo](https://glenzli.github.io/paperjs-offset/) to inspect offset direction, join styles, stroke outlines, compound paths, strategy selection, and quality scoring. The gallery is split into basic, advanced, boundary, and edge-case pages so each page renders a smaller deterministic case set.
591
+
592
+ Run the demos locally:
593
+
594
+ ```sh
595
+ npm run demo:serve
596
+ ```
597
+
598
+ The root `index.html` redirects to `demo/index.html`. `npm run build` writes browser bundles to `dist/` and syncs the generated files into `demo/`, so local demos and published artifacts exercise the same build.
599
+
600
+ The live demo is deployed with GitHub Pages. Set the repository Pages source to GitHub Actions in GitHub Settings -> Pages; after that, `.github/workflows/pages.yml` runs on pushes to `master` and on manual dispatch:
601
+
602
+ ```sh
603
+ npm ci
604
+ npm run pages:build
605
+ ```
606
+
607
+ `pages:build` rebuilds the package, copies `index.html`, `demo/`, and `public/` into `.pages/`, and copies the locked Paper.js build into `.pages/demo/vendor/`. The Pages workflow only deploys the demo site; it does not publish to npm.
608
+
609
+ ### Compatibility API
610
+
611
+ Older entry points still exist, but new code should prefer the named functions above.
612
+
613
+ #### Static Class
614
+
615
+ ```ts
616
+ import { PaperOffset } from 'paperjs-offset';
617
+
618
+ const expanded = PaperOffset.offset(path, 12, { join: 'round' });
619
+ const outline = PaperOffset.offsetStroke(path, 8, { cap: 'round' });
620
+ const quality = PaperOffset.analyze(path, expanded, 12);
621
+ ```
622
+
623
+ The browser bundle also exposes `window.PaperOffset` as an alias for older scripts.
624
+
625
+ #### Prototype Extension
626
+
627
+ ```ts
628
+ import paper from 'paper';
629
+ import extendPaperJs from 'paperjs-offset';
630
+
631
+ extendPaperJs(paper);
632
+
633
+ const expanded = path.offset(12, { join: 'round' });
634
+ const outline = path.offsetStroke(8, { cap: 'round' });
635
+ ```
636
+
637
+ This mutates `paper.Path.prototype` and `paper.CompoundPath.prototype`. It remains available for compatibility, but named functions are easier to type, test, and migrate.
638
+
639
+ ### Limitations and Bug Reports
640
+
641
+ Path offsetting is a geometric approximation problem, especially around Bezier joins, self-intersections, compound paths, and large negative offsets. Difficult shapes can still expose edge cases.
642
+
643
+ Bug reports are most useful when they include:
644
+
645
+ - `pathData`
646
+ - distance
647
+ - options
648
+ - whether the operation is `offset` or `offsetStroke`
649
+ - expected shape or screenshot
650
+ - `analyze(...)` output when available
651
+
652
+ Report issues at <https://github.com/glenzli/paperjs-offset/issues>.
653
+
654
+ ### Development
655
+
656
+ ```sh
657
+ npm ci
658
+ npm run lint
659
+ npm test
660
+ npm run test:stress
661
+ npm run test:stress:quick
662
+ npm run test:stress:boundary
663
+ ```
664
+
665
+ Useful build commands:
666
+
667
+ ```sh
668
+ npm run build
669
+ npm run build:ts
670
+ npm run build:bundles
671
+ ```
672
+
673
+ `npm test` rebuilds the package, runs smoke tests, and then runs the full generated stress corpus. Use `test:stress:quick` for the normal gallery groups, and `test:stress:boundary` for the aggressive boundary groups.
674
+
675
+ ### Release
676
+
677
+ Publishing is intentionally triggered locally. There is no GitHub Action that publishes to npm. For a major release, commit normal code changes first, then run:
678
+
679
+ ```sh
680
+ npm run release:major
681
+ ```
682
+
683
+ Use `npm run release:minor` or `npm run release:patch` for smaller releases. `release:prepare` requires a clean git worktree, updates `package.json` and `package-lock.json`, runs `release:check`, commits `chore: release vX.Y.Z`, and creates the `vX.Y.Z` tag.
684
+
685
+ After verifying the version commit and tag, ship the release:
686
+
687
+ ```sh
688
+ npm run release:ship
689
+ ```
690
+
691
+ `release:ship` requires the current commit to have the tag matching the current package version. It checks `npm whoami` and `gh auth status`, reruns `release:check`, then runs `git push origin HEAD --follow-tags`, `npm publish`, and `gh release create --generate-notes` through the GitHub CLI. If npm publish requires 2FA, run `npm run release:ship -- --otp=123456`.
692
+
693
+ `release:check` runs typecheck, tests, `npm audit`, and `npm pack --dry-run --ignore-scripts`. `npm test` builds first, so the pack check does not depend on npm lifecycle scripts. Direct `npm publish` also runs the same release check through `prepublishOnly`.
694
+
695
+ ### License
68
696
 
69
- You can use open demo folder for simple cases demonstration.
697
+ MIT. See [LICENSE](LICENSE).
70
698
 
71
- ## License
72
- Distributed under the MIT license. See [LICENSE](https://github.com/glenzli/paperjs-offset/blob/master/LICENSE) for detail.
699
+ [Back to language switch](#paperjs-offset)