Kick Out the World

技術的なメモとかポエムを書きます。

Moddableでアイコンを手軽に表示したい

ModdableでUIを持つアプリケーションを作るあたり、アイコン的なものを表示したいケースがある。

単品のアイコンであれば画像アイコンをPNGなりで読みこむか、スプライトな画像を作ってTextureでvariantで表示を切り替えるという方法が従来からあるが、今回ある程度の数を持ったアイコンセットを手軽に扱いたいケースを考える。

fontbmでアイコンフォントを使う

Webで手軽にアイコンを表示したい場合に使うのはアイコンフォント。対象アイコンのコードポイントとして文字を表示をすればよいので扱いは容易。 ただし、数百もアイコンがあるフォントファイルとなるとマイコン向けのリソースとしては容量が大きいので、必要分に絞って容量を削減しつつ使いたい。

ということで使うのは昨年サポートされた(fontbm)https://github.com/vladimirgamalyan/fontbm/を使う。

fontbmでは、ttfファイルをModdableで使用するfntとpngを生成するコマンドラインツール。Moddableではmanifestファイルにfontbmの設定を書くことでビルド時に自動生成させる仕組みが導入された。詳細はModdableのBlogでも紹介されている。

今回はアイコンセットとしてI(coMoon)https://icomoon.io/を使用した。今回の手順に従って生成されたアイコンの再配布については、IcoMoonの再配布の条件に従ってくださ。

fontbm導入

通常のModdableセットアップ手順には含まれていないため、別途インストールする必要がある。

Windowsであれば実行可能形式で配布されており、Mac/Linuxでは自前でビルドする必要がある。手順については割愛。 その後、FONTBM環境変数を設定する。なお、これはfontbmのフォルダではなく、 fontbmのプログラムそのもののパス の指定となるため注意。

manifest.jsonでfontbmを設定する

今回使用したアイコンフォントは(IcoMoonのFree Version)(https://icomoon.io/#icons-icomoon)でアイコン数は490。ファイルサイズとしては127KBだった。

指定としては、以下のように対象のアイコンファイル、出力のサイズ、対象の文字(今回は適当に選んだ5個)を指定する。

   "resources":{
        "*-mask": [
            {
                "source": "./IcoMoon-Free",
                "size": 28,
                "characters":"\ue900\ue905\ue90f\ue911\ue912"
            }
        ]
    }

これでビルドすると、($MODDABLE)//build/tmp/mac/mc/debug/($application)/resources/にfontbmで変換されたフォントファイルが生成される。 リソースとして読み込む.fnt.pngのファイルサイズが小さくなっていることが確認できる。

~ ll ~/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-*
-rw-r--r--  1 satoshi  staff   2.6K  1  8 22:21 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28-alpha.bmp
-rw-r--r--  1 satoshi  staff   1.0K  1  8 22:21 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28.bf4
-rw-r--r--  1 satoshi  staff   186B  1  8 22:21 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28.fnt
-rw-r--r--  1 satoshi  staff   146B  1  8 22:20 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28.json
-rw-r--r--  1 satoshi  staff   1.9K  1  8 22:21 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28.png
-rw-r--r--  1 satoshi  staff    15B  1  8 22:20 /Users/satoshi/Projects/moddable/build/tmp/mac/mc/debug/text/resources/IcoMoon-Free-28.txt

あとは通常のPiuアプリケーションにて、対象のアイコンをUnicode文字参照として記載すればアイコンが表示される。

const blackSkin = new Skin({ fill: "black" });
const whiteSkin = new Skin({ fill: "white" });
const textStyle = new Style({
  font: "IcoMoon-Free-28",
  left: 10,
  right: 10,
  top: 15,
  bottom: 15,
});
const centerStyle = new Style({ horizontal: "center" });

let TestApplication = Application.template(($) => ({
  skin: blackSkin,
  style: textStyle,
  contents: [
    Text($, {
      left: 0,
      right: 0,
      top: 0,
      skin: whiteSkin,
      style: centerStyle,
      blocks: [{ spans: ["\u{e900}\u{e905}\u{e90f}\u{e911}\u{e912}"] }],
    }),
    Icon({ icon: "home", left: 0, top: 100, size: 32, color: "white" }),
    Icon({ icon: "pencil", left: 40, top: 100, size: 48, color: "green" }),
    Icon({ icon: "camera", left: 90, top: 100, size: 56, color: "yellow" }),
    Icon({ icon: "music", left: 150, top: 100, size: 24, color: "red" }),
    Icon({ icon: "play", left: 180, top: 100, size: 32, color: "purple" }),
  ],
}));

Outlineを使ったSVG描画

これも(昨年追加された機能)https://blog.moddable.com/blog/scalableoutlines/で、Canvas互換のAPIでパスを描画できるようになり、SVGのパスも指定することでレンダリングができるようになった。 そのため、SVGで作られたアイコンがそればそのまま使うことができそう。

幸いにも(IcoMoonのカスタマイズアプリ)https://icomoon.io/appでアイコン単体のSVGをがダウンロードできるため、ここからパスを抜き出して使ってみる。 アイコンフォントでは文字として直接使うことができたが、Outlineの場合はパスの描画処理を書く必要があるため、アプリケーションから取り回ししやすいようにIcon Objectを定義してみた。

const iconMap = {
  home: "M512 295.222l-256-198.713-256 198.714v-81.019l256-198.713 256 198.714zM448 288v192h-128v-128h-128v128h-128v-192l192-144z",
  pencil:
    "M432 0c44.182 0 80 35.817 80 80 0 18.010-5.955 34.629-16 48l-32 32-112-112 32-32c13.371-10.045 29.989-16 48-16zM32 368l-32 144 144-32 296-296-112-112-296 296zM357.789 181.789l-224 224-27.578-27.578 224-224 27.578 27.578z",
  camera:
    "M152 304c0 57.438 46.562 104 104 104s104-46.562 104-104-46.562-104-104-104-104 46.562-104 104zM480 128h-112c-8-32-16-64-48-64h-128c-32 0-40 32-48 64h-112c-17.6 0-32 14.4-32 32v288c0 17.6 14.4 32 32 32h448c17.6 0 32-14.4 32-32v-288c0-17.6-14.4-32-32-32zM256 446c-78.425 0-142-63.574-142-142s63.575-142 142-142c78.426 0 142 63.575 142 142s-63.573 142-142 142zM480 224h-64v-32h64v32z",
  music:
    "M480 0h32v368c0 44.183-50.145 80-112 80s-112-35.817-112-80c0-44.184 50.145-80 112-80 31.342 0 59.671 9.2 80 24.020v-184.020l-256 56.889v247.111c0 44.183-50.144 80-112 80s-112-35.817-112-80c0-44.184 50.144-80 112-80 31.342 0 59.671 9.2 80 24.020v-312.020l288-64z",
  play: "M490.594 80.054c-71.816-10.325-151.166-16.054-234.593-16.054-83.43 0-162.778 5.729-234.597 16.054-13.765 53.863-21.404 113.375-21.404 175.946 0 62.57 7.639 122.083 21.404 175.945 71.819 10.326 151.168 16.055 234.597 16.055 83.427 0 162.776-5.729 234.593-16.055 13.766-53.862 21.406-113.375 21.406-175.945 0-62.571-7.64-122.083-21.406-175.946zM192.001 352v-192l160 96-160 96z",
};

class IconBehavior extends Behavior {
  onCreate(shape, data) {
    var scale = data.size / 512;
    const path = Outline.SVGPath(iconMap[data.icon]);
    shape.fillOutline = Outline.fill(path).scale(scale);
  }
}

const Icon = Shape.template(($) => ({
  left: $.left,
  top: $.top,
  width: $.size,
  height: $.size,
  skin: new Skin({ fill: $.color }),
  Behavior: IconBehavior,
}));

let TestApplication = Application.template(($) => ({
  skin: blackSkin,
  style: textStyle,
  contents: [

    Icon({ icon: "home", left: 0, top: 100, size: 32, color: "white" }),
    Icon({ icon: "pencil", left: 40, top: 100, size: 48, color: "green" }),
    Icon({ icon: "camera", left: 90, top: 100, size: 56, color: "yellow" }),
    Icon({ icon: "music", left: 150, top: 100, size: 24, color: "red" }),
    Icon({ icon: "play", left: 180, top: 100, size: 32, color: "purple" }),
  ],
}));

まとめ

SVG描画の方が、リソースサイズも抑えられ、アプリケーションからの描画操作も容易そう。 一方で今回のサンプルではSVGパスをSVGファイルから転機した形となったため、もっと大量のアイコンを扱いたい際の事前orアプリケーション側での仕組みの検討が必要。