この記事は ここのえ Advent Calendar 2023 Day 22 の記事です。
Figmaのココだけ納得いかない……
昨年のAdobeによるFigma買収発表後1、年明け早々Adobeの製品一覧からAdobe Xdが姿を消しました。2。10月頃には終了をにおわせるような内容が記載されていた3ことで、Twitterで話題になったのも記憶に新しいです。
そんなこんなでXdを諦めて、今年の3月頃から Figma に本格的に移行を始めました。
少人数のチーム開発で何度か使っていますが、かなりパワフルな印象があります。最近はDev Mode
なんかもあるのでエンジニア視点でも期待しています。
そんな Figma にはどうしても1点だけ腑に落ちない点があります。
マスクの仕様です。
イラレ、Xdと逆!!
どうやら色々調査をしていると、Sketchを源流としてUIデザインツールではこの仕様が当たり前みたいな風潮があるようです。
調べた限りではXd以外の大半のツールがレイヤーを下に置く形みたいです。
ただ理解はできても納得はできず、同じように考えてる人は相当数いるようで…… フォーラムにもこんな記事が出てます。
なんだかんだでしばらく我慢していたのですが、どうやっても毎秒操作を間違えておかしなマスクを作ってしまいます。
一年の後、汝水のほとりに宿った時、遂に発狂して虎になってしまいました。
作った
上からマスクする方法は、設定でどうにかできるものでもありません。プラグインも軽く探しましたが、発見できませんでした。
無いならどうするか? 作りましょう。……作りました。
マスクしたいオブジェクトとシェイプを複数選択した状態で右クリックして、 コンテキストメニューの Plugins
-> upper-mask
でマスクを作成できます。
Figmaプラグイン実装まわりのTips
ここから本題です。
せっかくプラグインを作ったので、Figmaのプラグイン周りのTipsを幾つか紹介します。
基本的な概念は Japanese Dammy Text の作者さんの記事が分かりやすいのでお勧めです。私も制作時にお世話になりました。この場を借りて感謝申し上げます
@types/node
と競合する
プラグイン作成時、自動で以下のような tsconfig.json
になっています。
{
"compilerOptions": {
"target": "es6",
"lib": ["es6"],
"strict": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@figma"
]
}
}
デフォルトでこんな設定になっていますが、Custom UI
の開発などの理由で@types/node
を導入した場合、 fetch()
の定義が干渉します。
node_modules/@types/node/globals.d.ts:350:18 - error TS2451: Cannot redeclare block-scoped variable 'fetch'.
350 declare function fetch(
~~~~~
node_modules/@figma/plugin-typings/index.d.ts:27:9
27 const fetch: (url: string, init?: FetchOptions) => Promise<FetchResponse>
~~~~~
'fetch' was also declared here.
Typescriptを入れると反射的に入れがちですが、不要なら@types/node
は導入しないほうが良いです。
typeRoots
を書き換えて解決するのも一応できます。
"typeRoots": [
"./node_modules/@figma"
]
group()
によるグループ化を行う際のparent要素
グループ化に使うgroup
メソッドですが、第二引数でグループが属する親要素を指定する必要があります。
group(nodes, parent, index?): GroupNode
Figmaは長方形や円、画像といったオブジェクトのことをNodeと呼びます。
node.js
のnodeではないので注意。
SceneNodeは基本的に親要素を所持しているため、例えば選択したノードをグループ化する時はこれが最適です。
// 選択しているノード
const selection = figma.currentPage.selection;
// 最後に選択したノードの親要素をグループの親要素とする
const parent = selection[selection.length - 1].parent
group(selection, parent)
ただし例外があり、ページ直下にあるノードは .parent
が null
になります。
従って、parent要素がない場合はページの参照を取ってくる必要があります。
let parent: (BaseNode & ChildrenMixin) | null = selection[selection.length - 1].parent;
if (parent === null) parent = figma.currentPage;
ドキュメントと並行してplugin-api.d.ts
も確認すべし
例えばオブジェクトをマスクにする際は、isMask
を true
に必要があります。ドキュメントを見ると、BooleanOperationNode
, ComponentNode
, ComponentSetNode
, etc...とあり16種のノードが対応しています。4
当然ですが、型ガードの為に全部|
で繋いでなんて書いていられません。Plugin APIのDocsには何も書いていないのですが、@figma/plugin-typings/plugin-api.d.ts
を見るとひっそりとインターフェースが定義してあります。
interface BlendMixin extends MinimalBlendMixin {
isMask: boolean
maskType: MaskType
effects: ReadonlyArray<Effect>
effectStyleId: string
}
ちなみに注意なのですが、BlendMixin
をドキュメント側で検索しても、何も出てきません。
Mixinの型ガード
FigmaというよりTypescriptの話題になりますが、プラグイン開発の上で使用頻度が高そうなので紹介しておきます。
例えば先述のisMask
を使うためにBlendMixin
かどうかの判定が必要になりますが、@figma/plugin-typings
で提供されているの型定義だけですし、実行時に判定するためinstanceOf
, typeof
は使えません。
ただ SceneNode
にはisMask
プロパティは存在しないので、当然以下のようなコードはコンパイルエラーになります。
if(maskNode.isMask !== undefined) {
// TS2339: Property isMask does not exist on type SceneNode
// Property isMask does not exist on type SliceNode
これについては2パターンの対応ができます。
1. as
を普通に使う
as
を使って判定を行う方法です。最もシンプルです。
if((maskNode as BlendMixin).isMask !== undefined) {
2. ユーザ定義型ガードを使う
Typescriptの構文に、 ユーザ定義型ガード があります。
foo is bar
の構文を使うことでユーザ定義の型ガード関数を作成することができます。
const isMaskable = (node: SceneNode | (SceneNode & BlendMixin)): node is SceneNode & BlendMixin => {
return (node as BlendMixin).isMask !== undefined;
};
...
if(isMaskable(maskNode)){
// code
}
その他
-
GroupNode.children
はレイヤー構造を保証します。[0]
が最背面、[length-1]
が最前面です。- 対して
figma.currentPage.selection
はレイヤー順を保証しません。恐らく選んだ順になっています。
- 対して
-
group.insertChild(index, target)
のtarget
をグループ内の要素にした場合、コピーはされず順序が入れ替わる挙動になります。
おわりに
プラグイン製作なので少し癖はありますが、比較的素直にAPIを叩けたのでなんとかなりました。Webベースで動いている事もあり、デバッグで普通にJavascriptのコンソールが使えるのも有難いです。
ちなみにこの記事を書き終えた時点では、まだFigma側の審査が終わっていないためPublishされていません。気長に待ちます。
実は「何故Figmaだけレイヤーを下に?」みたいな記事を書こうと思ってたのですが、調べてたらむしろXdが特殊だという事に気づきボツにした経緯があります。マスク上が普通である世界線に行きたい……
なおその後
upper-mask
が承認されたタイミングでリリースページを見ていたら、「これもオススメ!」に こんなプラグインが出てきました。
車輪の再発明で泣きました。プラグイン検索でもGoogle検索でも全然出てこなかったのに……