🐣

Create Figma Plugin を使って Figma Plugin 開発に入門してみるぞ

島袋恵2022/10/19に公開

はじめに

  • この記事は Figma Plugin に入門してみたくなって、いろいろと試したことをまとめた記事です
  • Figma Plugin に入門したくなったモチベーション
    • デザイナーさんといい感じに仕事できるようにFigmaの知識を持っておきたかった
    • Figma Plugin がどういう仕組みで動いてるのか気になってた
    • Create Figma Plugin が気になってた
  • Create Figma Plugin を使って Figma Plugin を開発してみたい人の参考になれば嬉しいです

Figma Pluginとは

Figmaは、プラグインをインストールして機能拡張することができます。詳細は、公式ドキュメントにて

https://www.figma.com/plugin-docs/

世の中には便利プラグインがたくさん公開されてる。以下、最近良いなと思った適当なプラグイン

https://www.figma.com/community/plugin/843461159747178978/Figma-Tokens

https://www.figma.com/community/plugin/1159123024924461424/html.to.design

https://www.figma.com/community/plugin/1160642826057169962/Figma-Autoname

また、プラグインは自分で作ることもできます。Figmaのドキュメントにあるセットアップガイド通りに試してみると、サンプルのプラグイン(rectangles生成するプラグイン)を動かしてみることができます。詳細は以下のページにて

https://www.figma.com/plugin-docs/setup/

Create Figma Pluginとは

Create Figma Pluginは、Figma Plugin の開発で利用するテンプレートやUIコンポーネント、ビルド周りの整備などをいい感じに提供してくれてるライブラリです。

https://github.com/yuanqing/create-figma-plugin

https://yuanqing.github.io/create-figma-plugin/

Figmaのドキュメントにあるセットアップガイド(Figmaのアプリからinitする方法)から 初期化した状態は、割とシンプルでプラグイン開発に必要な最低限の状態なので、プラグインを開発するためのテンプレートやUIコンポーネント、ユーティリティなどが充実して欲しい場合は、Create Figma Pluginを使うと便利で良さそうです。

Create Figma Pluginを使ってテンプレートを動かしてみる

Create Figma Pluginのドキュメントの クイックをスタート を参考に動かしてみます。reactを使いたかったので、plugin/react-editor のテンプレートを選択しまた。

$ npx --yes create-figma-plugin
? Select a template: plugin/react-editor
First:
  cd react-editor

To build the plugin:
  npm run build

To watch for code changes and rebuild the plugin automatically:
  npm run watch

$ cd react-editor

テンプレートで一部修正しないといけない点があり、下記のsrc/ui.tsx 二行を削除します。(参考

+// import 'prismjs/components/prism-clike.js'
+// import 'prismjs/components/prism-javascript.js'

ビルドします。

$ npm run build

Figmaのアプリにて、開発中のプラグインを取り込むため、react-editor プロジェクト内にある manifest.json をimportします。

開発用のプラグイン react-editor を実行すると、テンプレートに元々用意されてる機能が使えます。Insert Codeボタンを押すと、コードのテキストがFigma上に挿入されます。(テンプレートのreact-editorの機能)

以上で、Create Figma Pluginのテンプレートからサンプルプラグインを動かしてみるは終わりです。

Create Figma Plugin での処理概要

そもそもプラグインが Figma で動く仕組み

まず、そもそもプラグインが Figma 上でどのように実行されてるかについてです。

ざっくりいうと UI側のコードとメインスレッド側のコードが postMessage を通してやりとりして動く仕組みです。

  • メインスレッド側のコード
    • Figma ドキュメントを構成するレイヤーの階層にアクセスして操作できるコード。UI側のコードからユーザーが入力したイベントを受け取って、Figmaレイヤー側の操作をしたり、Figmaレイヤー側で処理した結果をUI側に返すときのイベントを投げたりする
  • UI側のコード
    • ブラウザ API にアクセスでき Figmaのアプリ上でユーザーからの操作を受け付けたり、情報を表示したりするコード。ユーザーからの入力を受け取って、Figmaレイヤー側で必要な処理について、メインスレッド側にイベント投げたり、メインスレッド側からUIで表示させてほしいイベントを受けとって表示させたりする

詳細は以下のドキュメントに詳しく書いています。

https://www.figma.com/plugin-docs/how-plugins-run/

以下、メインスレッド側のコードとUI側の処理の関係性について、具体的なコードと合わせてまとめて見ました。

プラグインがどのように動作しているのかについては、この記事もわかりやすかったです。

https://zenn.dev/ixkaito/articles/how-to-make-a-figma-plugin#プラグインがどのように動作しているのか

Create Figma Plugin の場合

Create Figma Plugin では、上記の仕組みをいい感じにラップしたユーティリティ関数を提供してたり、generateした際できるファイル構成(テンプレートを使うと main.tsui.tsx などが生成される)もシンプルでわかりやすくなっています。

  • main.ts は、メインとなるエントリーポイントで、Figmaのレイヤーの操作するなどの処理を書きます。

  • ui.tsx は、UI側のコードのエントリーポイントで、ユーザーからの入力、ブラウザからのアクセスなどの処理を書きます。

$ tree ./src -L 1
./src
├── main.ts # sandbox。メインスレッド側のコード。Figmaレイヤーの操作など。
├── styles.css
├── styles.css.d.ts
├── types.ts
└── ui.tsx # iframe。UI側のコード。ユーザーからの入力、ブラウザからのアクセスなど。

上述した postMessageonmessage のやりとりのコードを以下のユーティリティ関数を使って書くことができます。

  • emit<Handler>(name, ...args) でイベント発火

  • on<Handler>(name, handler) でイベント受け取る

詳細はドキュメント

https://yuanqing.github.io/create-figma-plugin/utilities/#events

Create Figma Plugin のテンプレートを改良してText Stylesが適用されていないテキストノードを探す機能を作ってみる

続いて、動かしてみた上記テンプレートをベースに機能を足してみます。定義済のテキストスタイルが適用されてないテキストノードがないかかどうか(野良スタイルが使われてないかどうか)チェックする機能を作ってみます。

以下のコードを変更

src/main.ts

メインスレッド側の処理

import { loadFontsAsync, once, on, emit, showUI } from '@create-figma-plugin/utilities'

import { RunAppHandler, FirstNodeHandler, SelectTextNodeHandler } from './types'

export default function () {
  on<RunAppHandler>('RUN_APP', async function () {
    if (figma.currentPage.selection.length === 0) {
      console.log("No selection");
      figma.notify("Select a frame(s) to get started", { timeout: 2000 });
      return;
    } else {
      let nodes = figma.currentPage.selection;
      let firstNode = [];

      firstNode.push(figma.currentPage.selection[0]);

      const errors = lint(firstNode);

      emit<FirstNodeHandler>('FIRST_NODE', errors);

    }
  })
  on<SelectTextNodeHandler>('SELECT_TEXT_NODE', async function (node: TextNode) {
    figma.currentPage.selection = [node]
    figma.viewport.scrollAndZoomIntoView([node])
  })
  showUI({ height: 400, width: 320 })
}

function lint(nodes: any) {
  let errorArray: any = [];
  let childArray: any = [];

  nodes.forEach((node: any) => {

    // Create a new object.
    let newObject: any = {};

    // Give it the existing node id.
    newObject["id"] = node.id;

    let children = node.children;

    newObject["errors"] = determineType(node);

    if (!children) {
      errorArray.push(newObject);
      return;
    } else if (children) {
      // Recursively run this function to flatten out children and grandchildren nodes
      node["children"].forEach((childNode: any) => {
        childArray.push(childNode.id);
      });

      newObject["children"] = childArray;

      // If the layer is locked, pass the optional parameter to the recursive Lint
      // function to indicate this layer is locked.
      errorArray.push(...lint(node["children"]));
    }

    errorArray.push(newObject);
  });

  return errorArray;
}

function determineType(node: any) {
  if (node.type === "TEXT") {
    return lintTextRules(node);
  }
}


function lintTextRules(node: any) {
  let errors: any[] = [];
  checkType(node, errors);
  return errors;
}

function checkType(node: any, errors: any) {
  if (node.textStyleId === "" && node.visible === true) {
    let textObject = {
      font: "",
      fontStyle: "",
      fontSize: "",
      lineHeight: {}
    };

    let fontStyle = node.fontName;
    let fontSize = node.fontName;

    if (typeof fontSize === "symbol") {
      return errors.push(
        createErrorObject(
          node,
          "text",
          "Missing text style",
          "Mixed sizes or families"
        )
      );
    }

    if (typeof fontStyle === "symbol") {
      return errors.push(
        createErrorObject(
          node,
          "text",
          "Missing text style",
          "Mixed sizes or families"
        )
      );
    }

    textObject.font = node.fontName.family;
    textObject.fontStyle = node.fontName.style;
    textObject.fontSize = node.fontSize;

    // Line height can be "auto" or a pixel value
    if (node.lineHeight.value !== undefined) {
      textObject.lineHeight = node.lineHeight.value;
    } else {
      textObject.lineHeight = "Auto";
    }

    let currentStyle = `${textObject.font} ${textObject.fontStyle} / ${textObject.fontSize} (${textObject.lineHeight} line-height)`;

    return errors.push(
      createErrorObject(node, "text", "Missing text style", currentStyle)
    );
  } else {
    return;
  }
}

function createErrorObject(node: any, type: any, message: any, value?: any) {
  let error = {
    message: "",
    type: "",
    node: "",
    value: ""
  };

  error.message = message;
  error.type = type;
  error.node = node;

  if (value !== undefined) {
    error.value = value;
  }

  return error;
}
src/types.ts
import { EventHandler } from '@create-figma-plugin/utilities'

export interface RunAppHandler extends EventHandler {
  name: 'RUN_APP'
  handler: () => void
}

export interface FirstNodeHandler extends EventHandler {
  name: 'FIRST_NODE'
  handler: (node: any) => void
}

export interface SelectTextNodeHandler extends EventHandler {
  name: 'SELECT_TEXT_NODE'
  handler: (node: TextNode) => void
}
src/ui.tsx

UI側の処理

import {
  Button,
  Container,
  render,
  VerticalSpace,
  Text,
  Banner,
  IconInfo32,
} from '@create-figma-plugin/ui'
import { emit, on } from '@create-figma-plugin/utilities'
import { h } from 'preact'
import { useCallback, useState } from 'preact/hooks'

import styles from './styles.css'
import { FirstNodeHandler, RunAppHandler, SelectTextNodeHandler } from './types'

function Plugin() {
  const [firstNodeArray, setFirstNodeArray] = useState<SceneNode[] | null>([])
  const [isSelected, setIsSelected] = useState<boolean>(false)
  const handleRunAppButtonClick = useCallback(
    function () {
      emit<RunAppHandler>('RUN_APP')
    },
    []
  )
  const handleSelectTextNodeButtonClick = (node: TextNode) => emit<SelectTextNodeHandler>('SELECT_TEXT_NODE', node)

  on<FirstNodeHandler>('FIRST_NODE', function (nodeArray: SceneNode[]) {
    // Reduce the size of our array of errors by removing nodes with no errors on them.
    let filteredErrorArray = nodeArray.filter(
      (item: any) => item.errors !== undefined && item.errors.length >= 1
    );

    setIsSelected(nodeArray.length > 0)
    setFirstNodeArray(filteredErrorArray)
  })

  return (
    <Container space="medium">
      <VerticalSpace space="small" />
      <div class={styles.container}>
        {
          firstNodeArray && firstNodeArray.length > 0 ?
            firstNodeArray.map((item: any, index) => {
              console.log("item", item);
              return (
                <Banner variant="warning" icon={<IconInfo32 />} style={{ "margin-bottom": "10px" }}>
                  <Text style={{ "margin-bottom": "10px", "color": "black" }}>{`${index + 1}. テキストスタイルが適用されていません`}</Text>
                  <Text style={{ "margin-bottom": "10px", "color": "black" }}>{item.errors[0].value}</Text>
                  <Button danger onClick={() => {
                    handleSelectTextNodeButtonClick(item)
                  }}>選択する</Button>
                </Banner>
              )
            })
            : (
              isSelected ? (<Text>テキストスタイルの適用漏れはありません</Text>) : (<Text>フレームを選択して下さい</Text>)
            )
        }
      </div>
      <VerticalSpace space="large" />
      <Button fullWidth onClick={handleRunAppButtonClick}>
        テキストスタイルの適用漏れを検出する
      </Button>
      <VerticalSpace space="small" />
    </Container>
  )
}

export default render(Plugin)

完成!!!

完成サンプルコード

https://github.com/shimabukuromeg/text-node-check-figma-plugin

おわりに

  • figma pluginを作るために figmaで色々遊んでみた結果、figmaの操作方法やオートレイアウトなどの仕組みを覚えれてよかった。figmaの操作感、フロントエンドのマークアップしてる感覚と近いなという感触を得れたのもよかった。デザイナーさんマークアップできる説ある

  • figma pluginいろいろ眺めてると、便利なプラグインがたくさんあって、ほんとみんなすごいなあという気持ちになった

  • この記事とあんまり関係ありませんが、figma to react などのデザインデータからコード生成する未来も割といけるのでは、みたいな気持ちになったので、フロントエンド × Figma の領域引き続きチェックしていきたい

参考

https://speakerdeck.com/mottox2/create-figma-plugin?slide=17

https://zenn.dev/seya/articles/127027b75dbba0

https://github.com/destefanis/design-lint

https://www.figma.com/plugin-docs/api/properties/PageNode-selection

https://www.figma.com/plugin-docs/api/properties/figma-mixed

https://www.figma.com/plugin-docs/api/nodes

GitHubで編集を提案
株式会社モニクル

モニクルは、金融サービステック企業です。 金融の専門知識がなくても、正しい意思決定ができる社会へ。 そのために必要なのは、人によるサービスとテクノロジーの掛け合わせ。 既存のフィンテックとは異なる挑戦に取り組みます。

Discussion

ログインするとコメントできます