0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#179 FigmaPluginでの逆行列を利用した仮想ガイドスナップの検証

0
Posted at

はじめに

前回の記事では、Figma Pluginでの座標変換の際に逆行列が必要になりそうだ(※)というところまで整理しました。 今回は実際に逆行列を利用してページ上の仮想ガイドにフレーム内Nodeをスナップすることは可能なのか、検証した内容についてまとめます。なお、本記事では行列についての解説は扱いませんのでご了承ください。

figma内での行列の取り扱いについては以下のリンクをご参照いただけますと幸いです。
https://developers.figma.com/docs/plugins/api/Transform/

※補足: 座標変換の際に逆行列が必要な理由について
figmaではページに配置されたNodeオブジェクトはページを基準とした絶対座標、フレーム内に配置されたNodeオブジェクトはフレームを基準とした相対座標で表されています。
Figma Plugin APIには、絶対座標への変換行列を取得するためのプロパティが用意されているため、相対座標→絶対座標に変換して比較することはできますが、絶対座標→相対座標に変換するための変換行列のプロパティは用意されていません。
そのため、絶対座標→相対座標に変換するためには、行列の世界で逆数のような立ち位置である「逆行列」を自前で計算して絶対座標→相対座標の変換に利用する必要があります。

手順案

Figma上で、ページに配置した仮想ガイド線(LineNode)に対して、フレーム内のNodeオブジェクトをスナップさせるために、次のような手順を検討しました。

  1. フレーム内オブジェクトの絶対座標(absoluteTransform)を取得する
  2. ガイド線の絶対座標(absoluteTransform)を取得する
  3. 1,2で取得したオブジェクトの座標とガイド線の座標を比較し、もしX座標(垂直ガイド)またはY座標(水平方向ガイド)の差が10px以内なら、オブジェクトの座標をガイド線の座標に置き換える(今回は動作確認のため閾値を仮に10pxに設定)
  4. 親フレームの絶対座標(absoluteTransform)を取得し、その逆行列を計算する
  5. 3で得られた新しい絶対座標と4で計算した逆行列を使って、スナップさせたオブジェクトの新しい相対座標(relativeTransform)を計算する
  6. 計算した相対座標を対象オブジェクトのrelativeTransformに再代入する(absoluteTransformは読み取り専用ですが、relativeTransformの方は読み書き可能です)
  7. これでオブジェクトがガイド線にスナップされた位置へ移動するはず

逆行列部分の確認

まずは逆行列部分の計算を作り、それが正しいかどうかを確認しました。

逆行列とは「行列を掛けると単位行列になるような行列」のことです。行列には逆行列を持つもの(正則行列)と、行列式が0になってしまうため逆行列を持たないものがあります。逆行列が存在しない場合というのは、高さや幅が潰れてしまい、元に戻せないようなイメージです。

Figma公式によるとrelativeTransformには高さや幅を0にするスケールは含まれていません。また、absoluteTransformはrelativeTransformをいくつか掛け合わせたものであるため、今回のケースで逆行列の分母の値が0になることは考えにくいです。しかし、数学的な正しさのため、念のため条件分岐を入れておくことにしました。

.ts
// FigmaAPIから取得できるrerlativeTransform(相対座標)と
// 読み取り専用のabsoluteTransform(絶対座標)の型
type Transform = [
  [number, number, number],
  [number, number, number]
];

// ページ直下のフレームの絶対座標 兼 相対座標
let parentTransform: Transform;

// フレーム内の対象オブジェクトの相対座標
let targetRelativeTransform: Transform;

// 対象オブジェクトの絶対座標
// targetAbsoluteTransform = parentTransform * targetRelativeTransform
let targetAbsoluteTransform: Transform;

// 2x3行列(アフィン変換行列)の逆行列計算
function invertMatrix(m: Transform): Transform {
  const [a, b, c] = m[0]; // absoluteTransform1行目
  const [d, e, f] = m[1]; // absoluteTransform2行目
  const det = a * e - b * d; //逆行列の公式の分母
  
  // NOTE: 
  // 高さや幅を0にするスケールの部分は
  // 行列(rerativeTransform)に含まれていないことが
  // Figma公式で明記されているため
  // おそらくこの値が0になることはないですが、
  // 数学的な正しさとしては分母が0になってはいけないため
  // 一応条件として付けることにします
  if (det !== 0) {
    // 逆行列をかえす
    return [
      [e / det, -b / det, (b * f - c * e) / det],
      [-d / det, a / det, (c * d - a * f) / det]
    ];
  } else {
    // ここに来ることはないはず
    throw new Error("逆行列が存在しません");
  }
}

次に、逆行列を使ってオブジェクトの相対座標を得るための計算部分を作りました。
行列の掛け算は数の掛け算と違って順序を変えると別のものになってしまうので順序を変えないようにしながら変形する(消したいものをどちらからかけるか考える)ことと、Transform型は拡大縮小・回転・平行移動を表すアフィン変換行列(ここにアフィン変換行列の定義)なので、平行移動分の計算のために、相対座標の行列に戻るか確かめる計算をするときは架空次元の3行目を補って計算することに気をつけました。

.ts
// フレームの絶対座標の逆行列と対象オブジェクトの絶対座標を使って、
// 対象オブジェクトのもとのフレーム内相対座標の行列に戻るか確かめる計算

// NOTE: 
// targetAbsoluteTransform = parentTransform * targetRelativeTransform なので、
// targetRelativeTransform = ~~ の形にするために、
// 両辺に(parentTransform ** -1)を左からかけてparentTransformをけして、
// 左右をひっくり返すと、
// targetRelativeTransform = (parentTransform ** -1) * targetAbsoluteTransform
//                         = parentTransformの逆行列 * targetAbsoluteTransform
//                         = parentInverseMatrix * targetAbsoluteTransform

function changeRelative(parentInverseMatrix: Transform, targetAbsoluteTransform: Transform) {
  const [a1, b1, c1] = parentInverseMatrix[0];
  const [d1, e1, f1] = parentInverseMatrix[1];
  const [a2, b2, c2] = targetAbsoluteTransform[0];
  const [d2, e2, f2] = targetAbsoluteTransform[1];
  //    [ 0,  0,  1] = targetAbsoluteTransform[2]; 
  // NOTE: 上記はアフィン変換行列を3*3行列積で計算するための補助行、この架空次元の3行目があると考えて計算します

  return [
    // parentInverseMatrix * targetAbsoluteTransform
    // NOTE: 2×3のアフィン行列は「2×2の線形部分」と「平行移動部分」に分けて考えます
    [
      a1 * a2 + b1 * d2, // 通常の2*2行列の積と同じ計算、掛けて掛けて足す
      a1 * b2 + b1 * e2, // 通常の2*2行列の積と同じ計算、掛けて掛けて足す
      a1 * c2 + b1 * f2 + c1 // アフィン変換行列特有の平行移動分の計算、親の線形変換で子の平行移動を変換+親の平行移動
      // NOTE: 
      // 上記3行は、架空次元の3行目を含めて計算した、
      // a1 * a2 + b1 * d2 + c1 * 0
      // a1 * b2 + b1 * e2 + c1 * 0
      // a1 * c2 + b1 * f2 + c1 * 1 と同値です
    ],
    [
      d1 * a2 + e1 * d2,
      d1 * b2 + e1 * e2,
      d1 * c2 + e1 * f2 + f1
      // NOTE: 
      // 上記3行は、架空次元の3行目を含めて計算した、
      // d1 * a2 + e1 * d2 + f1 * 0
      // d1 * b2 + e1 * e2 + f1 * 0
      // d1 * c2 + e1 * f2 + f1 * 1 と同値です
    ]
  ];
}


固定値を使って、逆行列計算で元の相対座標に戻せるかを確認しました。

.ts
// フレームの絶対座標 兼 相対座標
parentTransform = [
  [1, 0, 500], // ページ内でx方向に500移動
  [0, 1, 500]  // ページ内でy方向に500移動
];

// 対象オブジェクトのフレーム内相対座標(元の値)
targetRelativeTransform = [
  [1, 0, 100], // フレーム内でx方向に100移動
  [0, 1, 200]  // フレーム内でy方向に200移動
];

// 対象オブジェクトの絶対座標
targetAbsoluteTransform = [
  [1, 0, 600], // フレーム内でx方向に100移動
  [0, 1, 700]  // フレーム内でy方向に200移動
];

// 対象オブジェクトの絶対座標行列の逆行列
const parentInverseMatrix = invertMatrix(parentTransform);

console.log("フレーム内オブジェクトの元の相対座標行列:");
console.log(targetRelativeTransform);
console.log("親フレームの絶対座標行列:");
console.log(parentTransform);
console.log("親フレームの絶対座標行列の逆行列:");
console.log(parentInverseMatrix);
console.log("フレーム内オブジェクトの絶対座標行列:");
console.log(targetAbsoluteTransform);
console.log("逆行列を使って戻したフレーム内オブジェクトの相対座標:");
console.log(changeRelative(parentInverseMatrix, targetAbsoluteTransform));


// コンソール出力結果
フレーム内オブジェクトの元の相対座標行列:
[ [ 1, 0, 100 ], [ 0, 1, 200 ] ]
親フレームの絶対座標行列:
[ [ 1, 0, 500 ], [ 0, 1, 500 ] ]
親フレームの絶対座標行列の逆行列:
[ [ 1, -0, -500 ], [ -0, 1, -500 ] ]
フレーム内オブジェクトの絶対座標行列:
[ [ 1, 0, 600 ], [ 0, 1, 700 ] ]
逆行列を使って戻したフレーム内オブジェクトの相対座標:
[ [ 1, 0, 100 ], [ 0, 1, 200 ] ]


結果は、期待どおりに、対象オブジェクトの移動後の相対座標を再取得できることが確認できました。

Figma上での動作確認

FigmaのNodeのTransformはNode左上の基準点を表すようです。 今回は、この基準点が逆行列を使った計算でガイド線にスナップできるかどうかを確認しました。

確認した内容は以下のとおりです。

  • スナップ後に座標が正しく更新されるか
  • Node左上の座標点がガイド線に吸着した見た目になるのか
  • 変形を加えた場合の挙動はどうか

なお、本来左上以外の座標点についても別途計算してガイド線に1番近い座標点をスナップさせるべきかと思いますが、今回はスナップの動作が可能かまでの確認に絞ったため他の座標点は一旦考慮していません。

結果として、閾値(10px)以下の距離へ近づけた状態で、仮想ガイド線へのスナップが行われました。

↓オブジェクトを閾値内に持ってきます


このとき、relativeTransformの右端2つの値はNodeオブジェクト左上の座標を表しています(次の画像のConsole内「フレーム内オブジェクトの元の相対座標」の3, 6個目の値と左上の座標が同じになっています)

↓スナップされます
座標もガイド線と同位置に変更されています


また、90°回転して作成した垂直ガイドにも同様にスナップできることを確認しました。




変形時はバウンディングボックス上の左上の点がスナップされました。これはfigma本来のガイド線と同じ挙動です。

↓変形時、figmaの本物のガイド線の場合
(スナップされて線の色が濃くなった位置です)


↓変形時、仮想ガイド線へのスナップの場合


まとめ

人生で初めて行列という概念に触れたので理解にかなり時間がかかりましたが、今回の検証で、逆行列を使えばフレーム内ノードの相対座標を正しく再取得できることが確認できました。これにより、変形時も含めページ上の仮想ガイドに対してノードをスナップさせることが可能であることがわかりました。

スナップのタイミングや複数ガイドへの対応、閾値の調整など、より実用的なスナップ機能への課題はまだまだ残っていますが、少しずつ現実的なものへ発展させていけたらいいなと思います。

最後までお読みいただきありがとうございました。

参考

参考書籍:
結城浩(2018)『数学ガールの秘密ノート/行列が描くもの』SBクリエイティブ

参考サイト:
Figma関連
https://developers.figma.com/docs/plugins/api/properties/nodes-relativetransform/

https://developers.figma.com/docs/plugins/api/Transform/

アフィン変換行列について
https://qiita.com/yuba/items/7fb6a49adfda8fa466d8

https://w3e.kanazawa-it.ac.jp/math/category/gyouretu/senkeidaisu/affine_transformation.html

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?