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

#はじめに
普段教材の画像制作をしています。教材の画像において注釈はとても重要です。注釈はその画像の1番伝えたい要件であることが多く、その分修正を頻繁に行います。

その作業で面倒なのが色の変更です。
線、塗り、半透明の塗りと、それぞれ的確に選択して色を設定し直す必要があります。そうしないと色をつけたくないところに色がついてしまうからです。

例えば下図では5つの要素からなる注釈を、線、塗り、半透明の塗りの3回に分けて色を変える必要があります。
<img width="682" alt="スクリーンショット 2019-12-13 1.09.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44990/818d8f3b-b272-ce9c-9589-cfe5e7510b03.png">
これが何セットもあったり、カラーパターンを出したりする時の面倒臭さと言ったら…。

そこで、選択したグループを一括で色置換するプラグインを作っていきます！
前編では特定の色に一括置換するところまでを作ります。

#コード
```ts
// 後編でこの色を入力できるようにしたい
const inputHex = "49C0EB";

figma.currentPage.selection.forEach((item) => {
  setColorRecursively(item);
});

function setColorRecursively(item: SceneNode) {
  // 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);
    const cloneStrokes = clone(item.strokes);
    item.strokes = changePaints(cloneStrokes);
  }
  // 子を持ちうるなら再帰処理
  if (item.type === "INSTANCE" || item.type === "GROUP" || item.type === "FRAME" || item.type === "BOOLEAN_OPERATION") {
    item.children.forEach((child) => {
      setColorRecursively(child);
    });
  }
}

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

function changePaint(paint) { 
  // 単色塗り以外（グラデーション、画像）は対応しない
  if (paint.type !== "SOLID") { 
    return paint;
  }
  // 16進数を10進数に変換した後、0~1の少数に置き換える
  const rgbArray = [inputHex.slice(0, 2), inputHex.slice(2, 4), inputHex.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))
}

figma.closePlugin();
```
これを実行すると線も塗りも一発で色を変更できます。
▽実行前
<img width="451" alt="スクリーンショット 2019-12-13 1.15.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44990/6f82a9e4-5357-3447-c616-6bc58ad9cc1a.png">
▽実行後
<img width="455" alt="スクリーンショット 2019-12-13 1.14.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44990/f702e081-c1d2-5758-0d37-ff4492de76dd.png">


#解説
##色を持ちうるレイヤーを対象にする
色の情報を持つfills（塗り）とstrokes（線）propertyのinterfaceであるGeometryMixinを実装しているtypeを条件にして色置換処理を行いました。（2019/12/12 現在）

```ts

  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") { 
```
色は主要機能なだけあって該当するtypeが多いですね...。

##propertyの内部は直接変更できない
propertyの内部は直接変更することができません。

```ts
// propertyのfillsの内部を直接変えてみる
item.fills[0].color.r = 10;
// TypeError: Cannot assign to read only property 'r' of object '#<Object>'
```

そこで一度、propertyを複製します。複製したものであればpropertyの内部を変更することができます。変更を行った後にpropertyごと差し替えます。

```ts
// javascriptの配列にする
const cloneFills = clone(item.fills);
// property毎差し替える
item.fills = changePaints(cloneFills);
```

詳しくは下記で紹介されています。
▽Figma - Editing Properties
https://www.figma.com/plugin-docs/editing-properties/
clone関数はここで紹介されているものを使わせてもらいました。

##色の指定方法を合わせる
fimgaの色指定方法のデフォルトは「000000 ~ FFFFFF」の16進数で設定する方法です。
<img width="244" alt="スクリーンショット 2019-12-12 8.40.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44990/771d903c-65a5-5662-8cf8-68ad6b4a1c13.png">

一方、properyの色の指定方法は、RGBそれぞれに「0 ~ 1」の小数となっています。

```ts
interface RGB {
  readonly r: number
  readonly g: number
  readonly b: number
}
```

これを変換するために下記を実装しました。

```ts

  // 16進数を10進数に変換した後、0~1の少数に置き換える
  const rgbArray = [inputHex.slice(0, 2), inputHex.slice(2, 4), inputHex.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];
```

▽MDN - parseInt
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt
javascriptで基数の変換は初めて実装したのですが、便利な関数が用意されていたので助かりました！

#おわりに
まだ色の指定がコードに入っている状態ですが、実際に業務で使ってみてストレスフリーになりました！汎用性を高くすれば公開もできるかも...？