13 KiB
bezier-kit
依存ゼロの TypeScript 製 3 次ベジェ曲線ライブラリ。2D / 3D 両対応で、モーフィング・分割・弧長計算・パス生成を同じ API で扱える。
インタラクティブデモを見る → sumisonic.github.io/bezier-kit
@sumisonic/bezier-kit-core— 幾何学操作(依存ゼロ、2D / 3D 両対応)@sumisonic/bezier-kit-style— 色・グラデーション・ストロークを伴うスタイル付きパス(core に依存、2D 限定)
クイックスタート
Note
: npm にはまだ公開していません。現時点ではリポジトリを clone してローカルで利用してください(
pnpm install→pnpm build)。pnpm addでの導入は今後対応予定。
import { createPathInterpolator, fromCatmullRom, type Point2D } from '@sumisonic/bezier-kit-core'
// 2 形状のモーフィング
const pathA = fromCatmullRom<Point2D>([
{ x: 0, y: 0 },
{ x: 100, y: 50 },
{ x: 200, y: 0 },
])
const pathB = fromCatmullRom<Point2D>([
{ x: 0, y: 0 },
{ x: 100, y: -50 },
{ x: 200, y: 0 },
])
const interp = createPathInterpolator(pathA, pathB)
// 毎フレーム 0〜1 を行き来させる
requestAnimationFrame(function tick(t) {
const phase = (Math.cos(t / 400) + 1) / 2
const path = interp(phase)
// path を Canvas 2D / SVG / three.js 等で描画
requestAnimationFrame(tick)
})
3D に切り替えるには型パラメータを Point3D にするだけ:
import { fromCatmullRom, createPathInterpolator, type Point3D } from '@sumisonic/bezier-kit-core'
const a = fromCatmullRom<Point3D>([
{ x: 0, y: 0, z: 0 },
{ x: 1, y: 1, z: 1 },
])
const interp = createPathInterpolator(a, b) // 引数・戻り値すべて Point3D
2D と 3D のパスを混ぜて渡すとコンパイルエラーになるので、次元の取り違えは型で防げる。
機能
- モーフィング: 2 つのパスを事前構築 1 回 + 毎フレーム呼び出し ~1μs で補間。Back / Elastic 系 easing で
tが 0〜1 の範囲外になっても自然に動く - セグメント数の自動マッチング:
fromとtoのセグメント数が違っても補間できる(matchSegmentCountが弧長比例で均等化) - 弧長比率でのパス上問い合わせ:
pointAtLength(path, ratio)/tangentAtLength(path, ratio)で座標と接線ベクトルを取得 - 弧長比率でのパス分割:
createPathSplitterで任意位置から 2 つに切り分け、分割後も補間・再分割が可能 - 点列からのパス生成:
fromCatmullRom(滑らかなスプライン)とfromPolyline(折れ線) - Frenet フレーム(3D、ホットパス向け): double-reflection 法で twist-free な (T, N, B) を
Float32Arrayに直接書き込み。Tube / Ribbon 描画に使える - Catmull-Rom の Float32Array 直書き API: 毎フレームの
BezierPath生成を回避し、alloc ゼロで制御点 → 幾何情報を変換 - スタイル付きパスの補間(
@sumisonic/bezier-kit-style): 色・グラデーション・ストロークを含む 2D パスを同じ手順でアニメーション可能 - 型で 2D / 3D を区別: 混在呼び出しはコンパイルエラー
主要 API
core
import {
// 型
type Point2D,
type Point3D,
type BezierPath,
type BezierSegment,
type BBox2D,
type BBox3D,
// パスの操作
createPathInterpolator,
createPathInterpolatorStrict,
createPathSplitter,
matchSegmentCount,
// 弧長比率での問い合わせ
pointAtLength,
tangentAtLength,
// セグメントレベル
pointAt,
tangentAt,
splitSegmentAt,
arcLengthTo,
segmentLength,
// 弧長インデックス(大量呼び出しの高速化)
createArcLengthIndex,
arcLengthToParam,
// バウンディングボックス
bbox,
// 点列からの生成
fromCatmullRom,
fromPolyline,
// Functor(2D ↔ 3D 変換・平行移動など)
mapPoints,
// 数学ユーティリティ
lerp,
clamp,
lerpPoint,
distance,
} from '@sumisonic/bezier-kit-core'
モーフィング(事前構築 + 毎フレーム呼び出し)
const interp = createPathInterpolator(pathA, pathB)
// t = 0 で pathA、t = 1 で pathB、範囲外で外挿
const morphed = interp(t)
tは 0〜1 の範囲外も OK(Back / Elastic 系 easing で-0.2や1.2になっても自然に動作)- セグメント数が異なる場合は内部で自動マッチング
セグメント数の一致を強制したい場合は createPathInterpolatorStrict(不一致で throw)。
弧長比率でパス上の点と接線を取る
const p = pointAtLength(path, 0.5) // パス上で弧長比率 50% の点
const v = tangentAtLength(path, 0.5) // 同じ位置の進行方向ベクトル
// 2D で角度が欲しい場合
const angle = Math.atan2(v.y, v.x)
ratioは内部でclamp(0, 1)されるので範囲外でも安全- 大量に呼ぶ場合は
createArcLengthIndex+arcLengthToParamで事前計算すると高速
弧長比率でのパス分割
const split = createPathSplitter(path)
const [left, right] = split(0.3) // 弧長で 30% / 70% に分ける
分割点は前半の end と後半の start で完全に一致する(De Casteljau 分割の性質)。
2D → 3D 変換(mapPoints)
// 2D パスに z=0 を付加して 3D パスにする
const path3d = mapPoints<Point2D, Point3D>(path2d, (p) => ({ x: p.x, y: p.y, z: 0 }))
// 平行移動、スケール、回転など任意の点変換にも使える
const shifted = mapPoints<Point2D, Point2D>(path2d, (p) => ({ x: p.x + 10, y: p.y }))
Frenet フレーム(3D 限定、ホットパス向け)
3D パスに沿った twist-free な直交基底 (T, N, B) をサンプル点ごとに計算する。double-reflection 法で法線の捩れを最小化し、Tube geometry や Ribbon 描画に使える。
import {
FRENET_STRIDE,
FRENET_OFFSET,
writeFrenetFrames,
readFrenetFrame,
computeFrenetFrames,
} from '@sumisonic/bezier-kit-core'
// デバッグ / 単発利用: オブジェクト配列を取得
const frames = computeFrenetFrames(path, samples)
frames[0].tangent // { x, y, z }、正規化済み
frames[0].normal // 同上、T と直交
frames[0].binormal // 同上、= T × N
// ホットパス: Float32Array に in-place 書き込み(alloc ゼロ)
const framesBuffer = new Float32Array(samples * FRENET_STRIDE)
writeFrenetFrames(framesBuffer, path, samples)
// バッファ直読み: stride + offset で任意成分にアクセス
const frameIdx = 5
const off = frameIdx * FRENET_STRIDE
const tx = framesBuffer[off + FRENET_OFFSET.TANGENT]
const ty = framesBuffer[off + FRENET_OFFSET.TANGENT + 1]
const tz = framesBuffer[off + FRENET_OFFSET.TANGENT + 2]
- twist-free: 隣接フレーム間の
Nの変化が最小(double-reflection 法) - alloc ゼロ:
pointAt/tangentAtを呼ばず、3 次ベジェ式を手展開で計算 - 精度制御:
{ arcLengthSamples: 64 }で弧長サンプル数を指定(既定 64) FRENET_STRIDE = 12とFRENET_OFFSET(POSITION=0, TANGENT=3, NORMAL=6, BINORMAL=9)はメジャーバージョン内 stable
Catmull-Rom の Float32Array 直書き API(ホットパス向け)
毎フレーム fromCatmullRom を呼ぶとパス 1 本につき数十個のオブジェクトが生成される。制御点が Float32Array で与えられる用途(WebAudio/WebXR/WASM 連携等)では、BezierPath オブジェクトを作らず直接セグメントの数値列を書き出す API を使える。
import {
CATMULL_ROM_SEGMENT_STRIDE,
CATMULL_ROM_SEGMENT_OFFSET,
writeCatmullRomSegments,
writeFrenetFramesFromCatmullRom,
} from '@sumisonic/bezier-kit-core'
// 20 制御点 → 19 セグメント(segment 1 個あたり 12 floats: start xyz, cp1 xyz, cp2 xyz, end xyz)
const controlPoints = new Float32Array(20 * 3) // [x0, y0, z0, x1, y1, z1, ...]
const segments = new Float32Array(19 * CATMULL_ROM_SEGMENT_STRIDE)
writeCatmullRomSegments(segments, controlPoints, 20)
// 制御点から直接 Frenet フレームへ(一体化 API、中間 segments バッファを隠蔽)
const samples = 101
const frames = new Float32Array(samples * FRENET_STRIDE)
writeFrenetFramesFromCatmullRom(frames, controlPoints, 20, samples)
CATMULL_ROM_SEGMENT_STRIDE = 12/CATMULL_ROM_SEGMENT_OFFSETはメジャーバージョン内 stable- 既存
fromCatmullRomと float32 精度で一致(1e-4 以内)
style(2D 限定)
import {
createStyledPathInterpolator,
createStyledPathSplitter,
matchStyledPathCount,
createStrokeLerp,
gradientToAbsolute,
remapGradient,
parseHexColor,
toHexColor,
lerpColor,
createColorLerp,
type StyledBezierPath,
type StrokeStyle,
type LinearGradient,
type GradientStop,
type ViewBox,
type StyledBezierData,
} from '@sumisonic/bezier-kit-style'
ストローク補間(createStrokeLerp)の仕様
片方しか存在しない値の扱いは以下のルール:
| 入力 | 出力 |
|---|---|
両方 undefined |
常に undefined |
片方だけ stroke あり |
固定値(補間なし) |
両方にあり、両方 width あり |
width を線形補間 |
片方だけ width |
固定値 |
両方にあり、両方 color あり |
color を RGB 線形補間 |
両方にあり、片方だけ gradient |
color から仮グラデーションを合成して補間 |
両方 gradient あり |
stop 数を揃えて座標・色・offset を補間 |
描画例
本ライブラリは描画エンジン非依存。パスデータを返すだけで、描画は呼び出し側の責務。
Canvas 2D
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.moveTo(path.start.x, path.start.y)
for (const seg of path.segments) {
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y)
}
ctx.stroke()
SVG
const d =
`M ${path.start.x} ${path.start.y} ` +
path.segments.map((s) => `C ${s.cp1.x} ${s.cp1.y}, ${s.cp2.x} ${s.cp2.y}, ${s.end.x} ${s.end.y}`).join(' ')
// React なら <path d={d} stroke="currentColor" fill="none" />
three.js(3D)
import * as THREE from 'three'
import { fromCatmullRom, pointAtLength, type Point3D } from '@sumisonic/bezier-kit-core'
const path = fromCatmullRom<Point3D>([
{ x: 0, y: 0, z: 0 },
{ x: 1, y: 1, z: 1 },
{ x: 2, y: 0, z: 2 },
])
const samples = 96
const points = Array.from({ length: samples }, (_, i) => {
const p = pointAtLength(path, i / (samples - 1))
return new THREE.Vector3(p.x, p.y, p.z)
})
const curve = new THREE.CatmullRomCurve3(points, false, 'catmullrom', 0.5)
const geometry = new THREE.TubeGeometry(curve, 96, 0.1, 16, false)
インタラクティブデモ
本リポジトリの examples/ にある:
canvas-morph— Canvas 2D の 2D ランダム形状モーフィング + 分割threejs-tube— @react-three/fiber の 3D ランダムチューブモーフィング + 分割
pnpm example:canvas
pnpm example:three
型階層
type Point2D = { readonly x: number; readonly y: number }
type Point3D = { readonly x: number; readonly y: number; readonly z: number }
type Point = Point2D | Point3D
type BezierSegment<P extends Point> = { readonly cp1: P; readonly cp2: P; readonly end: P }
type BezierPath<P extends Point> = { readonly start: P; readonly segments: readonly BezierSegment<P>[] }
type BBox2D = { readonly minX; readonly minY; readonly width; readonly height }
type BBox3D = BBox2D & { readonly minZ; readonly depth }
type BBox<P extends Point> = P extends Point3D ? BBox3D : BBox2D // 入力次元に連動
BezierPath<Point2D> / BezierPath<Point3D> を明示的に書く(デフォルト型パラメータなし)。
対応していないこと
- 自己交差の検出 / boolean 演算(intersect / subtract 等)
- SVG path
d文字列のパース / 生成 - HSL / OKLCh 色補間(RGB 線形補間のみ)
- 2 次ベジェ / 楕円弧セグメント(3 次固定)
- 弧長の極値解析による厳密な bbox(現在の
bboxは制御点凸包ベースの上界) - 3D グラデーション / テクスチャ(style は 2D 限定)
他ライブラリとの棲み分け
- bezier-js: 広範なベジェ幾何操作(offset、projection 等)。bezier-kit は動的モーフィング / 弧長経路問い合わせに特化 + 3D 対応
- paper.js: 描画・パス編集まで含む総合フレームワーク。bezier-kit はデータ層のみ
- three.js の
CatmullRomCurve3: three.js 専用。bezier-kit は three.js 非依存で汎用的
ライセンス
MIT © sumisonic
開発・コントリビュート
CONTRIBUTING.md を参照。