色を置き換えるFigmaプラグインを作ろう ~後編~

#はじめに
色を置き換えるFigmaプラグイン開発の後編です。
[前編](https://qiita.com/310ma3/items/da41da464a856db4c42c)では特定の色に一括置換するところまでを作りました。
後編では使い勝手を考慮してUIから操作できるようにしていきました。

できたものがこちらです。
![色変えプラグイン.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44990/18b95e24-67a6-4b12-1067-0b3efc2f4f8d.gif)
流れるように色を試せるように、UIにhoverするだけで選択しているオブジェクトの色が変わるようにしました。
また、登録していない色もその場で試せるようにhexの値を入力できる枠も用意しました。

#コード
```ui.html
<style>
body{
  font-family:Arial, Helvetica, sans-serif;
}
label{
  font-weight: bold;
}
#other-color-text-field{
  width: 50px;
}
button{
  width: 100%;
  height: 40px;
}
</style>

<!-- ここによく使う色を追加していく -->
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color1" value="49C0EB" checked="checked">
  <label for="color1" style="color: 49C0EB;">blue</label>
</p>
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color2" value="2CC679">
  <label for="color2" style="color: 2CC679;">green</label>
</p>
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color3" value="8491A5">
  <label for="color3" style="color: 8491A5;">dark gray</label>
</p>
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color4" value="B4C4D3">
  <label for="color4" style="color: B4C4D3;">light gray</label>
</p>
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color5" value="B5A4FF">
  <label for="color5" style="color: B5A4FF;">purple</label>
</p>
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="color6" value="E75252">
  <label for="color6" style="color: E75252;">red</label>
</p>
<!-- これだけカスタム枠 -->
<p class="hover-area">
  <input type="radio" name="color-radio-btn" id="custom-color-radio-btn" value="inputed-color">
  <label id="other-color-label" for="custom-color-radio-btn">other:<input id="other-color-text-field"></label>
</p>
  
<script>
  const hoverAreas = document.getElementsByClassName("hover-area"); 
  for (let i = 0; i < hoverAreas.length; i++) {
    hoverAreas[i].addEventListener("mouseover", () => {
      hoverAreas[i].getElementsByTagName("input")[0].checked = true;
      changeColor();
    });
  }

  document.getElementById('other-color-text-field').onchange = () => {
    document.getElementById('other-color-label').style.color = document.getElementById("other-color-text-field").value;
    changeColor();
  }

  function changeColor(){
    const radioBtns = document.getElementsByName('color-radio-btn');
    radioBtns.forEach((color) => {
      if (!color.checked) {
        return;
      }
      let val = color.value;
      if (val === "inputed-color") {
        val = document.getElementById("other-color-text-field").value;
        // hexの指定に必要な6桁でなければはじく
        if (val.length !== 6) {
          return;
        }
      }
      parent.postMessage({ pluginMessage: { type: 'change-color', val } }, '*')
    });
  }
</script>
```
```code.ts
// 色を増減したらwindowの大きさを調整する必要あり
figma.showUI(__html__, { width: 140, height: 250 });

figma.ui.onmessage = msg => {
  if (msg.type === 'change-color') {
    const color: String = msg.val;
    figma.currentPage.selection.forEach((item) => {
      setColorRecursively(item, color);
    });
  }
};

function setColorRecursively(item: SceneNode, color: String) {
  // GeometryMixinを実装しているtypeが色変更の対象
  if (item.type === "RECTANGLE" || item.type === "LINE" || item.type === "ELLIPSE" ||
    item.type === "POLYGON" || item.type === "STAR" || item.type === "VECTOR" ||
    item.type === "TEXT" || item.type === "FRAME" || item.type === "INSTANCE" || item.type === "BOOLEAN_OPERATION") {
    // 塗りと線の色を変更
    const cloneFills = clone(item.fills);
    item.fills = changePaints(cloneFills, color);
    const cloneStrokes = clone(item.strokes);
    item.strokes = changePaints(cloneStrokes, color);
  }
  // 再帰処理
  if (item.type === "INSTANCE" || item.type === "GROUP" || item.type === "FRAME" || item.type === "BOOLEAN_OPERATION") {
    item.children.forEach((child) => {
      setColorRecursively(child, color);
    });
  }
}

function changePaints(paints: Array<any>, color: String) {
  if (paints !== null && paints.length > 0) {
    paints.forEach((paint, index) => {
      paints[index] = changePaint(paint, color);
    });
  }
  return paints;
}

function changePaint(paint, color: String) {
  // 単色塗り以外（グラデーション、画像）は対応しない
  if (paint.type !== "SOLID") {
    return paint;
  }
  // 16進数を10進数に変換した後、0~1の少数に置き換える
  const rgbArray = [color.slice(0, 2), color.slice(2, 4), color.slice(4, 6)].map(function (str) {
    return parseInt(str, 16) / 255;
  });
  paint.color.r = rgbArray[0];
  paint.color.g = rgbArray[1];
  paint.color.b = rgbArray[2];
  return paint;
}

function clone(val) {
  return JSON.parse(JSON.stringify(val))
}
```

#解説
##htmlとts間の通信方法
雛形を作成した時点で分かりやすいサンプルコードが記述されているので理解しやすかったです。
サンプルコードの解説は下記記事が参考になります！
[Figmaプラグイン開発時の各ファイルの役割を把握しよう](https://qiita.com/tatsuyafukui/items/6f5b47cf228d2d14aef8)

##色変更処理の発火タイミングはmouseoverとonchangeの2つ
小さなUIをクリックするのは地味に集中が必要な作業で、何回も繰り返す作業には不向きです。
そこでhtml側でmouseover時にtsと通信処理を呼び出すchangeClolorを発火するようにしました。

```javascript
// hoverを検知するp要素群を取得する
const hoverAreas = document.getElementsByClassName("hover-area"); 
for (let i = 0; i < hoverAreas.length; i++) {
  // mouseover時の処理を仕込む
  hoverAreas[i].addEventListener("mouseover", () => {
    // radioボタンを選択状態にする
    hoverAreas[i].getElementsByTagName("input")[0].checked = true;
    // tsに通信する処理を実行する
    changeClolor();
  });
}
```

これはinput入力後も同じでわざわざクリックするのが手間だったので、Enter押下時にchangeClolorが実行されるようにしました。

```javascript
document.getElementById('other-color-text-field').onchange = () => {
  // 文字色を変える処理
  document.getElementById('other-color-label').style.color = document.getElementById("other-color-text-field").value;
  // tsに通信する処理を実行する
  changeColor();
  }
```

##windowのサイズはshowUIメソッドで指定する
今回のプラグインはFigma操作時の効率を考えて、Xで閉じない限り表示し続けるようにしました。
そうなるとwindowは小さければ小さいほど作業スペースが確保されるので良いです。
windowのサイズはshowUIメソッドでwidthとheightをそれぞれ指定することができました。
▽Figma - showUI
https://www.figma.com/plugin-docs/api/properties/figma-showui/

#おわりに
一旦これで実装したかった機能を実装することができました。
ローカルのコードなので、好きな値に書き換えながら使えるのは便利ですね。