TL;DR
- 複素数平面に対してのアフィンなので、それはそう
本文
曲線とその2端点が与えられていて、今は始点$A$から終点$B$まで曲線が伸びているけど、それを拡大・縮小・回転・並行移動することで始点$A'$から終点$B'$まで伸びるようにしたい、という問題を考えようと思います。
私がこの問題に直面したのは、「以下の図のように、辺の両端に短い線(スラブ)を付けたフォント(スラブセリフという)を用意するので、それを元に肉付けしたものにしてほしい」という要望がきっかけでした。
https://github.com/sozysozbot/linzklar-typesetting
これをどのように実現するか少し考えたところ、元の曲線を拡大・縮小・回転・平行移動させ、両端がスラブに乗るようにすればいいと気づきました。
そこで重要になってくるのが、以下の点です。
- それをする方法は必ず存在するのか
- それをする方法は必ず一通りなのか
- それは簡単に計算できるのか
答え
- 必ず存在します
- 必ず一通りです1
- 比較的簡単に計算できます
直感的な説明
まず、before と after で、それぞれ始点から終点までの直線2を引いてみましょう。
次に、線の距離を測り、after の距離を before の距離で割ってみましょう。これで拡大率が出ます。
そうしたら、今度は直線の角度を測り、その角度差を計算しましょう。
ということで、今回の$A$, $B$をそれぞれ $A' $, $B' $ に写すためには、1.20倍の拡大と、25.48度の反時計回り回転が必要であるということが分かりました。したがって、これらの拡大と回転をしたあとに、平行移動で $A$ を $A'$ に重ねれば、勝手に $B$ が $B'$ に重なります。というわけで、必ずできるのです。
さらに直感的な説明
本記事を書いたところ、解答略さん( https://twitter.com/kaitou_ryaku )が 1 ツイートに収まる簡潔な要約をしていたので、付け加えておきます。
直感的には、スマホで画像を開いて、親指と人差し指をディスプレイに付けて、グッと開いて画像を拡大し、その手の形を保ったままキュッと画像を動かすのを考えると良さそう。
グッと開くときの指の広がりで拡大率が一意に定まり、キュッと動かすので回転・平行移動が一意に定まるのが直感的にわかる
複素数平面による証明(読み飛ばしても構いません)
写像 $f(z) = uz+w$ に関して $f(\alpha) = \alpha', f(\beta) = \beta'$ (ただし $\alpha \ne \beta$)となるためには、$\beta'-\alpha' = f(\beta)-f(\alpha) = (u\beta+w)-(u\alpha+w) = u(\beta-\alpha) $ なので $u = \frac{\beta'-\alpha'}{\beta-\alpha}$ が必要。また $\alpha' = u\alpha + w$ となるためには $w = \alpha' - u\alpha = \alpha' - \frac{\beta'-\alpha'}{\beta-\alpha}\alpha = \frac{\alpha'\beta-\alpha\beta'}{\beta-\alpha}$ が必要。
逆に、 $u = \frac{\beta'-\alpha'}{\beta-\alpha}, w=\frac{\alpha'\beta-\alpha\beta'}{\beta-\alpha}$ であれば、$f(z) = uz+w$ は $f(\alpha) = \alpha', f(\beta) = \beta'$ を満たす。よって示された。
SVG での応用方法
SVG では、transform="matrix(1.083 -0.516 0.516 1.083 21.744 -54.366)"
などと書くことによって、以上の「拡大・縮小・回転・平行移動」を一気に要素に適用することができます3。詳しい導出は省略しますが、
$$d_x = b_x-a_x, \quad d_y = b_y-a_y, \quad d_x' = b_x'-a_x', \quad d_y' = b_y'-a_y'$$
$$K = d_x^2+d_y^2, \quad u_r = \frac{d_x d_x' + d_yd_y'}{K}, \quad u_i = \frac{d_xd_y'-d_yd_x'}{K} $$
$$w_x = a_x' -u_ra_x+u_ia_y, \quad w_y = a_y' -u_ra_y -u_ia_x$$
とすると
$$\begin{pmatrix}u_r&-u_i & w_x \\ u_i & u_r & w_y \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix}a_x & b_x \\ a_y & b_y \\ 1&1 \end{pmatrix} = \begin{pmatrix}a_x' & b_x' \\ a_y' & b_y' \\ 1&1 \end{pmatrix}$$
が成り立ちます。transform
属性はこの$\begin{pmatrix}u_r&-u_i & w_x \\ u_i & u_r & w_y \\ 0 & 0 & 1 \end{pmatrix}$ を「縦に」読んだものなので、四則演算だけでこの transform
属性の値を計算してやることができます。JavaScript 実装がこちら。
function computeSvgTransform(before, after) {
const d = { x: before.b.x - before.a.x, y: before.b.y - before.a.y };
const d_after = { x: after.b.x - after.a.x, y: after.b.y - after.a.y };
const K = d.x * d.x + d.y * d.y;
const u_r = (d.x * d_after.x + d.y * d_after.y) / K;
const u_i = (d.x * d_after.y - d.y * d_after.x) / K;
const w = {
x: after.a.x - u_r * before.a.x + u_i * before.a.y,
y: after.a.y - u_r * before.a.y - u_i * before.a.x
};
return `transform="matrix(${u_r} ${u_i} ${-u_i} ${u_r} ${w.x} ${w.y})"`;
}
console.log(computeSvgTransform(
{ a: { x: 2, y: 7 }, b: { x: 5, y: 3 } },
{ a: { x: 20, y: -3 }, b: { x: 32, y: 2 } }
)); // `transform="matrix(0.64 2.52 -2.52 0.64 36.36 -12.52)"`
実際に試してみた画像がこちら。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 40 20">
<path d="M2 7L5 3" stroke-width=".4" stroke="black" />
<path d="M20 -3L32 2" stroke-width=".4" stroke="black" />
<path d="M 2 7 C 3 7 5 5 5 3" stroke="blue" stroke-width=".4" fill="none" />
<g transform="matrix(0.64 2.52 -2.52 0.64 36.36 -12.52)">
<path d="M 2 7 C 3 7 5 5 5 3" stroke="blue" stroke-width=".4" fill="none" />
</g>
</svg>
$(2,7)$ から $(5,3)$ に向けて伸びているパス M 2 7 C 3 7 5 5 5 3
に、今計算した matrix(0.64 2.52 -2.52 0.64 36.36 -12.52)
を適用してやることで、無事 $(20, -3)$ から $(32, 2)$ に向けて伸びるパスになってくれました。