Create Figma Plugin を使って Figma Plugin 開発に入門してみるぞ
はじめに
この記事は、Figma Pluginに興味を持ち、実際に触れてみた経験をまとめたものです。私がFigma Pluginをさわってみた理由は以下の通りです。
- デザイナーとの仕事をよりスムーズに進めるため、Figmaの知識を深めたかった。
- Figma Pluginの仕組みに興味を持った。
- Create Figma Plugin の存在を知り、使ってみたかった。
この記事が、Create Figma Pluginを使ってFigma Pluginの開発を始めたい方にとって参考になれば嬉しいです。
Figma Pluginとは
Figmaでは、プラグインをインストールして機能を拡張できます。詳細は 公式ドキュメントをご覧ください。
また、以下は最近気に入った便利なプラグインの例です。
さらに、Figma Pluginは自作も可能です。公式セットアップガイドを参考にすれば、簡単なサンプルプラグイン(四角形を生成するプラグイン)を試せます。
Create Figma Pluginとは
Create Figma Pluginは、Figma Plugin の開発で利用するテンプレートやUIコンポーネント、ビルド周りの整備などをいい感じに提供してくれてるライブラリです。
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で表示させてほしいイベントを受けとって表示させたりする
詳細は以下のドキュメントに詳しく書いています。
以下、メインスレッド側のコードとUI側の処理の関係性について、具体的なコードと合わせてまとめて見ました。
プラグインがどのように動作しているのかについては、この記事もわかりやすかったです。
Create Figma Plugin の場合
Create Figma Plugin では、上記の仕組みをいい感じにラップしたユーティリティ関数を提供してたり、generateした際できるファイル構成(テンプレートを使うと main.ts
、ui.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側のコード。ユーザーからの入力、ブラウザからのアクセスなど。
上述した postMessage
、onmessage
のやりとりのコードを以下のユーティリティ関数を使って書くことができます。
-
emit<Handler>(name, ...args)
でイベント発火 -
on<Handler>(name, handler)
でイベント受け取る
詳細はドキュメント
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)
完成!!!
完成サンプルコード
おわりに
Figma Plugin開発を通じて、Figma操作の理解が深まりました。特にオートレイアウトなどの仕組みが、フロントエンドのマークアップに似ていると感じました。
また、Figma Pluginの活用可能性や「Figma to React」といった分野への関心が高まりました。引き続き、この領域を探索していきたいと思います。
参考
Discussion