2DズームUIの基本のキ
https://gyazo.com/65317ccdaac94e467df6b1ba2262c84a
エディタ系アプリを開発しているとキャンバスのズーム機能が必要になることがあります
PCの場合、Photoshopや画像プレビューアプリはマウスのポインタ位置を基点にしてスクリーン上でその画素の位置が同じになるようにキャンバスのスケーリングと平行移動を行います
これは視覚的にも操作的にも非常にアタリマエ感があるUIなのですが、これを実装するには少し工夫が必要になります
まずこの図を見てください
https://gyazo.com/af343735ff169d09e5966d69574c081e
このキャンバス上には2つの座標系があります
viewport座標
ユーザにとって固定されている矩形がある座標系。一般的にはウィンドウ。
content座標
描画するコンテンツの座標系。
viewport座標とcontent座標の間には以下の等式がなりたちます
$ \begin{pmatrix} vx \\ vy \\ 1 \end{pmatrix} = M \times \begin{pmatrix} cx \\ cy \\ 1 \end{pmatrix}
$ \begin{pmatrix} cx \\ cy \\ 1 \end{pmatrix} = M^{-1} \times \begin{pmatrix} vx \\ vy \\ 1 \end{pmatrix}
この等式は、一般的にviewツリーの親子の間で成立する座標変換になります
ですので、viewツリーが伸びていった場合も、直近の親子同士でのアフィン変換を繰り返すことで任意の座標系同士の変換が可能になります
ドラッグ操作の変換
例えばキャンバス上でパン操作(ドラッグして移動)をした場合、キャンバスの内容は平行に移動するのが自然です
この場合viewport座標で発生したマウスイベントをトラックしてcontentの表示位置を変える必要があります
このとき、contentのtx, tyはこうなります
$ \begin{pmatrix} tx' \\ ty' \end{pmatrix} = \begin{pmatrix} tx + \delta x \\ ty + \delta y \end{pmatrix}
※$ \delta x ,$ \delta y はマウスのデルタ移動値
アフィン変換行列はこうなります
$ M' = \begin{bmatrix} 1 & 0 & \delta x \\ 0 & 1 & \delta y \\ 0 & 0 & 1 \end{bmatrix} \times M
これはアフィン変換を知らなくても普通にできると思います
ズーム操作の変換
ズームはアフィン変換的にはスケール値を変更する操作なので、アフィン変換行列はこうなります
$ M' = \begin{bmatrix} sx' / sx & 0 & 0 \\ 0 & sy'/s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \times M
この場合、M'とMのtxとtyの値が変わらないので、画像はtx, tyを基準に拡大されます
任意点を基準としたズーム操作の変換
viewport上の任意の点を基準にあるスケール値(sx',sy')にズームを行いたい場合は、ちょっとした工夫が必要です
なぜかというと、単純なズームと違い、ズーム後のcontentのマウスの下の画素が同じ場所にないといけないからです
つまり、スケールの値と平行移動の値(tx, ty)を同時に変える必要があるのです
それを実現するために以下の3ステップが必要になります
https://gyazo.com/2b7b4da589280237799f7ce8ae0edd57
まず、contentをviewport上のx,yの値だけ逆に平行移動させ、原点に戻します
次にその状態でスケーリングを行います
最後にx,yの値だけもとに平行移動させます
すると、マウスの下の画素の座標は固定されたままズームと平行移動ができています
上の画像にもあるのですが、変換後のcontentのtx, tyは、以下の式で表されます
$ \begin{pmatrix} tx' \\ ty' \end{pmatrix} = \begin{pmatrix} (tx-x)*(sx' / sx) + x \\ (ty-y)*(sy'/sy) + y \end{pmatrix}
アフィン変換行列式はこう
$ M' = \begin{bmatrix} 1 & 0 & x \\ 0 & 1 & y \\ 0 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} sx' / sx & 0 & 0 \\ 0 & sy'/sy & 0 \\ 0 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} 1 & 0 & -x \\ 0 & 1 & -y \\ 0 & 0 & 1 \end{bmatrix} \times M
この、アフィン変換式の掛け算は最後に行う変換が掛け算の最初に来るという原則を知らなかったせいで4年くらいズームUIを実装しようとするたびに計算が合わずに死んでました
が、恥を忍んで会社の先輩に助けを求めたら一瞬でうまくいきました
悲しいなぁ
実際に動くDEMOは以下からどうぞ