インターンの修了証を自動でバリエーションを出して生成する方法 with Figma Plugin

## この記事の趣旨

これはFigma内のオブジェクトをプログラムによって生成、ランダムなタイルの作成などをプラグインで簡単に行えることの紹介記事です。

## あらすじ

YUMEMI Design internship 2022 summerにて、2週間のプログラムを完了した人に修了証を配るという企画が始動。そしてその修了証はNFTとして配布し、人によってバリエーションがあるものに、ということで、デザイナーさんがこんな感じのデザインを作ってくれました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/c483c326-93ac-3617-3e1d-28962c715886.png)

バリエーションというのは、この上部のタイルの部分で、ある程度決まったルールを基にランダムに生成できるようにしたいとの事で、これを自動でランダムに生成するFigmaPluginを作っていきます。

## アプローチ
### FigmaのオブジェクトはJSONで取得できる
Figmaのすべてのオブジェクトは、JSONによる階層構造で表現され、API経由で取得することができます。今回はタイルが13種類存在しますが、それぞれをのタイルをデータとして取得し、それをAutoLayoutの横列・その集合の縦列と順に格納することでグラフィックを生成していきます。

## 実装

### 下準備

FigmaのPluginはJavascript等を利用してとても簡単に実装できます。

Figmaの右上、自分のアイコンからPluginsのメニューを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/485d09f9-94c1-fd7c-0fb4-c96d988cb1b2.png)
下の方に`InDevelopment`のコーナーがあるので、右上の＋ボタンから、NewPluginを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/254b0d9a-f3c1-61e9-20a0-15887df62c7e.png)

プロンプトに従って進めます。
今回はFigmaのみで利用できればOKなのでFigmaを選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/88e085c0-9927-eda6-e824-86fa85281cf7.png)

次に、テンプレートの形式ですが
- 全くの空の状態から作る
- 実行した瞬間にFigma上に何かが起こるタイプを作る
- 起動するとUIが出てそこで何かを行うタイプを作る

の3つが選べます。
今回は名前や生成枚数をUIで設定できるようにしたかったため、3つ目を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/05effbc0-c3ac-c196-65de-c0ccbe55423a.png)

そこまで選択すると、FinderないしExplorerのプロンプトで保存先を選択することができるので保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/4d36e34a-4f7c-c3ea-d156-8b9d4412abd3.png)

小さな構成のPluginが生成されました。生成された内容一つ一つはこの記事では触れませんが、いろんなところで解説されているのでそちらをご覧ください。

今回大きく関係してくるのは、
- code.ts
    - メインのロジックを記述
- manifest.json
    - 設定をいろいろ記述
- ui.html
    - UIとして表示されるHTMLを記述

の3つです
ファイルを増やして読み込むことや、VueやReactをバンドルして使うなんてことも出来ますが、今回はそんな大したものではないので割愛です。

### UIの構築

FigmaのPlugin（UIあり）は、さながらElectronのように、UI側からのメッセージを受け取り、code側で処理をするというような仕組みで動いています。

UI側ではHTMLとCSSによるUI構築と、機能を発火させるための軽いJavascriptの記述が必要になります。

以下にui.htmlを全部はっ付けますが、CSSなども含まれているため詳しくは下で解説します。
```html
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<style>
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  font-family: 'Manrope', 'Noto Sans JP', sans-serif;
}
p {
  margin: 0;
  padding: 0;
}
.main {
  width: 500px;
  height: 500px;
  background: #fffffe;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: auto;
}
.title-wr {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 2rem;
}
.contents {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.title {
  color: #272343;
  font-size: 32px;
  font-weight: 700;
  margin-bottom: 12px;
}
.cap {
  color: #2d334a;
  font-size: 20px;
  font-weight: 300;
}
.note {
  color: #2d334a;
  margin-bottom: 1rem;
}
.input {
  height: 44px;
  width: 280px;
  border: 2px solid #272343;
  border-radius: 8px;
  font-size: 20px;
  margin-bottom: 1rem;
  padding:  0 1rem ;
}
.bu {
  margin-top: 1rem;
  font-size: 20px;
  height: 44px;
  width: 280px;
  border-radius: 8px;
  background: #ffd803;
  color: #272343;
  font-weight: 700;
  display: flex;
  justify-content: center;
  align-items: center;
  outline: none;
  border: 0px;
  margin-bottom: 1rem;
}
.bu2 {
  font-size: 20px;
  height: 44px;
  width: 280px;
  border-radius: 8px;
  background: #e3f6f5;
  color: #272343;
  font-weight: 700;
  display: flex;
  justify-content: center;
  align-items: center;
  outline: none;
  border: 0px;
}
.nameplate {
  padding: 8px 12px;
  background-color: #bae8e8;
  font-weight: 700;
  min-width: 280;
  border-radius: 8px;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 0.5rem;
}
</style>
<div id="app" class="main">
  <div class="title-wr">
    <h1 class="title">GENERATE CERTIFICATE</h1>
    <p class="cap">YDI2022 Summer</p>
  </div>
  <div class="contents" v-if="mode === 'init'">
    <p class="note">生成する枚数を入力してください</p>
    <input v-model="amount" type="number" class="input">
    <button @click="nameMode" class="bu">次へ</button>
  </div>
  <div class="contents" v-if="mode === 'name'">
    <p class="note">人数分の名前を入力してください</p>
    <input v-for="(name, i) in names" v-model="names[i]" type="text" class="input">
    <button @click="confMode" class="bu">次へ</button>
  </div>
  <div class="contents" v-if="mode === 'conf'">
    <p class="note">以下の内容で生成します。よろしいですか？</p>
    <p class="nameplate" v-for="name in names">{{ name }}</p>
    <button @click="gen" class="bu">生成する</button>
    <button @click="topMode" class="bu2">最初からやり直す</button>
  </div>
</div>
<script>
const instance = new Vue({
  el: '#app',
  data() {
    return {
      mode: 'init',
      amount: 0,
      names: []
    } 
  },
  methods: {
    nameMode() {
      if(this.amount < 1) {
        alert('1以上で入力して下さい')
      } else if(this.amount > 99) {
        alert('重いので100以下で入力してください')
      } else {
        for(let i = 0; i<this.amount;i++ ) {
          this.names.push('名前を入力')
        }
        this.mode = 'name'
      }
    },
    confMode() {
      this.mode = 'conf'
    },
    topMode() {
      this.names = [];
      this.amount = 0;
      this.mode = 'init'
    },
    gen() {
      const config = {
        amount: this.amount,
        names: this.names
      }
      parent.postMessage({ pluginMessage: { type: 'generate', config } }, '*')
      this.topMode();
    }
  }
})
</script>

```

まず、jQueryやバニラJSは苦手なので、CDNでVueを持ってきます。複雑なものは作らないのでバニラで書ける人は不要です。

```html
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
```

CSSやフォントのロードをしていますがすっ飛ばします。

v-ifを使って現在の表示を出し分け、疑似的な複数ページを実現しています。
入力された枚数分の名前入力欄を生成し、そのデータをVueに保持します。
```html
<div id="app" class="main">
  <div class="title-wr">
    <h1 class="title">GENERATE CERTIFICATE</h1>
    <p class="cap">YDI2022 Summer</p>
  </div>
  <div class="contents" v-if="mode === 'init'">
    <p class="note">生成する枚数を入力してください</p>
    <input v-model="amount" type="number" class="input">
    <button @click="nameMode" class="bu">次へ</button>
  </div>
  <div class="contents" v-if="mode === 'name'">
    <p class="note">人数分の名前を入力してください</p>
    <input v-for="(name, i) in names" v-model="names[i]" type="text" class="input">
    <button @click="confMode" class="bu">次へ</button>
  </div>
  <div class="contents" v-if="mode === 'conf'">
    <p class="note">以下の内容で生成します。よろしいですか？</p>
    <p class="nameplate" v-for="name in names">{{ name }}</p>
    <button @click="gen" class="bu">生成する</button>
    <button @click="topMode" class="bu2">最初からやり直す</button>
  </div>
</div>
```

ページ切り替え機能はしょうもないので割愛しますが、最後に生成ボタンが押されたタイミング、プロセス側にメッセージを飛ばすタイミングはこのようにします。

メッセージとして名前、付随情報を送ることができます。
今回は`generate`というコマンドが発火されて引数はこんな感じです！みたいな渡し方です。

```javascript
gen() {
      const config = {
        amount: this.amount,
        names: this.names
      }
      parent.postMessage({ pluginMessage: { type: 'generate', config } }, '*')
      this.topMode();
    }
```

### メインのロジック

メインのロジックでは、UI側からのメッセージを受け取り、タイルのコードを生成し、それをFigmaのCanvas上に描画します。

#### Figmaオブジェクトのコード化

さて、Figmaにオブジェクトを色々と生成したいのですが、正直1からコードで全部定義するのは大変です。
そこで、既にFigmaに存在しているオブジェクトをコードとして出力し、それを使いまわす方針で行きます。

Node Decoderというプラグインを利用します。
https://www.figma.com/community/plugin/933372797518031971

Codeにしたいオブジェクトを選択してプラグインを起動するだけで、コードの状態でオブジェクトを表示してくれます。

例としてこれは、Textが2つ格納されたAutoLayoutフレームをでコードしたものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/b568a52d-bf03-13f1-5d4c-fcd4acd4eb91.png)

フォントやAutoLayoutの設定、色、そのほか全てが記述されたCodeが出てきます。

この例をよく読んでみるとわかるのですが、Figmaのオブジェクトは親に対して子をappendしており、その連続で階層構造が表現されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/1a641796-4699-5ea7-33f9-e493490126f4.png)
生成されたコードは、生成というよりもFigmaの内部で扱われている生データをそのまま外に出力したイメージなので、かなり網羅的です。

このように、使うパーツパーツを出力して、一つのコードとして保持しておきます。

タイルのパーツも例外ではなく、13種類のタイルをそれぞれ出力し、変数に格納しておき、そこから必要に応じて複製をしてFigmaのフレームにappendをしていきます。

#### UIからのメッセージを受け取る

code.tsの上部では、UIの呼び出しとサイズ変更、UIからのメッセージを受け取る部分を定義しています。
```typescript
figma.showUI(__html__);
figma.ui.resize(500, 500);
figma.ui.onmessage = (msg) => {
  if (msg.type === "generate") {
    console.log(msg);
    startGenerate(msg.config.amount, msg.config.names);
  }
  // figma.closePlugin();
};
```

`generate`コマンドが送信されたら、`startGenerate`関数をconfigの引数付きで発火させています。

#### 生成枚数の制御
わざわざ乗せるほどのコードでもないですが、一応。ここから修了証を生成していきます。
```typescript
const startGenerate = async (amount: number, names: string[]) => {
  console.log(amount, names);
  for (let i = 0; i < amount; i++) {
    console.log(names[i]);
    generateCertificate(names[i], i);
  }
};
```

#### 修了証の生成
ここではタイル以外の修了証の部分を生成しています。というのも、タイルは13オブジェクトのNodeを所持しているのでとてもコードが多く、関数として分離したかったためです。

```typescript
const generateCertificate = async (studentName: string, index: number) => {
  // 前提タイルを生成
  const baseTiles = await generateBaseTiles();
  const tileArray = generateTileArray(baseTiles);
  // 大元のフレームを作る
  const certification = figma.createFrame();
  certification.name = studentName;
  certification.primaryAxisSizingMode = "AUTO";
  certification.counterAxisSizingMode = "AUTO";
  certification.layoutMode = "VERTICAL";
  certification.x = index * 1500;
  // グラフィック全体のフレームを作る
  const graphics = figma.createFrame();
  graphics.primaryAxisSizingMode = "AUTO";
  graphics.counterAxisSizingMode = "AUTO";
  graphics.layoutMode = "VERTICAL";

  // 繰り返し（行)
  for (let q = 0; q < 11; q++) {
    // 横1列のフレームを作る
    const row = figma.createFrame();
    row.primaryAxisSizingMode = "AUTO";
    row.counterAxisSizingMode = "AUTO";
    row.layoutMode = "HORIZONTAL";
    //繰り返し(列)
    for (let p = 0; p < 14; p++) {
      const tileNode = tileArray.pop();
      row.appendChild(tileNode!);
    }
    graphics.appendChild(row);
  }
  certification.appendChild(graphics);

  //ほかのコンポーネント
  //font
  async function loadFonts() {
    await Promise.all([
      figma.loadFontAsync({
        family: "Yumemi Gothic beta",
        style: "Regular",
      }),
      figma.loadFontAsync({
        family: "Zen Kurenaido",
        style: "Regular",
      }),
    ]);
  }

  await loadFonts();
  const informationWr = figma.createFrame();
  informationWr.resize(1400, 300);
  informationWr.fills = [
    {
      type: "SOLID",
      color: { r: 1, g: 1, b: 1 },
    },
  ];
  informationWr.primaryAxisAlignItems = "CENTER";
  informationWr.counterAxisAlignItems = "CENTER";
  informationWr.primaryAxisSizingMode = "FIXED";
  informationWr.layoutMode = "VERTICAL";
  const information = figma.createFrame();
  information.resize(1300, 1);
  information.counterAxisSizingMode = "AUTO";
  information.primaryAxisAlignItems = "SPACE_BETWEEN";
  information.counterAxisAlignItems = "MAX";
  information.layoutMode = "HORIZONTAL";
  information.primaryAxisSizingMode = "FIXED";
  information.counterAxisSizingMode = "AUTO";
  const informationDetail = figma.createFrame();
  informationDetail.primaryAxisSizingMode = "AUTO";
  informationDetail.counterAxisSizingMode = "AUTO";
  informationDetail.layoutMode = "VERTICAL";
  informationDetail.counterAxisSizingMode = "AUTO";
  informationDetail.itemSpacing = 6;
  const titleGroup = figma.createFrame();
  titleGroup.primaryAxisSizingMode = "AUTO";
  titleGroup.counterAxisSizingMode = "AUTO";
  titleGroup.layoutMode = "VERTICAL";
  titleGroup.counterAxisSizingMode = "AUTO";
  titleGroup.itemSpacing = 12;
  const welcome = figma.createFrame();
  welcome.resize(438.0, 47.3045196533);
  welcome.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: { r: 0, g: 0.5960784554481506, b: 0.21960784494876862 },
    },
  ];
  welcome.strokes = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: { r: 0.9529411792755127, g: 0.7647058963775635, b: 0 },
    },
  ];
  welcome.paddingLeft = 44;
  welcome.paddingRight = 44;
  welcome.paddingTop = 8;
  welcome.paddingBottom = 8;
  welcome.primaryAxisAlignItems = "CENTER";
  welcome.primaryAxisSizingMode = "FIXED";
  welcome.strokeTopWeight = 0;
  welcome.strokeBottomWeight = 0;
  welcome.strokeLeftWeight = 19;
  welcome.strokeRightWeight = 0;
  welcome.backgrounds = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: { r: 0, g: 0.5960784554481506, b: 0.21960784494876862 },
    },
  ];
  welcome.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  welcome.layoutMode = "VERTICAL";
  welcome.itemSpacing = 10;
  // Create TEXT
  const welcomeText = figma.createText();
  welcomeText.name = "Welcome!";
  welcomeText.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: { r: 1, g: 1, b: 1 },
    },
  ];
  welcomeText.relativeTransform = [
    [1, 0, 44],
    [0, 1, -0.8477401733],
  ];
  welcomeText.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  welcomeText.fontName = {
    family: "Yumemi Gothic beta",
    style: "Regular",
  };
  welcomeText.characters = "Congratulations!";
  welcomeText.fontSize = 41;
  welcomeText.fontName = { family: "Yumemi Gothic beta", style: "Regular" };
  welcomeText.textAutoResize = "WIDTH_AND_HEIGHT";
  // Create TEXT
  const title = figma.createText();
  title.name = "Summer Design Internship 2022.";
  title.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.12156862765550613,
        g: 0.12941177189350128,
        b: 0.12156862765550613,
      },
    },
  ];
  title.relativeTransform = [
    [1, 0, 0],
    [0, 1, 59.3045196533],
  ];
  title.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  title.fontName = {
    family: "Yumemi Gothic beta",
    style: "Regular",
  };
  title.characters = "Summer Design Internship 2022.";
  title.fontSize = 57;
  title.fontName = { family: "Yumemi Gothic beta", style: "Regular" };
  title.textAutoResize = "WIDTH_AND_HEIGHT";
  const nameGroup = figma.createFrame();
  nameGroup.primaryAxisSizingMode = "AUTO";
  nameGroup.counterAxisSizingMode = "AUTO";
  nameGroup.relativeTransform = [
    [1, 0, 0],
    [0, 1, 133.3045196533],
  ];
  nameGroup.counterAxisAlignItems = "MAX";
  nameGroup.layoutMode = "HORIZONTAL";
  nameGroup.counterAxisSizingMode = "AUTO";
  nameGroup.itemSpacing = 20;
  const nameTitle = figma.createText();
  nameTitle.name = "name.";
  nameTitle.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.12156862765550613,
        g: 0.12941177189350128,
        b: 0.12156862765550613,
      },
    },
  ];
  nameTitle.relativeTransform = [
    [1, 0, 0],
    [0, 1, 21],
  ];
  nameTitle.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  nameTitle.fontName = {
    family: "Yumemi Gothic beta",
    style: "Regular",
  };
  nameTitle.characters = "name.";
  nameTitle.fontSize = 41;
  nameTitle.fontName = { family: "Yumemi Gothic beta", style: "Regular" };
  nameTitle.textAutoResize = "WIDTH_AND_HEIGHT";
  const name = figma.createText();
  name.name = "りりーのんのん";
  name.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.12156862765550613,
        g: 0.12941177189350128,
        b: 0.12156862765550613,
      },
    },
  ];
  name.relativeTransform = [
    [1, 0, 153],
    [0, 1, 0],
  ];
  name.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  name.fontName = {
    family: "Zen Kurenaido",
    style: "Regular",
  };
  name.characters = studentName;
  name.fontSize = 48;
  name.fontName = { family: "Zen Kurenaido", style: "Regular" };
  name.textAutoResize = "WIDTH_AND_HEIGHT";
  const logoVector = figma.createVector();
  logoVector.resize(174.9774932861, 113.8938217163);
  logoVector.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.12156862765550613,
        g: 0.12941177189350128,
        b: 0.12156862765550613,
      },
    },
  ];
  logoVector.strokes = [];
  logoVector.strokeAlign = "INSIDE";
  logoVector.relativeTransform = [
    [1, 0, 1125.0224609375],
    [0, 1, 89.410697937],
  ];
  logoVector.x = 1125.0224609375;
  logoVector.y = 89.41069793701172;
  logoVector.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  logoVector.vectorNetwork = あまりにも長いので割愛（自動生成された物）
  logoVector.vectorPaths = あまりにも長いので割愛（自動生成された物）

  //情報欄を構成して全体に追加
  welcome.appendChild(welcomeText);
  titleGroup.appendChild(welcome);
  titleGroup.appendChild(title);

  informationDetail.appendChild(titleGroup);

  nameGroup.appendChild(nameTitle);
  nameGroup.appendChild(name);

  informationDetail.appendChild(nameGroup);

  information.appendChild(informationDetail);
  information.appendChild(logoVector);

  informationWr.appendChild(information);
  certification.appendChild(informationWr);

  //生成したベースタイルを除去c
  console.log(tileArray);
  console.log(baseTiles);
  baseTiles.forEach((tile, x) => {
    try {
      tile.remove();
    } catch (error) {
      console.error(error);
    }
  });
  console.log(baseTiles);

  //フォーカス!!!
  figma.viewport.scrollAndZoomIntoView([certification]);
};
```
概ねコメントに書いてある通りですが、大まかな流れとしては
1. フレームを生成
1. フレームに対してAutoLayoutなどの設定
1. TextやVectorなどの子要素を生成 ( or 生成したデータを持ってくる）
1. フレームに対してAppend
1. それぞれのフレームを親のフレームにどんどんAppend

といった感じです。

##### AutoLayoutについて

FigmaでAutoLayoutを使い慣れていると、少しコード上では概念が違くなってきているので注意が必要です。（おそらく人に分かりやすいようにUIでラップアップしている）

```typescript
const information = figma.createFrame();
  information.resize(1300, 1);
  information.counterAxisSizingMode = "AUTO";
  information.primaryAxisAlignItems = "SPACE_BETWEEN";
  information.counterAxisAlignItems = "MAX";
  information.layoutMode = "HORIZONTAL";
  information.primaryAxisSizingMode = "FIXED";
  information.counterAxisSizingMode = "AUTO";
```
これが一つのフレームに対する設定ですが、縦横それぞれ大きさの設定と、全体のレイアウトモードがあり、そこの値によってタテのAutoLayout,　ヨコのAutoLayount, そもそもAutoLayoutでないなどが変更できます。縦横それぞれの大きさで、FillContainer,HugContents,fixedなどのおなじみの設定が出来ます。恐らく人の手でいじるとFigmaでは表現できないデータになってしまうこともあると思うので、変なのにならないように注意して生成してください。（そうだったとしてもエラー落ちとかはしませんが...)

##### フォントについて

利用するフォントは適切にロードしてあげないとプラグインがエラーになってしまいます。
使うタイミングより前にロードしておきましょう。

```typescript
async function loadFonts() {
    await Promise.all([
      figma.loadFontAsync({
        family: "Yumemi Gothic beta",
        style: "Regular",
      }),
      figma.loadFontAsync({
        family: "Zen Kurenaido",
        style: "Regular",
      }),
    ]);
  }

  await loadFonts();
```


#### タイルの生成

タイルは、様々なルールで生成の制限がされています。
- 赤いピエロは1つだけ存在する
- 隣り合うタイルは同じではならない
- このタイルは大体こんぐらいの割合で

これを実現するために、大量のベースタイルの中からランダムでタイルを並べていくというアプローチで実装します。
ここから先の話は記事の趣旨にそこまでマッチしていないので興味がある方だけご覧ください。

`generateTileArray`でタイルをフレーム用の2次元配列に格納
`generateUniqueTile`で被りのないタイルを生成しています。

これらは、事前に生成してあったBaseTilesの中からランダムで抽出されています。

```typescript
const generateTileArray = (baseTile: FrameNode[]) => {
  const tileArray = [];
  for (let i = 0; i < 11 * 14; i++) {
    if (i === 0) {
      tileArray.push(baseTile.shift());
    } else {
      const currentTile = [];
      currentTile.push(tileArray[i - 1]);
      if (i > 14) {
        currentTile.push(tileArray[i - 14]);
      }
      const tile = generateUniqueTile(baseTile, currentTile) as FrameNode;
      tileArray.push(tile);
    }
  }
  const temp = tileArray.shift();
  const r = Math.floor(Math.random() * tileArray.length);
  const rNode = tileArray[r];
  tileArray[r] = temp;
  tileArray.push(rNode);
  return tileArray;
};

const generateUniqueTile: any = (
  baseTile: FrameNode[],
  currentTile: FrameNode[]
) => {
  const rand = Math.floor(Math.random() * baseTile.length);
  const tile = baseTile[rand];

  const conflict = currentTile.some(
    (currentTile) => tile.name === currentTile.name
  );
  if (conflict) {
    return generateUniqueTile(baseTile, currentTile);
  } else {
    tile.rotation = Math.floor(Math.random() * 4) * 90;
    if (rand === 3 || rand === 4 || rand === 5 || rand === 6) {
      tile.rotation = 0;
    }
    return tile.clone();
  }
};
```

#### ベースタイルの生成

まず地獄のスクショです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/1be7622e-9a15-d4c7-b23a-00ab5e0fbcff.png)
お判りいただけましたでしょうか。そうです。18000行の関数です。

これはなぜかというと、この関数の中で各タイルのCodeを記述し、Nodeとして扱える状態にする→それらを出現確立に基づいて複製してランダム取り出しようの配列に格納するということをしており、でコードされたオブジェクトは一個一個が千行近くあり、それが何種類も存在しているためです。（シンプルな四角とかだと少ないが、Vectorなどだとパスが全部記述されているため）

全部貼ると大変なことになるんので一個の例だけ貼ります（少ないモノ）

```typescript
// Create FRAME
  const moonY = figma.createFrame();
  figma.currentPage.appendChild(moonY);
  moonY.name = "moonY";
  moonY.relativeTransform = [
    [1, 0, 2529],
    [0, 1, 921.5],
  ];
  moonY.x = 2529;
  moonY.y = 921.5;
  moonY.fills = [];
  moonY.strokeTopWeight = 1;
  moonY.strokeBottomWeight = 1;
  moonY.strokeLeftWeight = 1;
  moonY.strokeRightWeight = 1;
  moonY.backgrounds = [];
  moonY.clipsContent = false;
  moonY.expanded = false;

  // Create VECTOR
  const vector_756_3649 = figma.createVector();
  moonY.appendChild(vector_756_3649);
  vector_756_3649.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.062745101749897,
        g: 0.45098039507865906,
        b: 0.6627451181411743,
      },
    },
  ];
  vector_756_3649.strokes = [];
  vector_756_3649.strokeAlign = "INSIDE";
  vector_756_3649.relativeTransform = [
    [0, 1, 0],
    [-1, 0, 100],
  ];
  vector_756_3649.y = 100;
  vector_756_3649.rotation = 90;
  vector_756_3649.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  vector_756_3649.vectorNetwork = {
    regions: [],
    segments: [
      {
        start: 0,
        end: 1,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
      {
        start: 1,
        end: 2,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
      {
        start: 2,
        end: 3,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
      {
        start: 3,
        end: 0,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
    ],
    vertices: [
      {
        x: 0,
        y: 0,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 100,
        y: 0,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 100,
        y: 100,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 0,
        y: 100,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
    ],
  };
  vector_756_3649.vectorPaths = [
    { windingRule: "NONE", data: "M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z" },
  ];

  // Create VECTOR
  const vector_756_3650 = figma.createVector();
  moonY.appendChild(vector_756_3650);
  vector_756_3650.resize(68.7399902344, 68.7399902344);
  vector_756_3650.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: {
        r: 0.9490196108818054,
        g: 0.7843137383460999,
        b: 0.24313725531101227,
      },
    },
  ];
  vector_756_3650.strokes = [];
  vector_756_3650.strokeAlign = "INSIDE";
  vector_756_3650.relativeTransform = [
    [1, 0, 15.6298828125],
    [0, 1, 15.6300048828],
  ];
  vector_756_3650.x = 15.6298828125;
  vector_756_3650.y = 15.6300048828125;
  vector_756_3650.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  vector_756_3650.vectorNetwork = {
    regions: [],
    segments: [
      {
        start: 0,
        end: 1,
        tangentStart: { x: 0, y: 18.982027053833008 },
        tangentEnd: { x: 18.982027053833008, y: 0 },
      },
      {
        start: 1,
        end: 2,
        tangentStart: { x: -18.982027053833008, y: 0 },
        tangentEnd: { x: 0, y: 18.982027053833008 },
      },
      {
        start: 2,
        end: 3,
        tangentStart: { x: 0, y: -18.982027053833008 },
        tangentEnd: { x: -18.982027053833008, y: 0 },
      },
      {
        start: 3,
        end: 0,
        tangentStart: { x: 18.982027053833008, y: 0 },
        tangentEnd: { x: 0, y: -18.982027053833008 },
      },
    ],
    vertices: [
      {
        x: 68.739990234375,
        y: 34.3699951171875,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 34.3699951171875,
        y: 68.739990234375,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 0,
        y: 34.3699951171875,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 34.3699951171875,
        y: 0,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
    ],
  };
  vector_756_3650.vectorPaths = [
    {
      windingRule: "NONE",
      data: "M 68.739990234375 34.3699951171875 C 68.739990234375 53.35202217102051 53.35202217102051 68.739990234375 34.3699951171875 68.739990234375 C 15.387968063354492 68.739990234375 0 53.35202217102051 0 34.3699951171875 C 0 15.387968063354492 15.387968063354492 0 34.3699951171875 0 C 53.35202217102051 0 68.739990234375 15.387968063354492 68.739990234375 34.3699951171875 Z",
    },
  ];

  // Create VECTOR
  const vector_756_3651 = figma.createVector();
  moonY.appendChild(vector_756_3651);
  vector_756_3651.resize(34.3800048828, 68.75);
  vector_756_3651.fills = [
    {
      type: "SOLID",
      visible: true,
      opacity: 1,
      blendMode: "NORMAL",
      color: { r: 1, g: 1, b: 1 },
    },
  ];
  vector_756_3651.strokes = [];
  vector_756_3651.strokeAlign = "INSIDE";
  vector_756_3651.relativeTransform = [
    [1, 0, 49.9899902344],
    [0, 1, 15.6199951172],
  ];
  vector_756_3651.x = 49.989990234375;
  vector_756_3651.y = 15.6199951171875;
  vector_756_3651.constraints = { horizontal: "SCALE", vertical: "SCALE" };
  vector_756_3651.vectorNetwork = {
    regions: [
      {
        windingRule: "NONZERO",
        loops: [[0, 1, 2, 3]],
        fills: [
          {
            type: "SOLID",
            visible: true,
            opacity: 1,
            blendMode: "NORMAL",
            color: { r: 1, g: 1, b: 1 },
          },
        ],
        fillStyleId: "",
      },
    ],
    segments: [
      {
        start: 0,
        end: 1,
        tangentStart: { x: 18.979999542236328, y: 0 },
        tangentEnd: { x: 0, y: -18.989999771118164 },
      },
      {
        start: 1,
        end: 2,
        tangentStart: { x: 0, y: 18.979999542236328 },
        tangentEnd: { x: 18.989999771118164, y: 0 },
      },
      {
        start: 2,
        end: 3,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
      {
        start: 3,
        end: 0,
        tangentStart: { x: 0, y: 0 },
        tangentEnd: { x: 0, y: 0 },
      },
    ],
    vertices: [
      {
        x: 0.010009765625,
        y: 0,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 34.3800048828125,
        y: 34.3800048828125,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 0,
        y: 68.75,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
      {
        x: 0,
        y: 0,
        strokeCap: "NONE",
        strokeJoin: "MITER",
        cornerRadius: 0,
        handleMirroring: "NONE",
      },
    ],
  };
  vector_756_3651.vectorPaths = [
    {
      windingRule: "NONZERO",
      data: "M 0.010009765625 0 C 18.990009307861328 0 34.3800048828125 15.390005111694336 34.3800048828125 34.3800048828125 C 34.3800048828125 53.36000442504883 18.989999771118164 68.75 0 68.75 L 0 0 L 0.010009765625 0 Z",
    },
  ];
```

これも最初のと理論は同じで、フレームを作ってVectorを作ってそれぞれをAppendしていくというように構成されています。

### まとめ

最後の方だいぶ脱線してしまいましたが、このようにFigmaのオブジェクトを操作、複製して新たに生成することで、いろいろなモノを生成することができます。今回は修了証でしたが、サムネイルであったり、チラシであったり、可能性は無限大です。

Figmaはブラウザベースで開発されていることもアリ、フロントエンドの技術が分かれば直ぐにカスタムすることができます。

こういう機能欲しい、が大体は自分で作っちゃおうで解決することができる素晴らしいツールなのでみなさん是非Figmaライフを楽しみましょう。

ご清聴ありがとうございました

追伸：そういえばこの前全身Figma男になったので自慢します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253700/3df16a7b-56f2-7e32-a698-45992aa0426c.png)