2024年4月22日月曜日

フォントを様々な形式に変換する fontconv を作った

フォントを様々な形式に変換する fontconv を作りました。 .ttf, .otf, .svg, .woff, .woff2 の相互変換とサブセット化に対応しています。.eot も出力だけは対応しています。

fontconv

なぜ作ったかというと、フォントを Web 上で利用するための woff2 最適化に苦労したからです。 私の場合は以前 ttf2svg を自作したので、 ttf2svg -> svg2woff2 の2回で woff2 は最適化できるようにしていたのですが、 巷のアイコンは woff2 形式でしか配布されていないことがあることに気付きました。 これがなかなか強敵で、woff2 をロードして最適化した後、 woff2 へ再変換できるライブラリが見つかりませんでした。 具体例を上げれば Material Icons の最適化は大変で、びっくりしました。 さすがにこれはおかしいなと思い、様々な形式に対応したフォントコンバーターを作りました。

ちなみに類似ライブラリとしては以下があることは認識しているのですが、 依存性が更新されていなかったり、手元で動かないのが厳しかったです。 どうやら Node.js に関してはきちんと動くものは作り直すしかないみたい。 pyftsubset は動きますが、Python でインストールがやや面倒で、異体字に対応していない問題があります。 良いものないなあ…と思っていたら、公開直前に tdewolff/font が出てきました。 拡張子ごとにパーサをフル実装しているようで凄いですが、私のおきらく実装でも選択肢が増えるのは良いんじゃないかな…。

OTF からの変換が難しすぎる!!!

実装上で一番厳しかったのは OTF を他の形式に変換するライブラリがなかったところです。 TTF と WOFF/WOFF2 の変換ライブラリは山ほどあるんですが、OTF を他形式に変換するライブラリがまったくありません。 フォントの最適化には OTF 出力しかできない opentype.js を使っていますが、 代替になるライブラリは現状存在しないので、OTF をベースに実装することは絶対条件です。 最初は、唯一きちんとした otf2ttf 実装に見えた fonteditor-core を試してみました。 しかし glyph-name の情報をバッサリ切り捨ててしまう実装だったので、とてもフォント最適化には使えないとわかりました。

その後色々なライブラリを確認してみたものの、otf2ttf で動くものは見つかりませんでした。 結局のところ今までのように ttf2svg を使って font→svg→ttf で変換するのが、一番仕様がわかりやすくて良いとわかりました。 ただ ttf2svg の実装も色々と問題があるのがわかっていたので、 良い機会なのでバッサリ書き直し、fontconv と同じオプションで動くようにしました。

EOT 形式について

いまは TTF/OTF/WOFF/WOFF2 が標準的なフォント形式ですが、昔は EOT という IE でしか動かないフォント形式があったらしいです。 もはや誰も使っていないのでサポートする必要はない気がしますが、一応サポートしてみようと思いました。 しかし案の定これも誰も作っていない。 fonteditor-core に eot2ttf が付いていたので試してはみましたが、 バグってて動かなかったので EOT 形式の入力は無理そうです。 自分で eot2ttf を作れば何とかなりますが、もはや誰も使わない EOT 形式のコードを書く気はさすがに起きなかったです。 ChatGPT 3.5 が作ってくれるかなと期待して雑に依頼してみたりもしましたが、案の定できませんでした。 AGI はまだまだ遠いか…。4 なら作れたりするのかな。

使い方

実装で色々詰まりましたが、きちんと完成はしました。 fontconv を使えばフォントの最適化は簡単に実行できます。 まずは以下のコマンドで CLI をインストールします。
deno install -fr --allow-read --allow-write --name fontconv \
https://raw.githubusercontent.com/marmooo/fontconv/main/cli.js
例えば Material Icons から特定のコードポイントだけを抽出して最適化する場合、使いたいコードポイントを行区切りでリスト化し、codepoints.lst として保存します。 その後、以下のコマンドを打てば完了です。
fontconv --code-file codepoints.lst in.woff2 out.woff2
ファイルを用意するのが面倒なときは、文字列やコンマ区切りで最適化ができます。
fontconv --text abcdef in.ttf out.woff
fontconv --code 0x61,98 in.otf out.eot


問題点

これでもうフォント最適化には困らないで済むわ〜と思ったら Material Icons は ligature (合字) を使っているので、合字の最適化ができませんでした。 というより fontconv は SVG フォントを経由して変換をしていますが、SVG フォントには合字がありませんからサポートは困難と言えます。 とはいえ svg2ttfunicode 属性を拡張して ligature に対応している ので、 ttf2svg で ligature 属性を考慮すればサポートできるような気はします。 あまり実装したくないのですが、opentype.js でも ligature の解析は対応しているみたい

ひとまず初期リリースの時点においては、Material Icons のように合字を使ったフォントの最適化については「合字を使わない」が結論になりそうです。 合字の定義だけでファイルサイズが増えてしまいますから、コードポイントを使って最適化し、HTML からもコードポイントで指定するのが良さそうです。 Bootstrap Icons のようにクラス名を使ってくださいな

fontconv を作ってみて、改めてフォントを扱うのは難しいなあと感じました。 他の課題としては、現時点では異体字 (IVD) をサポートしていません。 これは opentype.js 1.3.4 が対応していないからですが、 最新版では対応しているようなので、opentype.js の更新に期待です。

初めて denoShims のバグを踏んだっぽい

さて npm に公開しようと思ったら、どうやら初めて denoShims のバグを踏みました。 たとえば以下は Deno 上ならどちらも動きますが、dnt のテストでは上だけ動かないです。 数行のコードでも発生するとなれば、おそらく Deno.readFileSync() の shims がきちんと動いてないです。
import { parse } from "npm:opentype.js@1.3.4";

Deno.test("Input check", async () => {
  const uint8array = Deno.readFileSync("test/format-check.otf");
  const font = parse(uint8array.buffer); // dnt でなぜかパースに失敗
});
readFile() なら動きます。さすがにバグだよなこれ…。
import { parse } from "npm:opentype.js@1.3.4";

Deno.test("Input check", async () => {
  const uint8array = await Deno.readFile("test/format-check.otf");
  const font = parse(uint8array.buffer);
});
まあ動いたから良いか…。

2024年4月6日土曜日

えもじパズルを作った

えもじパズル を作りました。 完成図と見比べながらパーツを並び替えて、福笑いみたいに絵文字を作るゲームです。 細部まで見比べる力を鍛えるので、特に美術や算数や漢字の学習へ応用しやすいかな。



最近は アイコン点描写漢字点描写 を作っていて、 絵文字点描写も作っていたのですが、ボツ案となりました。 絵文字は (1) 複雑すぎる、(2) 直感的な描画ではないことが多い、の2つが厳しかったです。 1 は左右に絵文字を表示することで自然と表示サイズが小さくなるため、 思っていたより点が密集してしまい、複雑に感じることがわかりました。 スキップアルゴリズムを付ければ複雑性は緩和できますが、 パーツの重なりを活かして作成されていることが多いので 点の直感性とズレていることが多く、本質的な厳しさを感じました。 描画の直感性は点つなぎだと誤魔化せるけど、点描写は誤魔化せないからなあ。

そこで絵文字を使った他のゲームを作ろうと思って、えもじパズルを作ってみました。 アイコンや漢字ではできないけど、絵文字ならできるタイプのゲームです。

作り方

作り方は割と簡単で、SVG のパーツを分解して drag & drop 可能にします。 小さなピースは動かしにくいので雑に計算した面積が全体の 5% 未満なら動かせないようにしたり、自動配置してプレイ感覚の向上に努めました。 あとはユーザにパーツを移動してもらいながら絵文字を作ってもらい、一致チェックします。 一致チェックはシンプルで、canvas を画像化してピクセル単位で比較します。 座標をもとにチェックする方法もありそうですが、重なりの解析が大変だしなあ。 80% くらいの一致率は簡単に出てしまうので、85% 以上の一致率があれば正解としました。 簡単な問題は 90% くらいが良いのですが、難しい問題は 90% を出すのがかなり難しい…。

パズル部分の作り方は簡単なのですが、様々な絵文字で動くようにするのが一苦労でした。 たとえば drag & drop のためにはピースごとに transform 属性を除去して新たな transform 属性を付与する必要があります。 ブラウザだと階層的な transform を getCTM() で解析できるので良いですが、非ブラウザだと解析はなかなか大変で対応ライブラリはないと思います。 getCTM() を使っても元々の path データに transform: matrix() があると移動用の transform 属性と混じってしまうので、まだ面倒なことあります。 これは元の transform の matrix を逆変換しながら座標変換を行い ( 参考1参考2 )、 正しい移動距離を算出し、matrix を更新する必要があります。意外と難しい。 DOMMatrix や SVGMatrix、getCTM() は初めて使いましたが、なかなか便利です。 やはりブラウザの API は最強です。

2024年3月30日土曜日

drop-inline-css でインライン化レベルを最適化する

CSS 最適化ツール drop-inline-css を更新し、drop-inline-css でインライン化レベルを最適化できるようにしました。 一部分だけ不要 CSS をチェックしたり、一部分だけはチェックをせずそのまま inline 化できたりします。 めちゃくちゃ便利な気がしますが、まあ私だけかな…。

drop-inline-css

どんなことができるなったかというと、こんな感じの HTML を、
<html>
  <head>
    <link class="drop-inline-css" rel="stylesheet" href="inefficient.css"></link>
    <link class="inline-css" rel="stylesheet" href="efficient.css"></link>
    <link rel="stylesheet" href="keep.css"></link>
  </head>
  <body>
    <p>styled</p>
  </body>
</html>
こんな感じに inline 化します。drop-inline-css クラスは HTML 構造を見ながら不要な CSS を drop するように最適化し、inline-css クラスはそのままインライン化し、keep.css には適用しないようにしました。
<html>
  <head>
    <style>p { text-decoration: underline; }</style>
    <style>pre { color: red; }</style>
    <link rel="stylesheet" href="keep.css"></link>
  </head>
  <body>
    <p>styled</p>
  </body>
</html>
なぜこのような実装を入れたか。 CSS は最適化を考えた時、最適化の可能性は大まかに 3種類に分けられます。
  1. 静的な HTML から要不要が判断できる CSS
  2. 動的な HTML ではあるものの事前記述で要不要が判断できる CSS
  3. JavaScript をよく見ないと要不要が判断しにくかったり設定が面倒な CSS
私が作っているアプリの例を上げると、以下が例として当てはまります。
  1. Bootstarp の CSS だけで動く大部分のコード
  2. Bootstrap の JavaScript が必要なコンポーネント
  3. autocompletr や simple-keyboard などのライブラリ
1-2 は drop-inline-css を適用し、3 は平凡に inline 化するのが良いと思われます。 うまく最適化すれば Bootstrap などは 1/10 以下のサイズで利用できます。

また上記 2-3 の CSS は遅延ロードが可能で、その最適化は 3種類に分けられます。
  1. <link rel="stylesheet" href="base.css" media="print" onload="this.media='all';this.onload=null;">
  2. template タグ内で style を load
  3. template タグ内で link タグを load
1 は昔なら有名なテクニックでしたが外部ファイルが必要で小さな CSS に適用しにくいです。 今は Shadow DOM がサポートされたので、 2 が処理を後回しにしながら inline 化でき小さな CSS に適用しやすいです。 3 なら通信も後回しにできるので大きな CSS にも適用できる利点があります。

こうして考えてみると、CSS の最適化には (1) drop-inline-css する、(2) drop-inline-css した上で遅延ロード1する、 (3) inline 化する、(4) 何もしないの 3つが考えられ、それらを選択できる必要があります。 これまで drop-inline-css では 1,2 ばかり考えていたのですが、 2 は実装がやや面倒なので deprecated でも良さそうで、 代わりに 3,4 ができると嬉しいなと思って実装してみました。

具体的には link タグに drop-inline-css クラスがある時には drop-inline-css し、 inline-css クラスがある時には平凡にインライン化し、何もないときは何もしないようにしました。 この改良によっていくつかのアプリの実装がかなりシンプルになるとわかりました。 破壊的変更で更新が大変でしたが…。

2024年3月16日土曜日

アイコン点描写と漢字点描写を作った

アイコン点描写漢字点描写 を作りました。



作り方

点描写は繋ぐことができる点と点を制限しながら、繋いでいくゲームになります。 実現するためには、まず Web 上ですべての SVG 描画要素を path に変換します。 SVG の下に Canvas を配置し、タップ時だけ線を書けるようにします。 これは SVG 全体にタッチイベントをセットしておいて、Canvas へ転送すれば良いです。 他の実装方法もある気がしますが、ぷちぷち点を繋げるので、この方法が今のところはいい感じに思えています。 次に、隣接する path 内部の 2座標が線で結ばれた時に、SVG の線を復元します。 線の復元は元の path データのコマンドをすべて M/m (move) にしておいて、 結ばれた座標だけ元のコマンドに戻すことで実現できます。

上記が作り方の基本ですが、実際はもう少し必要です。 たとえば点と点の隣接状態は Z/z/M/m コマンドを解析しながら行なう必要があり、 重複する座標に点があったときのイベント処理を考慮する必要があります。 この 2つがかなり面倒でした。

アイコンの調整

対象アイコンは アイコン点つなぎ漢字点つなぎ と同じものが使えます。 ただアイコンのほうはやや苦労しました。 具体的には アイコン点描写 は解答を先に表示しているので、プロットするときと差があると面白くありません。 このとき、透明な外枠を付けている Tabler Icon の違和感が大きい問題が起きました。透明な枠はプロットする必要ないですからね。 アイコンセットを変えることも検討して結構色々なアイコンセットを試したのですが、 透明な外枠を付けているアイコンが意外と多かったので、Tabler Icon は中身を少し弄ることで対処しました。 こういう細かな調整が必要な時もあるので、アイコンセットはライセンスが緩くないと難しいなと感じました。 今のところ MIT / Apache-2.0 / CC-BY / SIL-OFL-1.1 に限定しています。

逆に対処不能で諦めたのは Solar Icon Set でした。 基本的にはいい感じなのですが、複雑なアイコン (ex: Astronomy/Rocket.svg) を描画する時に、不要なデータが多すぎて点を繋ぐことが困難でした。 まあ SVG はどう作っても良いものなので、最小のポイントデータで作成されている保証はないですからねえ。 画像から枠を抽出しながら SVG に自動変換したもののほうが最小化しやすかったりするかも知れません。 仕方ないので再びアイコン探しを頑張り、Streamline Core (line) が良さそうだったので利用してみました。 たいていのアイコンはやはり Star 数をベースに探していくと、最適化されていることが多いかな。 Material Icons とか Bootstrap Icons などはがっつり最適化されてます。


個人的には結構いい感じに完成したアプリなんじゃないかと思っています。 前作の アイコン点つなぎ では線を引く仕組みを作るのは困難でしたが、 これなら線を引く練習にも使えます。 ただしスマホでプレイするのは難しいので、タブレットか PC でプレイするほうが良さそう。 またほとんど同じ位置に点がある時は二回なぞらないといけないのは注意点でしょうか。 こういうのをどこまでチェックするかは割と難しい問題です。

2024年2月2日金曜日

えもじ点つなぎを作った

えもじ点つなぎ を作りました。 アイコン点つなぎ はアイコンを使っているので、モノクロでシンプルなものがほとんどでした。 逆に 漢字点つなぎ は難しすぎる子もいるでしょう。 絵文字ならカラフルで難易度はほどほどのものが多いので、新鮮なものになります。 プレイ感覚は絵を書いている感覚に近くなり、デジタルデータの仕組みを学ぶのにも使えます。



今のところ、カラフル版は Noto Emoji, Twemoji, Fluent Emoji (Color)、Blobmoji、Emoji Two (Color)、 モノクロ版は Noto Emoji Twotone, Fluent Emoji (High Contrast)、Emoji Two (Twotone) をサポートしています。 初期リリースでのサポート数は 2万個 ほどです。

作り方

アイコン点つなぎ漢字点つなぎ では数字の再配置アルゴリズムだけで十分に遊べるものになりましたが、 絵文字は絵の複雑性に一貫性がないため、問題によっては再配置で対処できないケースがたまにあります。 同じ地点を何度も通ったり、局所的に複雑なケースがあり得るということですね。 再配置で対処できない点を一時的に消すのが楽そうと思いましたが、 局所的に問題が発生したときにわかりにくいし、紙に印刷できないのが気になりました。 そこで密集度の高すぎる領域をスキップする実装を取り入れました。

このスキップアルゴリズムでは、点から次の点への距離を見て、全体の構図の中で無視できるくらい短い時はスキップします。 ピクセルデータをベクトルデータにする時などにも使えそうなアルゴリズムですね。 これでレスポンシブ対応もできますし、難易度調整などもできます。 ただ完璧に重複を排除できるアルゴリズムではなく、同じ場所に後から10回点を打たれたら重複する可能性があります。 とはいえさすがに 10回も同じ場所にプロットすることはほとんどないと思いますし、 真面目にチェックするのは処理時間がかなり掛かるので、暫定解はこんなもんでしょう。

2024年1月16日火曜日

漢字点つなぎを作った

漢字点つなぎを作りました。 書き順は考慮する必要がないのでいい感じのフォントさえあれば作れます。 とはいえそこがハードルで、思ったより考えることがありました。



フォント選び

まず教育用途のアプリでは漢字のフォントをどうするか問題があります。 教育用途では教科書体が使われるのですが、いまだに教科書体の無料フォントはありません。 ただ最近クレーOne というのが出てきて、多少の課題はありますが教科書体として使えそうだったので使ってみました。 しかし点つなぎで遊ぶには点の数がちょっと多過ぎると感じました。 フォントはアイコンや絵文字と違ってシンプルさを求めているとは限らないですからね。 今回は綺麗さよりシンプルさのほうが大事とわかったので、Noto Serif JP を使うことにしました。 中学以降は教科書体ではなく明朝体が使われるので、 その書き方に慣れるくらいの意味合いで遊べば、まあ問題はないでしょう。

Noto Serif JP 以外に良いフォントがないか、改めて色々なフォントを見ました。 シンプルで良いなあと思うのはゴシック形式の Noto Sans JP で、これはサポートしておきました。 ただ Noto 以外で点つなぎに使えそうなもの、と考えるとなかなかちょうど良いものはない気がしています。

ttf2svg

フォントから SVG への変換は @marmooo/ttf2svg を使っていますが、 細かな調整が必要になったので、ついでに改良しておきました。 具体的には、文字を仕様上表示され得る領域にSVG化するのと、 ぴったり拡大して最大領域に SVG 化するのは微妙に違い、 対応しにくい問題があったので、細かな調整ができるようにしました。

たいていの絵文字やフォントは advancedWidth と linespace = ascent - descent + linegap を見れば、SVG の viewport を決定できます。 ただ漢字にこの条件を当てはめると、特に descent の影響が大きすぎて不自然な余白が生まれます。 とはいえ descent だけを無視する訳にもいきません。 なぜならフォントの作者によって、ascent, descent はバラバラの意味を持っているからです。 そこで units-per-em を使うと、ぴったりになることが多いです。 ただ units-per-em も壊れたデータが入力されていたり、縦横比が壊れていると意味をなさなかったりします。 仕方ないのでオプションで数字調整できるようにしました。

しかしこれでも点つなぎで遊ぶには調整が不足しています。 具体的には、(1) ローマ字でさえ x 座標がマイナスになることがあり、SVG 化で表示が崩れる危険性があります。 (2) また表示するテキスト自体がない文字も多数あるので、細かな除外が必要です。 TTF を SVG 化するのは、細かいところを見始めると思っていたより難しいんだなと思いました。 getBoundingBox() でぴったりのサイズにするのも一つの手ですが、縦横比が崩れるので一長一短です。 これらの問題をチマチマ対処するのは面倒で、JIS 第 4 水準までの漢字に限定するのが楽そうです。 その場合は縦横がシンプルな固定領域で表現されるので、悩む必要がありません。

そんな訳でサポートしているのは約 1万1000種類×2=2万2000個のグリフです。 これだけあれば量で困ることはないでしょう。 アイコン点つなぎと違ってそれなりに歯ごたえがあります。

2024年1月6日土曜日

アイコン点つなぎを作った

アイコン点つなぎ を作りました。



幼児教育では点つなぎやグリッド点つなぎが有名です。 それらは線を書く練習や、簡易的に認識力を向上させる練習として便利ですが、題材を作ったり探すのが大変です。 そこで自動生成してゲーム化すると面白そうだなと思って作りました。

難しいところ

とはいえ一般的な点つなぎを作るのはなかなか難しいです。一番の問題として、点つなぎは点が重複しない特徴がありますが、アイコンはかなりの確率で点の位置が重なります。 他にも SVG は図形が重なる問題が大きいです。 これらの問題を解決するために、すべての数字を表示するのは諦めて path ごとに数字を表示して、ちょっとずつ絵を作るゲームにすることで回避しました。 点つなぎだけでなく SVG の勉強にもなります。 この回避策を入れても重なるケースはままあるので、点を近傍領域へ再配置するようにしてみたところ、まあまあいい感じに動きました。

作り方

path 以外の図形をどうするか、という問題が気になった人もいるかも知れませんが、Web 上ですべての SVG 描画要素を path に変換しています。 これは使いやすいライブラリがなかったので自作しました。 あとは点をつなぐためのの数字の表示ですが、まず svgpath を使って配列の座標にします。 path 配列から text で数字を表示します。 あとは text を順番にタッチしたときに path の線を復元するようなコードを書けば完成です。

ただ実際には SVG の様々な表示形式のサポートが必要です。 たとえば transform の復元、use タグの復元、それに伴った CSS の復元、style 属性の調整、currentColor の調整などなどを行う必要がありますが、なんとかします。 さらに真面目に処理する場合、style タグの解析、animate タグの解析が必要ですが、面倒なので今は使っていないアイコンに限定しました。 もっとも style タグが影響があるのは Arcticons と Carbon Icon くらい、animate タグは Material Line Icons くらいでしょうか。 また数字の表示位置は元の SVG の表示領域を突き抜ける可能性があるので調整が必要です。 そのような細かな処理をきちんと加えていけば、どんな SVG でも動く点つなぎが出来上がります。

アイコンにも色々ある

アイコンをたくさんサポートするのは容量的にも作業的にも大変なので、軽量で有名で使いやすく、多くのアイコンを含むセットだけをサポートすることにしています。 このチェックが意外と面倒で、たとえば Phosphor Icons はアイコンの種類は 1200 ほどで、形状が異なるものが 6種類あります。 形状に関しては、多くのアイコンセットではたいてい fill か outline かぐらいの違いしかないので、すべてサポートするのはあまり意味がありません。 fill なら外枠だけ書けば良いので楽、outline は内枠も必要ですこし難しいとは思う一方で、 黒字を内抜きするような場合は同じ手間が掛かる可能性があるので、細かな分類は大変そうです。 両方サポートすることにしました。 他には sharp, round, bold などの違いがありますが、これは本当に差がないので、一番ファイルサイズが小さくてシンプルなものにしました。 この基準でサポートしているのは、Material Icons、Material Design Icons、Phosphor Icons、Tabler Icons、Bootstrap Icons、Remix Icon の 6つです。

アイコンセットによっては、稀に外枠と内枠を一つの線で書いているもの (Line Icon?) があります。 これが世の中の点つなぎに一番近いもので重要と考えています。 ただそのような書き方をしているアイコンセットは多くありません。 また地道にソースコードを見る以外で見分けが付かないので確認が大変です。 今のところ Line Icon として使えて数が多いものは Iconoir、Solar icon set、Majesticons、Lucide くらいと思っています。 これらは優先的にサポートしました。 Material Line Icons も使えそうでしたが、animateMotion タグの解析が大変そうだったので見送りました。

今のところ 3万個くらいのアイコンをサポートしています。 少し変わった点つなぎと考えても良いですし、IT デザインの仕組みを知るのにも良さそうです。 アイコンだとあっさりした問題が多いので、他にもいくつか似たようなものを作ってみています。