0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ここのえAdvent Calendar 2023

Day 22

Figmaのマスク仕様が納得いかないので、プラグインを作った

Last updated at Posted at 2023-12-21

この記事は ここのえ Advent Calendar 2023 Day 22 の記事です。

Figmaのココだけ納得いかない……

昨年のAdobeによるFigma買収発表後1、年明け早々Adobeの製品一覧からAdobe Xdが姿を消しました。2。10月頃には終了をにおわせるような内容が記載されていた3ことで、Twitterで話題になったのも記憶に新しいです。

そんなこんなでXdを諦めて、今年の3月頃から Figma に本格的に移行を始めました。

少人数のチーム開発で何度か使っていますが、かなりパワフルな印象があります。最近はDev Modeなんかもあるのでエンジニア視点でも期待しています。


そんな Figma にはどうしても1点だけ腑に落ちない点があります。

マスクの仕様です。

figma_mask.png

イラレ、Xdと逆!!

どうやら色々調査をしていると、Sketchを源流としてUIデザインツールではこの仕様が当たり前みたいな風潮があるようです。
調べた限りではXd以外の大半のツールがレイヤーを下に置く形みたいです。

ただ理解はできても納得はできず、同じように考えてる人は相当数いるようで…… フォーラムにもこんな記事が出てます。

なんだかんだでしばらく我慢していたのですが、どうやっても毎秒操作を間違えておかしなマスクを作ってしまいます。

一年の後、汝水のほとりに宿った時、遂に発狂して虎になってしまいました。

作った

上からマスクする方法は、設定でどうにかできるものでもありません。プラグインも軽く探しましたが、発見できませんでした。

無いならどうするか? 作りましょう。……作りました。

hero.png

マスクしたいオブジェクトとシェイプを複数選択した状態で右クリックして、 コンテキストメニューの Plugins -> upper-mask でマスクを作成できます。

Figmaプラグイン実装まわりのTips

ここから本題です。
せっかくプラグインを作ったので、Figmaのプラグイン周りのTipsを幾つか紹介します。

基本的な概念は Japanese Dammy Text の作者さんの記事が分かりやすいのでお勧めです。私も制作時にお世話になりました。この場を借りて感謝申し上げます :bow:

@types/node と競合する

プラグイン作成時、自動で以下のような tsconfig.json になっています。

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 を書き換えて解決するのも一応できます。

tsconfig.json
"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)

ただし例外があり、ページ直下にあるノードは .parentnull になります
従って、parent要素がない場合はページの参照を取ってくる必要があります。

let parent: (BaseNode & ChildrenMixin) | null = selection[selection.length - 1].parent;
if (parent === null) parent = figma.currentPage;

ドキュメントと並行してplugin-api.d.tsも確認すべし

例えばオブジェクトをマスクにする際は、isMasktrue に必要があります。ドキュメントを見ると、BooleanOperationNode, ComponentNode, ComponentSetNode, etc...とあり16種のノードが対応しています。4

当然ですが、型ガードの為に全部|で繋いでなんて書いていられません。Plugin APIのDocsには何も書いていないのですが、@figma/plugin-typings/plugin-api.d.tsを見るとひっそりとインターフェースが定義してあります。

plugin-api.d.ts
interface BlendMixin extends MinimalBlendMixin {
  isMask: boolean
  maskType: MaskType
  effects: ReadonlyArray<Effect>
  effectStyleId: string
}

ちなみに注意なのですが、BlendMixinをドキュメント側で検索しても、何も出てきません

Tips: WebStormを開発に使っている場合

パッケージ内にある型定義ファイルに存在しているので、Include non-project items をオンにしておかないとSearch Everywhereの検索にひっかりません。
自分はこれにやられました。

image.png

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検索でも全然出てこなかったのに……

  1. https://blog.adobe.com/jp/publish/2022/09/16/cc-intent-to-acquire-figma

  2. https://forest.watch.impress.co.jp/docs/serial/yajiuma/1472928.html

  3. https://helpx.adobe.com/jp/support/xd.html

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

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?