Figma Pluginの作り方(初級編)とTips

f:id:kudakurage:20190810231459p:plain

こんにちは、くだくらげです。今年のはじめにはじめてのUIデザインという本を共著で書きました。 PEAKSさんから出版しており購入できますので、ご興味ある方はぜひ手にとってみていただけると嬉しいです。 peaks.cc

概要

FigmaにもようやくPlugin機能が公開されましたね。 私もどうしても欲しかったテキストのみ(Styleなし)をコピペするだけのプラグインを作りました。地味に便利なプラグインなので、よろしければ使ってみてください。

Plugin機能はまだまだ公開されたばかりなので、Figma Pluginに関するドキュメント(Tipsブログとか)はまだ少ないですが、割と簡単に作れてしまうのでFigma Pluginの作り方をかんたんに紹介したいと思います。

これからPluginを書いてみようかなと思っている方の参考になればと思います。

基本的にはHTML + Javascript

Figmaプラグインは基本的にHTML + Javascriptでできています。Figma自体がWebAssemblyでできておりC++で書かれていることから、相性が良いのかもしれません。

実際にはVS CodeTypescriptで書いてコンパイルするような感じです。

とはいえ、ほぼJavascript感覚で書けるので、ある程度経験のある方なら楽に書けるなぁという印象でした。

www.figma.com

VS CodeとTypescriptで開発環境を整える

ドキュメントどおりに開発するのが楽だったので、そのとおりにやるのが良いと思います。

1. VS Codeをダウンロード&インストールする

code.visualstudio.com まずはVS Codeをダウンロードして、圧縮ファイルを展開し、アプリケーションフォルダに入れて起動します。

2. TypeScriptをインストールする

次にターミナルでTypeScriptをインストールします。 node.js(npm)が入っていない場合は事前にインストールしておきます。

nodejs.org

ターミナルを起動して、以下のコマンドを入力&実行してインストールができればOKです。

npm install -g typescript

3. FigmaでPluginを作成する

f:id:kudakurage:20190810223808p:plain

Menu > Plugins > Development > New Plugin... で新しいプラグインを作成します。

f:id:kudakurage:20190810224002p:plain

プラグイン名を入力します。

f:id:kudakurage:20190810224121p:plain

テンプレートは3種類で、空のテンプレートと一度実行して終わるプラグインのテンプレート、UIを伴うプラグインのテンプレートから選びます。何を作りたいか次第ですが、最初は一度実行して終わるプラグインのテンプレート、UIを伴うプラグインのテンプレートを選ぶのが良いと思います。

4. VS CodeでPluginのフォルダーを開く

f:id:kudakurage:20190810234620p:plain

新規作成したPluginフォルダをVS Codeで開きます。VS Code内でファイルを展開して見れるようになります。

5. TypeScriptの自動コンパイルを設定する

f:id:kudakurage:20190810234753p:plain

Menu > Terminal > Run Build Task... でTypeScriptの自動コンパイルを設定します。

f:id:kudakurage:20190810234941p:plain

選択肢で tsc: watch - tsconfig.json を選択すれば設定完了です。

これで、 code.ts ファイルを編集・保存すると自動的にコンパイルされて code.js ファイルが生成されるようになります。

6. 試しにPluginを実行する

f:id:kudakurage:20190810235200p:plain

Menu > Plugins > Development に先程作成したPluginが追加されているのでクリックすると試しに実行することができます。 Plugin ファイルを編集して、すぐに気軽に実行できるので良いですね。

7. ConsoleでDebugする

f:id:kudakurage:20190810235514p:plain

Menu > Plugins > Development > Open Consoleでデバックコンソールを表示できます。 Chromeでのデバックコンソールと同じように利用できるので、慣れている人には楽ですね。

code.tsを編集してプラグインのコードを書く

メインで編集することになるのは code.ts ファイルです。 APIドキュメントを参考にしつつ、Pluginの実行処理コードを書きます。

manifest.jsonを編集してプラグインの基本情報を編集する

manifest.json ファイルにはプラグインの基本情報が記述されています。

  • name
  • id
  • api
  • main
  • ui(任意)
    • iframeモーダルで表示するプラグインのUIで使用されるHTMLファイルへの相対ファイルパスです
  • menu(任意)
    • メニューに複数コマンドを含めたり、サブメニューを表示するための項目です

menujson形式でメニューの名前とコマンド名を羅列していくと複数コマンドをプラグインメニューに表示できます。 separator も表示できますね。

"menu": [
  {"name": "Create Text", "command": "text"},
  {"name": "Create Frame", "command": "frame"},
  {"separator": true},
  {"name": "Create Shape",
   "menu": [
     {"name": "Create Circle", "command": "circle"},
     {"separator": true},
     {"name": "Create Rectangle", "command": "rectangle"}
   ]
  }
]

プラグインのモーダルUIを表示する

プラグインのモーダルUIはHTMLで記述することができます。

作成したHTMLファイルへのパスを manifest.jsonui の項目に追記します。 メインコード( code.ts )からは figma.showUI(__html__) で呼び出すことができます。 これでFigma内で <iframe/> によってUIを表示することができます。

メインコードとモーダルUIとの間でメッセージをやり取りすることもできます。 メッセージといってもstring型だけでなく、もちろんarraysやData Objectもやり取りできます。

モーダルUIからメインコードにメッセージを送信する場合

UIのHTML側には以下のように記述します。

<script>
...
parent.postMessage({ pluginMessage: 'anything here' }, '*')
...
</script>

メインコード側には以下のように記述します。

figma.ui.onmessage = (message) => {
  console.log("got this from the UI", message)
}

メインコードからモーダルUIにメッセージを送信する場合

メインコード側には以下のように記述します。

figma.ui.postMessage(42)

UIのHTML側には以下のように記述します。

<script>
... javascript
onmessage = (event) => {
  console.log("got this from the plugin code", event.data.pluginMessage)
}
...
</script>

プラグインをPublishする

コードを書き上げ、デバックもしてプラグインが完成したらPublishします。 Figma.app上のPluginタブに移動して、Develomentに表示されている開発中のPluginでコンテキストメニューからPublishできます。かんたんで便利ですね。

f:id:kudakurage:20190811071112p:plain

Publishには書いたコード群の他に、説明文やアイコン等を用意する必要があります。

  • Name
  • Icon
  • Artwork
    • プラグインのアートワーク画像で、2048x1024pxのサイズで作成します
  • Description
  • Support contact
    • プラグインのサポート用の連絡先です
    • EmailアドレスかURLを入力します
  • Version notes
    • バージョンごとの変更履歴を入力します(最初はリリースですね)

f:id:kudakurage:20190811072017p:plain

APIドキュメント

Figma.app上の要素にアクセスするためのAPIはドキュメントとしてまとまっています。詳しくはAPIドキュメントを参照しながら作ってみてください。

www.figma.com

figma というgrobal object、componentsやTextなどのエレメント別のNodeの情報などがまとまっています。もちろんeffectやfontといったstyle周りのプロパティにもアクセス可能です。

サンプルプラグイン

最初にプラグインを書くときには、サンプルのプラグインが割と参考になりました! github.com

かんたんなサンプルばかりなので、難しいことをやろうと思うと参考にはなりません。ですが、最初にどんな感じで書き始めたら良いかなというときには参考になります。 モーダルUIを使用するものから、図形を扱うもの、Figmaのデータから検索するもの、webpackを使ったプラグインなど種類も様々です。

レビューガイドライン

PluginはPublishしてすぐには公開されません。レビューされて承認され初めて公開されます。

私の場合はレビューが返ってくるまでに2日くらいかかりました。

レビューにはかんたんなガイドラインがあるので、一応目を通しておくと良いと思います。

Plugin Review Guidelines - Figma

ちなみにプラグインを使った収益化は禁止されているようです。

それからアップデートのときにはレビューがされないようです。(2019年8月20日時点) Republishしたら最新版がすぐに反映されたので、おそらくレビューされずに通っているのではないかと。

これは悪意を持って変な操作をできてしまう可能性があるので、ちょっと怖いところもありますね。

開発のヒント・Tips

今回実際にプラグインを作ってみてわかった、こういうときはこうすると良いというTips的なこともご紹介します。 私が作った範囲での話ですので、これからこんな感じでTipsが公開されていくと良いなぁと思っています。

選択しているObjectのデータは順序が保証されない

figma.currentPage.selection

選択しているオブジェクトを取得するという基本的ですが、よく使うであろうAPIで以上のように書くことができます。 Data形式はArray型で各種形式のNodeデータが格納されています。

注意すべき点はselectionで得られるObject Dataは順序がどうなるかわからないということです。 レイヤー順で返ってくることを期待するかもしれませんが、順序は保証されないので、ランダムで返ってくることを考慮してPluginを作るべしと書かれています。

selection

The selected nodes on this page. Each page stores its own selection separately. The ordering of nodes in the selection is not guaranteed -- you should assume that it is random, like a Set.

www.figma.com

Browser APIを使用するためにはモーダルUIを使用する

今回私が作ったプラグインは、テキストをコピペするだけというシンプルなプラグインなので、モーダルUIを表示する必要はありませんでした。 しかし、ClipboardにアクセスするためにはBrowser APIを使用する必要がありそうでした。

Browser APIを使用するためにはモーダルUIからでないとアクセスできません。

そのため、今回はBrowser APIを使用するためにモーダルUIを使用するが表示はしないという、ちょっとハック的な感じで書きました。

具体的には以下のような感じで、 figma.ui を呼び出しつつ、 hide() 関数で非表示にしています。 非表示にはなりますが、コード自体は引き続き実行されるので、UIは必要ないけどBrowser APIは使いたいという場合には良いと思います。

figma.showUI(__html__);
figma.ui.hide();

ちなみに、ClipboardにアクセスするためにAsync Clipboard APIを使ってNavigatorクラスから navigator.clipboard.writeText(hoge); とこんな感じで扱えると便利だったのですが、どうやらまだ対応していないようでした…。

なので、 <textarea/>document.execCommand("copy"); を使った古典的な方法で実装しました。

プラグインコマンドを実行したときにメッセージを表示する

テキストをコピーするコマンドでは実行したときに画面に変化がないので、正しく実行できたかわからないという部分がありました。

標準のコピーコマンドも同じく画面に変化がないので、そのままでも良い気はしますが、何をコピーできたのかを確認できる意味も含めてメッセージを出すことにしました。

Figma Pluginでコマンド実行時にトーストメッセージを出すのはかんたんで、プラグインの実行完了時に呼ぶ figma.closePlugin() の変数にメッセージを追加するだけです。

closePlugin(message?: string): void

Closes the plugin. You should always call this function once your plugin is done running. When called, any UI that's open will be closed and any setTimeout or setInterval timers will be cancelled.

Text Object(Node)をいじる場合は予めFontをロードしておく

Text Objectを新しく作成したり、既存のテキストを上書きする場合には事前にFontをロードしておく必要があります。

Loading fonts

The important thing with text is that changing the content of a text node requires its font to be loaded and that fonts are not always available. If you attempt to change, say, fontSize without first loading the font for that text node, the plugin will throw an exception.

Loading a font is done via figma.loadFontAsync(text.fontName) or figma.loadFontAsync(newFont) before setting a new font.

https://www.figma.com/plugin-docs/api/TextNode/#loading-fonts

具体的にはText Objectの変更を実行する前に、以下のような感じで使用するフォントをロードしておく必要があります。 (大したことではないですが、正直めんどいですね)

async function loadFont() {
  await figma.loadFontAsync({ family: 'Robot', style: 'Regular' });
}

Text Object(Node)をいじる場合はMixed Stylesの状況を考慮しておく

テキストを書き換えたり、テキストのStyleを取得したりする場合に、複数のスタイルが混ざっている場合を考慮する必要があります。

Mixed styles

Many text properties can be set on individual characters. For that reason, you will find that many of text properties can return the special value figma.mixed if there are characters in the text with different values for that property. Always consider that a text properties could have mixed values.

https://www.figma.com/plugin-docs/api/TextNode/#mixed-styles

例えば、一つのテキストの一部がBoldになっていたり、文字サイズが大きくなっていたりという場合です。

私の場合は、フォントを事前にロードするときに現状使われているフォントを見て、それをロードするというコードを書いていたのですが、複数フォントが使われている場合を考慮できていなかったのでエラーが出てしまいました。 複数のフォントStyleが混ざっている場合は figma.mixed が返されます。

async function loadFont(selectedItem) {
  ...
  await figma.loadFontAsync({ family: selectedItem.fontName.family, style: selectedItem.fontName.style });
  ...
}

↑この場合 selectedItem.fontName.familyfigma.mixed が返され、ロードに失敗してエラーが出るという感じです。

私の場合は、テキストの先頭の1文字のStyleを見るようにすることで解決しました。

async function loadFont(selectedItem) {
  ...
  let selectedItemFontName = selectedItem.getRangeFontName(0, 1);
  await figma.loadFontAsync({ family: selectedItemFontName.family, style: selectedItemFontName.style });
  if(selectedItem.fontName == figma.mixed){
    selectedItem.setRangeFontName(0, selectedItem.characters.length, selectedItemFontName);
  }
  ...
}

まとめ

作りたいものは事前に決まっていましたが、作ろうと思ってセッティングをしたりドキュメントを読んだりして、出来上がってPublishするまで半日経たないくらいでできてしまったので、本当にお手軽に作れて良いなぁという印象でした。

そんなに凝ったPluginでなくても、ちょっと便利になるようなものをこれからも思いついたときに作ってみたいと思っています。

あと、これから作るという人もコードやTipsを公開してもらえると、いろいろと参考になるので嬉しいですね!

雑なコード(今見ても直さないと…と思うところばかり)ですが、私のもgithubにアップロードしていますので、参考にしてみてください。もちろんバグ報告やPRもお待ちしております。