paperjs-offset
v2.1.2
Published
An offset function for paperjs path.
Readme
paperjs-offset

Temporary notice / 临时提示
EN: The GitHub-hosted project page may be temporarily unavailable because the maintainer's GitHub account has unexpectedly been flagged. The maintainer is appealing the account status, but the recovery timeline is currently unknown. During this period, use the GitLab mirror to view the project. The npm package remains available; this is an account-status issue, not a project shutdown.
中文: 由于维护者 GitHub 账号当前被异常标记(flagged),GitHub 托管的项目页面可能暂时无法访问。维护者正在申诉账号状态,但恢复时间尚不可知。此期间可以通过 GitLab 镜像 查看项目。npm 包仍可使用;这是账号状态问题,不是项目停止维护或下架。
中文
简介
paperjs-offset 是 Paper.js 的路径偏移辅助库,用于生成闭合路径偏移、描边轮廓,以及分析偏移结果质量。
项目主页:https://glenzli.com/projects/paperjs-offset/
新代码推荐直接使用命名函数:
import paper from 'paper';
import { analyze, offset, offsetStroke } from 'paperjs-offset';
paper.setup(new paper.Size(400, 300));推荐 API
function offset(
path: paper.Path | paper.CompoundPath,
distance: number,
options?: OffsetOptions
): paper.Path | paper.CompoundPath;
function offsetStroke(
path: paper.Path | paper.CompoundPath,
distance: number,
options?: OffsetOptions
): paper.Path | paper.CompoundPath;
function analyze(
source: paper.Path | paper.CompoundPath,
result: paper.Path | paper.CompoundPath,
distance: number,
options?: { stroke?: boolean }
): OffsetQuality;offset(path, distance, options)用于扩张或收缩路径。offsetStroke(path, distance, options)将描边中心线转换成可填充的闭合轮廓。distance是轮廓到中心线的距离,因此效果类似描边宽度的一半。analyze(source, result, distance, options)对生成结果做质量评分,并为困难几何场景输出警告。
这三个函数都接受 paper.Path 和 paper.CompoundPath。默认情况下,结果会插入当前 Paper.js project;如果希望自行管理返回对象,请传入 insert: false。
安装
npm install paper paperjs-offset使用这个附加库的应用需要自行安装 paper。paperjs-offset 不声明运行时依赖;包内提供 CommonJS、ESM、TypeScript 类型声明和浏览器 bundle。
快速开始
import paper from 'paper';
import { offset, offsetStroke } from 'paperjs-offset';
paper.setup(new paper.Size(400, 300));
const source = new paper.Path.Star({
center: [160, 120],
points: 8,
radius1: 70,
radius2: 36,
insert: false
});
const expanded = offset(source, 14, {
join: 'round',
insert: false
});
const outline = offsetStroke(source, 8, {
join: 'round',
cap: 'round',
insert: false
});API 使用模式
1. 偏移闭合图形
正数距离会扩张闭合图形,负数距离会向内收缩。
const expanded = offset(source, 18, {
join: 'round',
insert: false
});
const contracted = offset(source, -18, {
join: 'round',
insert: false
});2. 生成可填充的描边轮廓
当你需要一个可填充的轮廓,而不是仅由渲染层绘制的 stroke 时,可以对开放或闭合路径使用 offsetStroke。
const openPath = new paper.Path({
segments: [[20, 80], [90, 30], [160, 80]],
insert: false
});
const outline = offsetStroke(openPath, 10, {
join: 'round',
cap: 'round',
insert: false
});对于开放路径,cap 控制端点样式;对于闭合路径,结果仍然是闭合的可填充轮廓。
3. 检查结果质量
Bezier 几何的偏移是近似问题。当图形包含紧凑曲线、凹角、自交,或使用较激进的负向偏移时,可以使用 analyze 检查结果质量。
const result = offset(source, -12, {
join: 'round',
algorithm: 'auto',
insert: false
});
const quality = analyze(source, result, -12);
console.log(quality.score, quality.warnings);检查 offsetStroke 结果时,请传入 stroke: true:
const outline = offsetStroke(openPath, 10, {
join: 'round',
cap: 'round',
insert: false
});
const quality = analyze(openPath, outline, 10, { stroke: true });选项
interface OffsetOptions {
join?: 'miter' | 'bevel' | 'round';
cap?: 'butt' | 'round';
limit?: number;
insert?: boolean;
algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
}| 选项 | 默认值 | 适用于 | 说明 |
| --- | --- | --- | --- |
| join | 'miter' | offset, offsetStroke | 角点重建样式。 |
| cap | 'butt' | offsetStroke | 开放路径端点样式。 |
| limit | 10 | offset, offsetStroke | 尖角的 miter 限制。 |
| insert | true | offset, offsetStroke | 将结果插入源对象父级或当前 active layer。 |
| algorithm | 'auto' | offset, offsetStroke | 策略选择模式。 |
算法模式
algorithm: 'auto' 是推荐默认值。它会尝试可用策略,使用与 analyze 相同的质量函数对候选结果评分,并返回评分最低的结果。
adaptive 有意保持保守。当严格结果看起来像 miter 尖刺,或负向偏移过度侵蚀时,它可能选择更安全的连接方式或更小的向内 fallback 距离。如果必须严格使用请求的距离,请使用 robust、split 或 legacy。
| 模式 | 使用场景 |
| --- | --- |
| auto | 推荐的应用默认值,会选择评分最佳的结果。 |
| adaptive | 先尝试严格 robust 结果,再为 miter 尖刺和激进内缩提供更安全的 fallback。 |
| robust | 对凹角和自交描边轮廓做额外清理。 |
| split | 拆分自交曲线,清理程度低于 robust。 |
| legacy | 尽量接近旧版本行为,适合需要保持历史输出的场景。 |
OffsetQuality 包含:
interface OffsetQuality {
algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
score: number;
warnings: string[];
selfIntersections: number;
containmentErrors: number;
distanceErrors: number;
segmentCount: number;
area: number;
}警告可能包括空结果、非有限 bounds、自交、非预期面积变化、包含关系错误、中心偏移、负向偏移坍缩和距离坍缩。
浏览器用法
在浏览器页面中,先加载 Paper.js,再加载浏览器版本的 paperjs-offset:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/paper-full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/paperjs-offset@2/dist/index.umd.min.js"></script>这里使用 @2 跟随 v2 的最新兼容版本。如果需要完全可复现的构建,可以 pin 到具体版本,例如 [email protected]。
使用 paperjsOffset 全局对象访问当前 API:
const expanded = paperjsOffset.offset(path, 12, {
join: 'round'
});
const outline = paperjsOffset.offsetStroke(path, 8, {
join: 'round',
cap: 'round'
});Dev Site Export Contract
本仓库不再维护独立 demo 页面。项目侧只负责导出内容、数据、截图资源和可挂载 runtime,页面布局和路由由外部 dev-site 统一组织。
生成标准输出:
npm run build:dev-site输出结构:
dist/dev-site/
├── manifest.json
├── app/
│ ├── paper-full.min-<hash>.js
│ ├── paperjs-offset.umd.min-<hash>.js
│ └── paperjs-offset.esm-<hash>.js
├── assets/
│ ├── banner.png
│ ├── guide.png
│ ├── gallery.png
│ └── playground.png
├── data/
│ ├── guide.json
│ ├── gallery.json
│ └── playground.json
└── runtime/
└── paperjs-offset-devsite.esm-<hash>.jsmanifest.json 使用 dev-site-project@1 schema,声明项目摘要、标签、链接、截图、数据文件、带内容 hash 的 JS 资源和 runtime。gallery.json 将压力测试 case 导出为确定性的 SVG path fixture,并包含 operation、distance、options 和 expected quality。playground.json 从 gallery case 派生 preset 和 control schema。
runtime 导出三个挂载函数:
import {
mountGallery,
mountGuide,
mountPlayground
} from './runtime/paperjs-offset-devsite.esm.js';
mountGuide(container, guideData, {
basePath: '/projects/paperjs-offset/',
paper,
paperjsOffset
});示例中的 runtime 路径仅用于说明导出接口。实际集成时应使用 manifest.runtime,因为 build:dev-site 会为 JS/runtime 文件名加入内容 hash,避免静态缓存命中旧版本。
runtime 不依赖 window.onload,只操作传入的 container,CSS 类名使用 po- 前缀。资源路径均为相对路径,或通过 basePath 解析。交互依赖可以通过 options.paper 和 options.paperjsOffset 传入;如果依赖缺失,dev-site 仍可使用 assets/*.png 做静态 fallback。
兼容 API
旧入口仍然可用,但新代码应优先使用上面的命名函数。
静态类
import { PaperOffset } from 'paperjs-offset';
const expanded = PaperOffset.offset(path, 12, { join: 'round' });
const outline = PaperOffset.offsetStroke(path, 8, { cap: 'round' });
const quality = PaperOffset.analyze(path, expanded, 12);浏览器 bundle 也会暴露 window.PaperOffset,作为旧脚本的兼容别名。
原型扩展
import paper from 'paper';
import extendPaperJs from 'paperjs-offset';
extendPaperJs(paper);
const expanded = path.offset(12, { join: 'round' });
const outline = path.offsetStroke(8, { cap: 'round' });这会修改 paper.Path.prototype 和 paper.CompoundPath.prototype。它仍然保留用于兼容,但命名函数更容易类型标注、测试和迁移。
限制和问题反馈
路径偏移本质上是几何近似问题,尤其是在 Bezier 连接、自交、复合路径和大幅负向偏移附近。复杂图形仍然可能暴露边界情况。
反馈问题时,最好包含:
pathData- 距离
- 选项
- 使用的是
offset还是offsetStroke - 预期图形或截图
- 如果方便,附上
analyze(...)输出
开发
npm ci
npm run lint
npm test
npm run test:stress
npm run test:stress:quick
npm run test:stress:boundary常用构建命令:
npm run build
npm run build:ts
npm run build:bundlesnpm test 会重新构建包、运行 smoke tests,然后运行完整生成式 stress corpus。普通 gallery 分组可使用 test:stress:quick,更激进的 boundary 分组可使用 test:stress:boundary。
发布
发布流程刻意保持在本地触发;当前没有 GitHub Action 自动发布到 npm。准备发大版本时,先提交普通代码改动,然后运行:
npm run release:major也可以用 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。
确认版本提交和 tag 无误后,再执行真正的发布:
npm run release:shiprelease:ship 要求当前提交带有匹配当前版本号的 tag,会先检查 npm whoami 和 gh auth status,再重新运行 release:check,然后显式推送当前分支和版本 tag、用官方 npm registry 执行 npm publish,最后通过 GitHub CLI 执行 gh release create --generate-notes。如果 npm 发布需要 2FA,可以运行 npm run release:ship -- --otp=123456。
release:check 会运行 typecheck、测试、npm audit 和 npm pack --dry-run --ignore-scripts。npm test 已经先完成构建,因此 pack 校验不依赖 npm lifecycle。直接执行 npm publish 时,prepublishOnly 也会运行同一套 release check。
许可证
MIT。详见 LICENSE。
English
Overview
paperjs-offset provides offset geometry helpers for Paper.js. It generates closed path offsets, filled stroke outlines, and quality reports for generated offset geometry.
Project homepage: https://glenzli.com/projects/paperjs-offset/
Use the named functions for new code:
import paper from 'paper';
import { analyze, offset, offsetStroke } from 'paperjs-offset';
paper.setup(new paper.Size(400, 300));Recommended API
function offset(
path: paper.Path | paper.CompoundPath,
distance: number,
options?: OffsetOptions
): paper.Path | paper.CompoundPath;
function offsetStroke(
path: paper.Path | paper.CompoundPath,
distance: number,
options?: OffsetOptions
): paper.Path | paper.CompoundPath;
function analyze(
source: paper.Path | paper.CompoundPath,
result: paper.Path | paper.CompoundPath,
distance: number,
options?: { stroke?: boolean }
): OffsetQuality;offset(path, distance, options)expands or contracts a path.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.analyze(source, result, distance, options)scores generated geometry and reports warnings for hard cases.
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.
Install
npm install paper paperjs-offsetInstall paper in applications that use this addon. paperjs-offset does not declare runtime dependencies; the package ships CommonJS, ESM, TypeScript declarations, and browser bundles.
Quick Start
import paper from 'paper';
import { offset, offsetStroke } from 'paperjs-offset';
paper.setup(new paper.Size(400, 300));
const source = new paper.Path.Star({
center: [160, 120],
points: 8,
radius1: 70,
radius2: 36,
insert: false
});
const expanded = offset(source, 14, {
join: 'round',
insert: false
});
const outline = offsetStroke(source, 8, {
join: 'round',
cap: 'round',
insert: false
});API Patterns
1. Offset a Closed Shape
Positive distances expand closed shapes. Negative distances contract them.
const expanded = offset(source, 18, {
join: 'round',
insert: false
});
const contracted = offset(source, -18, {
join: 'round',
insert: false
});2. Build a Filled Stroke Outline
Use offsetStroke for open or closed paths when you need a fillable outline instead of a rendered stroke.
const openPath = new paper.Path({
segments: [[20, 80], [90, 30], [160, 80]],
insert: false
});
const outline = offsetStroke(openPath, 10, {
join: 'round',
cap: 'round',
insert: false
});For open paths, cap controls the terminals. For closed paths, the result is still a closed filled outline.
3. Inspect Quality
Offsetting Bezier geometry is approximate. Use analyze when a shape may contain tight curves, concave joins, self-intersections, or aggressive negative offsets.
const result = offset(source, -12, {
join: 'round',
algorithm: 'auto',
insert: false
});
const quality = analyze(source, result, -12);
console.log(quality.score, quality.warnings);When inspecting offsetStroke output, pass stroke: true:
const outline = offsetStroke(openPath, 10, {
join: 'round',
cap: 'round',
insert: false
});
const quality = analyze(openPath, outline, 10, { stroke: true });Options
interface OffsetOptions {
join?: 'miter' | 'bevel' | 'round';
cap?: 'butt' | 'round';
limit?: number;
insert?: boolean;
algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
}| Option | Default | Applies to | Notes |
| --- | --- | --- | --- |
| join | 'miter' | offset, offsetStroke | Corner reconstruction style. |
| cap | 'butt' | offsetStroke | Terminal style for open paths. |
| limit | 10 | offset, offsetStroke | Miter limit for sharp joins. |
| insert | true | offset, offsetStroke | Insert the result into the source parent or active layer. |
| algorithm | 'auto' | offset, offsetStroke | Strategy selection mode. |
Algorithms
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.
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.
| Mode | Use case |
| --- | --- |
| auto | Recommended application default. Chooses the best-scored result. |
| adaptive | Tries strict robust output first, then safer fallbacks for miter spikes and aggressive inward collapse. |
| robust | Extra cleanup for concave joins and self-intersecting stroke outlines. |
| split | Splits self-intersecting curves with less cleanup than robust. |
| legacy | Closest behavior to older releases when preserving historical output matters. |
OffsetQuality includes:
interface OffsetQuality {
algorithm?: 'auto' | 'adaptive' | 'robust' | 'split' | 'legacy';
score: number;
warnings: string[];
selfIntersections: number;
containmentErrors: number;
distanceErrors: number;
segmentCount: number;
area: number;
}Warnings can include empty results, non-finite bounds, self-intersections, unexpected area changes, containment errors, center shift, negative offset collapse, and distance collapse.
Browser Usage
For browser pages, load Paper.js first and then the browser build:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/paper-full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/paperjs-offset@2/dist/index.umd.min.js"></script>This uses @2 to follow the latest compatible v2 release. Pin an exact version such as [email protected] when you need fully reproducible builds.
Use the paperjsOffset global for the current API:
const expanded = paperjsOffset.offset(path, 12, {
join: 'round'
});
const outline = paperjsOffset.offsetStroke(path, 8, {
join: 'round',
cap: 'round'
});Dev Site Export Contract
This repository no longer maintains standalone demo pages. The project exports content, data, screenshots, and mountable runtime modules; the external dev-site owns layout and routing.
Build the standard export:
npm run build:dev-siteOutput:
dist/dev-site/
├── manifest.json
├── app/
│ ├── paper-full.min-<hash>.js
│ ├── paperjs-offset.umd.min-<hash>.js
│ └── paperjs-offset.esm-<hash>.js
├── assets/
│ ├── banner.png
│ ├── guide.png
│ ├── gallery.png
│ └── playground.png
├── data/
│ ├── guide.json
│ ├── gallery.json
│ └── playground.json
└── runtime/
└── paperjs-offset-devsite.esm-<hash>.jsmanifest.json uses the dev-site-project@1 schema and declares project copy, tags, links, screenshots, data files, content-hashed JS assets, and runtime. gallery.json exports stress cases as deterministic SVG path fixtures with operation, distance, options, and expected quality metadata. playground.json derives presets and controls from gallery cases.
The runtime exports three mount functions:
import {
mountGallery,
mountGuide,
mountPlayground
} from './runtime/paperjs-offset-devsite.esm.js';
mountGuide(container, guideData, {
basePath: '/projects/paperjs-offset/',
paper,
paperjsOffset
});The runtime path in this example only documents the exported interface. Integrations should read manifest.runtime because build:dev-site adds content hashes to JS/runtime filenames to avoid stale static-cache hits.
The runtime does not depend on window.onload, only mutates the provided container, and prefixes CSS classes with po-. Asset paths are relative, or resolved through basePath. Interactive rendering can receive dependencies through options.paper and options.paperjsOffset; if dependencies are missing, the dev-site can still use assets/*.png as static fallback.
Compatibility API
Older entry points still exist, but new code should prefer the named functions above.
Static Class
import { PaperOffset } from 'paperjs-offset';
const expanded = PaperOffset.offset(path, 12, { join: 'round' });
const outline = PaperOffset.offsetStroke(path, 8, { cap: 'round' });
const quality = PaperOffset.analyze(path, expanded, 12);The browser bundle also exposes window.PaperOffset as an alias for older scripts.
Prototype Extension
import paper from 'paper';
import extendPaperJs from 'paperjs-offset';
extendPaperJs(paper);
const expanded = path.offset(12, { join: 'round' });
const outline = path.offsetStroke(8, { cap: 'round' });This mutates paper.Path.prototype and paper.CompoundPath.prototype. It remains available for compatibility, but named functions are easier to type, test, and migrate.
Limitations and Bug Reports
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.
Bug reports are most useful when they include:
pathData- distance
- options
- whether the operation is
offsetoroffsetStroke - expected shape or screenshot
analyze(...)output when available
Development
npm ci
npm run lint
npm test
npm run test:stress
npm run test:stress:quick
npm run test:stress:boundaryUseful build commands:
npm run build
npm run build:ts
npm run build:bundlesnpm 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.
Release
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:
npm run release:majorUse 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.
After verifying the version commit and tag, ship the release:
npm run release:shiprelease: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, explicitly pushes the current branch and version tag, publishes to the official npm registry, and runs gh release create --generate-notes through the GitHub CLI. If npm publish requires 2FA, run npm run release:ship -- --otp=123456.
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.
License
MIT. See LICENSE.
