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 + ... + d49

t での点の座標は正確

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 ライブラリは見つからなかった