Open15

星占いのFigma Widgetをつくってみる

Hiroki TaniHiroki Tani

Figma開発アドベントカレンダー16日目として「星占いのFigma Widget」をつくるチャレンジをしました。
FigJamでのワークショップの前などのアイスブレイクに使えるようなものをイメージしています。

この記事ではWidget開発の勘所が分かる範囲で、簡易的なもの目指して実装してみようとおもいます。

  1. Figmaのデスクトップアプリから新規作成する
  2. WidgetのUIをつくる
  3. Widgetのメニューから星座を選ぶ
  4. 星占いの結果を返すAPIを叩いて表示する

この記事の範囲での最終的な成果物はGitHubに公開しています。

https://github.com/hiloki/figma-horoscope-widget-demo

Hiroki TaniHiroki Tani

今回の記事における最終成果物は下記のようなイメージ。

Widgetのメニューから星座を選ぶと、それに応じた「今日の運勢」が表示されるようにします。

今回利用するAPIはaztro API。各星座のイメージはFreepikにあるものを利用しています。
※aztro APIは認証キーなども不要で無料で使えますが、おそらく個人で運用しているので、一気に大量のリクエストを送るなど、大きな負荷をかけるような処理には気をつけましょう

Hiroki TaniHiroki Tani

FigmaのPluginとWidgetはデスクトップアプリのメニューから作り始めることができます。
Widget > Development > New widget

今回のはFigJamで使えるものにしたいので、Figma design&FigJam を選びます。

適当に名前をつけて、今回はUIなどを用意せずにWidgetの部分だけで完結するものをつくるので、
Simple Widget を選びます。

このウィザードに従っていくと、任意のフォルダを指定して、必要なコードが展開されます。

Hiroki TaniHiroki Tani

前述のウィザードで展開したフォルダに移動し、ターミナルやコードエディタからnpmの各パッケージをインストールします。

# 該当フォルダに移動
$ cd Horoscope
# npmの各パッケージをインストールします
$ npm install

インストール後は下記のコマンドを実行して、コードのビルドをします。これで実際にFigma上で動くコードへの変換をし、Figmaのデスクトップアプリから開発中であるこのWidgetを呼び出すことができます。

# コードの編集時に自動でビルドされる
$ npm run watch

開発中のWidgetはデスクトップアプリのPluginやWidgetを呼び出すメニューから選べます。
開いたpanelのWidgetタブを選び、Recentと表示されているプルダウンでDevelopmentを選ぶと見つけやすいです。

Simple Widgetを選ぶと「-(マイナス)」ボタンと「+(プラス)」ボタンで数値を増減させる単純なWidgetが表示されるはずです。

Hiroki TaniHiroki Tani

主に触るファイルは code.tsx です。
Simple Widgetとしての最初のコードは下記のコードです。

const { widget } = figma
const { useSyncedState, usePropertyMenu, AutoLayout, Text, SVG } = widget

function Widget() {
  const [count, setCount] = useSyncedState('count', 0)

  if (count !== 0) {
    usePropertyMenu(
      [
        {
          itemType: 'action',
          propertyName: 'reset',
          tooltip: 'Reset',
          icon: `<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="http://www.w3.org/2000/svg">
          // ※SVGのところはコード量が多いため省略
          </svg>
          `,
        },
      ],
      () => {
        setCount(0)
      },
    )
  }

  return (
    <AutoLayout
      verticalAlignItems={'center'}
      spacing={8}
      padding={16}
      cornerRadius={8}
      fill={'#FFFFFF'}
      stroke={'#E6E6E6'}
    >
      <SVG
        src={`<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect width="30" height="30" rx="15" fill="white"/>
        <rect x="7.5" y="14.0625" width="15" height="1.875" fill="black" fill-opacity="0.8"/>
        <rect x="0.5" y="0.5" width="29" height="29" rx="14.5" stroke="black" stroke-opacity="0.1"/>
        </svg>`}
        onClick={() => {
          setCount(count - 1)
        }}
      ></SVG>
      <Text fontSize={32} width={42} horizontalAlignText={'center'}>
        {count}
      </Text>
      <SVG
        src={`<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect width="30" height="30" rx="15" fill="white"/>
        <path d="M15.9375 7.5H14.0625V14.0625H7.5V15.9375H14.0625V22.5H15.9375V15.9375H22.5V14.0625H15.9375V7.5Z" fill="black" fill-opacity="0.8"/>
        <rect x="0.5" y="0.5" width="29" height="29" rx="14.5" stroke="black" stroke-opacity="0.1"/>
        </svg>`}
        onClick={() => {
          setCount(count + 1)
        }}
      ></SVG>
    </AutoLayout>
  )
}

widget.register(Widget)

基本的にはこの構成をふまえ、この記事の目標となる星占いWidgetをつくっていきます。

Hiroki TaniHiroki Tani

本題であるコードに入る前にデザインの話をしておきます。

WidgetのUIは完全にゼロからつくるのではなく、Figmaのキャンバス上でのFrameやText等の要素で組み合わせてUIをつくるのと同じようにつくります。それがGUIでの組み合わせではなく、コードで実装するというのがWidgetの開発です。

詳細なコードの解説は後ほど解説しますが、例えば、

FrameをAuto Layoutにして、垂直方向に子要素を並べ、
内側余白の四方16px、子要素間の余白を8px、Fillを #CCCCCCとする

このようなFrameがあった場合、Widgetのコードでは下記のように書きます。
前述のコードでいうと、function Widget() の中の return のあとに記述するコードです。

<AutoLayout
    direction={"vertical"}
    padding={16}
    spacing={8}
    fill={"#CCCCCC"}
> ... </AutoLayout>

なので最終形のデザインはFigma上でモックとしてAuto Layoutや適切な余白や幅の設計、使う色の整理などをしておくと良いでしょう。
Widgetでどのようにそれらを再現するか、使える要素についてはAPI Referenceを参照しましょう。

https://www.figma.com/widget-docs/api/component-AutoLayout

今回の記事の範囲では、このUI部分のコードは下記のようになります。

const CONTAINER_COLOR = "#FFFFFF";
const HEADER_COLOR = "#FCE1BC";
const TEXT_COLOR = "#37143B";
const EMPHASIS_TEXT_COLOR = "#CF4720";
const GRAY_TEXT_COLOR = "#676767";

return (
    <AutoLayout direction={"vertical"} verticalAlignItems={"center"} horizontalAlignItems={"center"} width={360} cornerRadius={12} fill={CONTAINER_COLOR}>
      <AutoLayout direction={"vertical"} horizontalAlignItems={"center"} padding={{ bottom: 16 }} spacing={4} width={"fill-parent"} fill={HEADER_COLOR}>
        {sign ? (
          // 星座を選択しているとき
          <Fragment>
            <SVG src={getSignImage(sign)} width={200} height={170} />
            <Text fill={TEXT_COLOR} fontSize={24}>
              {capitalizeFirstLetter(sign)}
            </Text>
          </Fragment>
        ) : (
          // 星座が未選択のとき
          <Fragment>
            <SVG src={PlaceholderImage} width={200} height={170} />
            <Text fill={TEXT_COLOR} fontSize={24}>
              Choose Your Zodiac Sign
            </Text>
          </Fragment>
        )}
      </AutoLayout>
       // 星座を選択しているとき
      {sign ? (
        <AutoLayout direction="vertical" padding={16} spacing={12} width={"fill-parent"}>
          <Text fontSize={24} width={"fill-parent"} horizontalAlignText={"center"} fill={EMPHASIS_TEXT_COLOR}>
            {currentDate}
          </Text>
          <Text fontSize={14} width={"fill-parent"} horizontalAlignText={"left"}>
            {description}
          </Text>
          <AutoLayout padding={{ top: 8 }} width={"fill-parent"}>
            <Text fontSize={12} width={"fill-parent"} horizontalAlignText={"center"} fill={GRAY_TEXT_COLOR}>
              Zodiac images by <Span href="https://www.freepik.com/free-vector/hand-drawn-zodiac-signs-set_14669618.htm#query=aries&position=26&from_view=search&track=sph">Freepik</Span>
            </Text>
          </AutoLayout>
        </AutoLayout>
      ) : null}
    </AutoLayout>
  );
Hiroki TaniHiroki Tani

次にWidgetのメニューの作り方を解決します。WidgetのメニューというのはWidgetを選んだときに表示されるメニューです。


画像の引用元: https://www.figma.com/widget-docs/api/properties/widget-usepropertymenu#remarks

開発に関するドキュメントは下記の usePropertyMenu の項目を参照してください。

https://www.figma.com/widget-docs/api/properties/widget-usepropertymenu

メニュー部分のコードは下記です。

// # 1. メニューの表示項目
// Dropdownの項目
  const signOptions = [
    { option: "", label: "Choose your sign" },
    { option: "aries", label: "Aries" },
    { option: "taurus", label: "Taurus" },
    { option: "gemini", label: "Gemini" },
    { option: "cancer", label: "Cancer" },
    { option: "leo", label: "Leo" },
    { option: "virgo", label: "Virgo" },
    { option: "libra", label: "Libra" },
    { option: "scorpio", label: "Scorpio" },
    { option: "sagittarius", label: "Sagittarius" },
    { option: "capricorn", label: "Capricorn" },
    { option: "aquarius", label: "Aquarius" },
    { option: "pisces", label: "Pisces" },
  ];

usePropertyMenu(
    [
      { // 星座を選ぶメニュー
        itemType: "dropdown",
        propertyName: "signs",
        tooltip: "Zodiac signs",
        selectedOption: sign,
        options: signOptions,
      },
      {
        itemType: "separator",
      },
      { // 初期状態にするためのアクションボタン
        itemType: "action",
        tooltip: "Reset",
        propertyName: "reset",
      },
    ],
    // # 2. メニューの選択・実行後の処理
    ({ propertyName, propertyValue }) => {
      if (propertyName === "reset") {
        // propertyNameがresetのメニュー(acition)を実行したとき
        resetSign();
      } else if (propertyName === "signs") {
        // propertyNameがsignsのメニュー(dropdown)を実行したとき
        if (propertyValue) {
        // propertyValueが値がある(=星座が選ばれている)とき
          waitForTask(getSign(propertyValue));
        } else {
          // propertyValueが値がない(=星座が選ばれていない初期状態になった)とき
          resetSign();
        }
      }
    }
  );

1. メニューの表示項目

Widgetに実装できるメニューの種類は6種類あります。

種類 内容 利用例
action 単純なアクションボタン データの更新や初期化
color-selector 色を選択するオプション 背景色・文字色の変更
dropdown ドロップダウンメニュー 表示する内容の変更
toggle トグル切り替え UIの表示・非表示の切り替え
link リンク ヘルプページへの誘導
separator メニュー項目の区切り メニューのグルーピング

今回は「星座を選択する」ための dropdown と、初期状態に戻す操作のために action を使います。

2. メニューの選択・実行後の処理

usePropertyMenu の第2引数にはメニューのクリック時に実行される関数を指定できます。その関数の引数に、メニューの名前( propertyName )とその値( PropertyValue )を渡せるので、それらを用いて必要な処理をすることができます。

ここには今回の肝となる「星座ごとの占い結果を取得する」関数 getSign() があります。

const FETCH_URL = "https://aztro.sameerkumar.website/?day=today";

const [currentDate, setCurrentDate] = useSyncedState<string>("currentDate", "");
const [description, setDescription] = useSyncedState<string>("description", "");
const [sign, setSign] = useSyncedState<string>("sign", "");

const options = {
  method: "POST",
};

function getSign(sign: string) {
  return fetch(`${FETCH_URL}&sign=${sign}`, options)
    .then(function (response) {
      return response.text();
    })
    .then(function (text) {
      const result = JSON.parse(text);
      setDescription(result.description);
      setCurrentDate(result.current_date);
    })
    .then(() => {
      setSign(sign);
      figma.notify("Wish you well 🪄");
    })
    .catch(function (error) {
      console.error(error);
    });
}

今回採用したaztro APIhttps://aztro.sameerkumar.website/ にPOSTでリクエストし、パラメータに星座(sign)と日にち(day)を渡せば、その日の星座の運勢に関する情報が返ってきます。

項目 内容
current_date 指定した日付 June 23, 2017
compatibility 相性の良い(?)星座 Cancer
lucky_time 幸運な時間帯 7am
lucky_number 幸運な数字 64
color 幸運な色 Spring Green
date_range 星座の該当する期間 Mar 21 - Apr 20
mood 気分 Relaxed
description 運勢の説明 It's finally time ...

今回のWidgetの仕様としては「今日の運勢」にしたいので、 daytoday で固定し、星座(sign)のみを選ぶようにします。また利用する項目も今回は current_datedescription に絞ります。

APIへのリクエストとレスポンスの処理ところは fetch を使い、リクエストが成功して結果が返ってきたら、順に処理をしていくようにしていきます。今回はfetchに関する詳細な説明は省きます。

補足として getSign() の処理の中で知っておくべきことを書いておくと、下記のようなコードの部分です。

const [currentDate, setCurrentDate] = useSyncedState<string>("currentDate", "");
const [description, setDescription] = useSyncedState<string>("description", "");
const [sign, setSign] = useSyncedState<string>("sign", "");

// ...省略
.then(function (text) {
  const result = JSON.parse(text);
  setDescription(result.description);
  setCurrentDate(result.current_date);
})
.then(() => {
  setSign(sign);
  figma.notify("Wish you well 🪄");
})

usesyncedstate は、状態管理として値を保持することができます。ここではAPIへのリクエストが成功したら、今日の日付(currentDate)と運勢の説明文(description)をそれぞれ保持するようにしています。

https://www.figma.com/widget-docs/api/properties/widget-usesyncedstate

Hiroki TaniHiroki Tani

ここまでの星座を選んでからの処理の流れを整理しましょう。

「今日の日付と運勢の説明文の保持」をした後は、その内容をWidgetのUIに反映する必要があります。
その処理のポイントは usePropertyMenu の解説にあるコードです。

星座(signs)メニューで選択した項目の値をpropertyValueとして、getSign()の引数に渡しています。

// ...省略
} else if (propertyName === "signs") {
if (propertyValue) {
  waitForTask(getSign(propertyValue));
}

waitForTask() はその中の非同期処理、今回であればAPIリクエストをして結果が返ってくるのを永続的に待つ場合に使えます。

https://www.figma.com/widget-docs/api/properties/widget-waitfortask

Hiroki TaniHiroki Tani

リクエストも成功して各値が更新された後は、その内容に応じてUIの表示を変えます。

// signが正 = 星座が選ばれていれば、その値に応じた内容にする
{sign ? (
  <Fragment>
    <SVG src={getSignImage(sign)} width={200} height={170} />
    <Text fill={TEXT_COLOR} fontSize={24}>
      {capitalizeFirstLetter(sign)}
    </Text>
  </Fragment>
) : (
  <Fragment>
    <SVG src={PlaceholderImage} width={200} height={170} />
    <Text fill={TEXT_COLOR} fontSize={24}>
      Choose Your Zodiac Sign
    </Text>
  </Fragment>
)}

getSignImage() は星座に応じて画像を入れ替える部分のコードです。
Widget開発の解説としては直接関係がないのですが、一応該当部分のコードも掲載しておきます。
細かいところは全体のコードを確認してください。

function getSignImage(sign: string) {
  // 選択された星座ごとのSVG画像を返す
  switch (sign) {
    case "aries":
      return Aries;
    case "taurus":
      return Taurus;
    // 省略
    default:
      return "";
  }
}

https://github.com/hiloki/figma-horoscope-widget-demo/blob/main/widget-src/code.tsx

Hiroki TaniHiroki Tani

星座を選んでから、改めて別の星座を選べばその星座でリクエストし直すことができます。ですが、初期状態に戻したいこともあるかもしれません。今回はサンプルの実装としてその処理についても書いておきます。

まずメニューの部分は action のtypeを指定しています。

usePropertyMenu(
  [
    // ...省略
    {
      itemType: "action",
      tooltip: "Reset",
      propertyName: "reset",
    },
  ],
  // ...省略

そして初期状態にする、つまり保持していた値をリセットするための処理を書きます。
Resetのアクションボタンをクリックされたら、その値を元にresetSign() という関数を実行します。

function resetSign() {
  setSign("");
  setDescription("");
  setCurrentDate("");
}

usePropertyMenu(
  // ...省略
  ({ propertyName, propertyValue }) => {
    // Resetの操作をされた場合の処理
    if (propertyName === "reset") {
      resetSign(); 
    }
    // ...省略
  }
);
Hiroki TaniHiroki Tani

冒頭でも案内しましたが、今回の記事で解説した範囲のコードはGitHubに公開しています。

https://github.com/hiloki/figma-horoscope-widget-demo

このコードをローカルにcloneした後は、Figmaのデスクトップアプリからインポートすると、ローカルで実際に動くものを確認できるはずです。

既存のコードをインポートする場合は、PluginやWidgetを呼び出すメニューのところにある「+(プラス)」ボタンをクリックし、Import widget from manifest ... で、cloneしたファイルにある manifest.json を選べばインポートできます。

Hiroki TaniHiroki Tani

利用するAPIではラッキーナンバーやカラーも受け取れますし、もっとカスタマイズする余地があります。

  • ラッキーナンバー、カラーを表示する
    • カラーはFillにいれて表示する
  • フォントを変える
  • 星座を選び方を変えてみる...など

またそのあたりの解説については、いつかまた記事か小さな書籍にできればと思います。

Figma開発アドベントカレンダーはFigmaに関する開発の有益な記事が展開されているので、他の記事もぜひチェックしてみてください!

https://qiita.com/advent-calendar/2022/figma-development