Create Figma Plugin を使って Figma Plugin 開発に入門してみるぞ
はじめに
- この記事は Figma Plugin に入門してみたくなって、いろいろと試したことをまとめた記事です
- Figma Plugin に入門したくなったモチベーション
- デザイナーさんといい感じに仕事できるようにFigmaの知識を持っておきたかった
- Figma Plugin がどういう仕組みで動いてるのか気になってた
- Create Figma Plugin が気になってた
- Create Figma Plugin を使って Figma Plugin を開発してみたい人の参考になれば嬉しいです
Figma Pluginとは
Figmaは、プラグインをインストールして機能拡張することができます。詳細は、公式ドキュメントにて
世の中には便利プラグインがたくさん公開されてる。以下、最近良いなと思った適当なプラグイン
また、プラグインは自分で作ることもできます。Figmaのドキュメントにあるセットアップガイド通りに試してみると、サンプルのプラグイン(rectangles生成するプラグイン)を動かしてみることができます。詳細は以下のページにて
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の操作方法やオートレイアウトなどの仕組みを覚えれてよかった。figmaの操作感、フロントエンドのマークアップしてる感覚と近いなという感触を得れたのもよかった。デザイナーさんマークアップできる説ある
-
figma pluginいろいろ眺めてると、便利なプラグインがたくさんあって、ほんとみんなすごいなあという気持ちになった
-
この記事とあんまり関係ありませんが、figma to react などのデザインデータからコード生成する未来も割といけるのでは、みたいな気持ちになったので、フロントエンド × Figma の領域引き続きチェックしていきたい
参考
Discussion