tags : TypeScript
セグメント数の均等化(equalize)
異なるセグメント数のパス間でモーフィングするため、少ない方を多い方に合わせて分割する。
分割数の配分(distributeCounts)
どのセグメントを何分割するかを 弧長比例 で決める。
- 追加で必要な分割数を、各セグメントの弧長の比率で配分
- 長いセグメントが優先的に分割される(短いセグメントを細かく割っても意味がない)
- 端数は余りが大きいセグメントに +1 して調整
例: 2セグメント → 3セグメントに増やす場合
- セグメント0(弧長80)→ 2分割
- セグメント1(弧長20)→ そのまま
セグメント内の分割位置(subdivideSegment)
1セグメント内の分割は パラメータ等分 (t = 1/n)。
- 弧長等分ではない(曲率によって実際の長さは不均等になる)
- 弧長等分にするなら
arcLengthToParamを使う手もあるが、モーフィング用途ではパラメータ等分で十分
De Casteljau アルゴリズムによる分割
制御点の線形補間を繰り返して分割点を求める。
元: start ── cp1 ── cp2 ── end
t で補間:
p01 = lerp(start, cp1, t)
p12 = lerp(cp1, cp2, t)
p23 = lerp(cp2, end, t)
p012 = lerp(p01, p12, t)
p123 = lerp(p12, p23, t)
p0123 = lerp(p012, p123, t) ← 分割点
左半分: start → p01 → p012 → p0123
右半分: p0123 → p123 → p23 → end重要な性質: 曲線の形を一切変えない 。同じ曲線を2つのセグメントで表現し直しているだけ。
弧長の計測
3次ベジェ曲線の弧長には閉じた数式(解析解)がない。
折れ線近似
曲線上に50個の点を等間隔にサンプリングし、隣り合う点同士の直線距離を足し上げる。
t=0 t=0.02 t=0.04 t=1.0
●──────●───────●─── ... ──────●
d0 d1 d49
弧長 ≈ d0 + d1 + d2 + ... + d49t での点の座標は正確
bezierPointAt はベジェの数式で厳密に求まる(近似ではない)。
近似が入るのは弧長(曲線に沿った長さ)の計測のみ。
「中間からほんの少しずれた位置に、正確に点を打つ」ということ。
カリー化による最適化
「初回に重い処理を済ませ、毎フレームは軽い計算だけにする」パターン。
構築時(1回だけ)
equalizePaths: 弧長計測 + 分割数配分 + De Casteljau 分割prepareLerpColor: 16進文字列のパースprepareLerpGradient: stop数の均衡化 + 色パースprepareLerpStroke: グラデーション解決 + 上記全部- セグメントのペア作成
→ 結果をクロージャに保存
毎フレーム(60fps = 毎秒60回)
各制御点の lerp(掛け算と足し算)だけ。
1セグメントあたり制御点3つ × xy = 掛け算6回・足し算6回。 カリー化しなかった場合、弧長計測だけで毎秒9000回の平方根計算が走る。
既存ライブラリとの比較
| ライブラリ | 補間単位 | 数の揃え方 | 曲線の性質 |
|---|---|---|---|
| このプロジェクト | ベジェ制御点 | De Casteljau 分割 | 保持される |
| GSAP MorphSVG | ベジェ制御点 | De Casteljau 分割 | 保持される |
| flubber | サンプリング点 | ポリゴン再サンプリング | 失われる |
| KUTE.js | サンプリング点 | ポリゴン再サンプリング | 失われる |
- GSAP MorphSVG が最も近いアプローチだが有料プラグイン
- Bezier.js は数学的操作を網羅するがモーフィング機能はない
- ベジェ制御点構造を保持したままモーフィングする OSS ライブラリは見つからなかった