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 デザインの仕組みを知るのにも良さそうです。 アイコンだとあっさりした問題が多いので、他にもいくつか似たようなものを作ってみています。

2024年1月2日火曜日

Unicode の部首を一意にする

@marmooo/kanji の部首データを Unicode 全漢字に対応させました。 @marmooo/kanji では画数データは一意になるように設計しているのですが、 Unihan Database の部首は Unicode 15.1 では 183 件の重複登録があり、この解決に苦労が伴ったのでメモを残しておきます。 ちなみに Unihan Database では画数の管理で int を諦めて string を使っているのですが、 情報が不確かな漢字のために int を放棄するのは嫌なので int にしています。 具体的なコードはこちらを参照してください

ちなみに画数も重複データがあるのですが、3件だけなので画数の対処は簡単です。 より正確に言うと kRSUnicode には大量の重複データがあるはずなのですが、kTotalStrokes には重複が 3件だけです。 ただこれも問題のある話で、たとえば「滋」を kRSUnicode で見ると部首以外の画数が 9画と 10画の両方があると登録されていますが、 それなら kTotalStrokes もデータが 2つないとおかしいんですよねえ。 同じような問題は草冠でたくさん見つかるので、真面目にやるなら 漢字データベースプロジェクトのようなチェック が必要になるのでしょうね。 将来的にはやってみるかも知れませんが、IDS も更新しないといけないので時間が掛かります。

部首データの 183件の重複のうち、大半はなぜ複数あるのかよくわからないものが多いです。 康煕字典に記載があるものはなるべくそちらに沿った部首を利用するのが筋だと思います。 康煕字典に記載がないものが問題で、例えば「习」の解決には苦労しました。 「习」は Unihan Database では「冫部」または「乙部」と登録されています。 しかし「习」は 1956年の漢字簡化方案で「習」を簡略化して作った漢字なので、「冫部」または「乙部」の字義は存在せず、その登録でいいの?となります。 ただ漢字簡化方案の元情報が見つからないので、どうしようもない。 本当は部首情報があったのかも知れませんし、元々なくて字体から推測しただけかも知れません。

同じように中国では簡体字を作ったときのことをよく考えないといけない漢字が多々あります。 たとえば日本だと「卧」はそのままの字形で理解されていて Unihan Database では「卜部」です。 ただ中国だと「臥」が元字で、簡化によって「卧」になったので、康煕字典に合わせた部首は「臣部」なんですよね。 康煕字典に記載があるからといって、合わせすぎるのも難しそうです。新しい漢字と古い漢字は分けて考えないといけません。

他には「凬」も難しいですね。字義としては康熙字典の「風部」ですが、Unihan Database だと「⼏部」、文字情報基盤は両方登録されています。 日本だと風が由来でも「⼏部」になることが多いようなので、まあ「⼏部」でいいのかな。 でも中国や台湾でどうかと聞かれると、なかなか難しいところですね。

「厼」のように康熙字典網にも文字情報基盤にも登録されていない漢字も、データがどこから来たのかよくわからないで困ります。 ネットで調べてもほぼ情報がなく、なぜか一番詳しいのが 日本語の Wiktionary みたいなケースがチラホラあります。 当然ながら部首がわかるはずもないので、Unicode に従うしか方法はないでしょう。 ちなみに「厼」の本字は「彌」のようで、そこから様々な異体字と新字体が生まれているようです。 異体字はどれも部首がバラバラでちょっと笑えます。文字を簡単にしようとすると部首が崩壊するんだなあ。 つまり本字以外の部首は深く考えたら負けで、特徴的なパーツを選び、Unicode 配列に従っておけば良さそうです。 また本字以外は部首より、異体字をたどったり、IDS で検索するほうが良さそう。

分類の難しい漢字は、可能な限り理由付けやチェックをしていますが、基本的には使いやすさを考えて Unicode 配列に合わせていますし、それで特に問題ないと思っています。 今のところ Unicode 配列から明確にずれた状態で登録しているのは BMP なら「丬」の「⽙部」だけですね。 SIP では U+20064 の「⼞部」、U+2B820 の「⼷部」、U+2B8D9 の「⽊部」、U+2B8DA の「⽊部」です。 BMP はともかく SIP は部首をどれにするか難しいものがいくつかありますねえ。 日本なら上記以外のものは Uicode 配列に従って良いと思います。 中国だと先に述べたような漢字がちょっとわかりにくい気がしますね。 台湾でも同じような問題があるかも知れません。 ただ Unicode 配列だからこれでいいのだと言い張れば、まあいいかという気持ちになれるのは大きいかな。

追記: Unicode® Standard Annex #38 の画数の説明 を読んでいると、一意にしようとはしているみたいですね。 あまり手を出さないようにしつつなるべく一意にする今の実装方針で良いんじゃないかなという気がします。