zudo-panel-designer docs
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

フォント

テキストはデフォルトで Silkscreen コンテナへ入るため、フォントピッカーは 「インストールされているものすべて」ではなく、シルクスクリーンのサイズと 印刷公差での読みやすさを重視してキュレートされています。テキストレイヤーを Copper や Solder mask へ移すこともでき、その場合は移動先コンテナが有効な マテリアルを決めます。ピッカー自体は 2 段構えで、小さな キュレート済み ドロップダウン(fonts.ts)と、公開カタログ全体へ手を伸ばすためのオプトインの Google Fonts エクスプローラ モーダル(dialogs/font-explorer.tsx)から なります。

セルフホスト、CDN なし

キュレートされたフォントはいずれもバージョン固定された @fontsource/* パッケージで、fonts.ts の先頭で静的にインポートされます。

import '@fontsource/inter';
import '@fontsource/oswald';
import '@fontsource/bebas-neue';
import '@fontsource/orbitron';
import '@fontsource/rajdhani';
import '@fontsource/audiowide';
import '@fontsource/share-tech-mono';
import '@fontsource/archivo-black';
import '@fontsource/monoton';
import '@fontsource/press-start-2p';

これらは OFL ライセンスのフォントファイルで、アプリのビルドにバンドルされています。fonts.googleapis.com などのフォント CDN への実行時リクエストは一切ありません。これはオフライン利用のために重要であり、また、パネル設計ツールがテキストを描画するためだけにサードパーティのネットワーク呼び出しに依存すべきではないからです。

キュレート済みリスト

CURATED_FONTS は、テキストインスペクタのフォント <select> がレンダリングの元にする配列です。

Family特徴
Interニュートラルで非常に読みやすい UI 向けサンセリフ。汎用のデフォルト候補。
Oswald縦長のサンセリフ。新しいテキストレイヤーの デフォルトフォントDEFAULT_FONT_FAMILY)。
Bebas Neue全大文字のディスプレイ縦長書体。力強いパネルラベル向け。
Orbitron幾何学的で、テクニカル/SF 的な性格。
Rajdhaniテクニカル/工業的な雰囲気の幾何学サンセリフ。
Audiowide丸みのある、太く未来的なディスプレイ書体。
Share Tech Mono等幅。数値/技術的なラベル付けに適する。
Archivo Black非常に太いウェイトのサンセリフ。小さなシルクスクリーンサイズでの最大限の可読性。
Monoton太いアウトラインスタイルのディスプレイ書体。
Press Start 2Pピクセル/8bit スタイルのディスプレイ書体。

共通しているのは、太い・幾何学的・等幅の書体であること、つまりシルクスクリーン工程で小さく細く印刷されても読みやすさを保つ書体です。繊細なセリフや細いウェイトのサンセリフは、一般的なパネルラベルのサイズではにじんだり完全に消えたりしてしまいます。

テキストインスペクタのフォント <select> では、Google Fonts エクスプローラ(後述)でスターを付けたキュレート済みフォントが、 の接頭辞付きでリストの先頭にソートされます。ネイティブの <select> は自前のスターコントロールを描画できないためです。

必要に応じたフェイスの読み込み

モジュールと一緒に eager に読み込まれるのは、キュレート済みセットの 定義@font-face の CSS)だけです。実際のフォントファイルのバイト列は、初回使用時に ensureFont(family, sampleText?) を通じて遅延読み込みされます。

export function ensureFont(family: string, sampleText?: string): Promise<void>

ensureFont(family, sampleText) の組ごとに冪等です。キュレート済みまたは CSS の汎用フォントについては family ごとに document.fonts.load() を一度だけ起動します(バンドルされたファイルは 1 つしかないため sampleText は関係ありません)。それ以外は下記の loadGoogleFont を経由し、family サンプルの組ごとに追跡されます。同じ Google Font を共有していても異なるスクリプトを描画する 2 つのテキストレイヤー(例えば一方はラテン文字、もう一方は日本語)は、それぞれ独自のグリフレンジの取得が必要になるためです。進行中の Promise はキャッシュされ、フェイスが実際に使える状態になった時点で解決します。呼び出し元は 2 か所です。

  • テキストツール。新しいテキストレイヤーを置いた直後。レイヤー自身の内容をサンプルとして渡します。

  • テキストインスペクタ。ユーザーが選択中のレイヤーのフォントを変更したとき——Font Explorer からの変更も含みます。

どちらの呼び出し元も .then(() => ctx.requestRepaint()) を連結するため、本物のグリフが用意でき次第、キャンバスがそれで再描画されます。それまでの間、レンダラーの ctx.font = ... はブラウザのフォールバックフェイスで描画しますが、これはエラーではなく正常で無害な一時状態です。

ensureFont は呼び出し元を例外で止めたりハングさせたりすることは決してありません。family の読み込みに失敗しても、あるいは実行環境に FontFaceSet API がまったくない場合(jsdom のデフォルトのテスト環境など)でも、Promise は解決します。ただ「ready」とマークされないだけで、呼び出しが何かをブロックする代わりに、フォールバックフェイスがそのまま描画され続けます。CURATED_FONTS になく、CSS の汎用キーワード(serifsans-serifmonospacecursivefantasysystem-ui——レガシー/インポートされたレイヤーの組み込みフォールバックであり、実際に取得すべきフォントではありません)でもない family は、Google Font として扱われ loadGoogleFont へルーティングされます。

Note

レイヤーの fontFamilyCURATED_FONTS に対して検証されません。手編集した JSON のインポート、デモドキュメントの汎用的な sans-serif、あるいは(今では)Font Explorer で選んだ family も、黙って差し替えられるのではなく保持され、インスペクタの <select> に合成された追加オプションとして表示されます。インスペクタ → Textを参照してください。

Google Fonts エクスプローラ

テキストインスペクタの Browse Google Fonts… ボタン(テキストレイヤーが選択されているときは、コマンドパレットBrowse Google Fonts コマンドからも到達できます)は font-explorer を開きます。バンドルされた Google Fonts カタログ(data/google-fonts-catalog.json — family、category、variants、そして Google が family ごとに公開している言語/スクリプトの subsets)に含まれる 1,942 family すべて を閲覧できるモーダルです。

  • 検索 — テキストボックスが、family 名に対する部分一致でフィルタします。

  • カテゴリフィルタAllSans SerifSerifDisplayHandwritingMonospaceJapanese のトグルボタン。同時にアクティブになるのは 1 つだけです(アクティブなものをもう一度クリックすると All に戻ります)。

  • subsets による日本語フィルタJapanese カテゴリは手で選んだ family の一覧ではなく、カタログ自身のデータから subsets.includes('japanese') としてライブに導出されます(1,942 family のうち 68 が該当します)。これを選ぶと、デフォルトのプレビューサンプルも "The quick brown fox" から "こんにちは日本語" に切り替わります。日本語フォントのラテン文字グリフは、たいてい二の次だからです。

  • プレビューテキスト — 編集可能なフィールドで自分のサンプルを入力できます。一度入力すると、カテゴリを切り替えてもデフォルトへは戻らず、そのまま保持されます。

  • お気に入り優先の並び順 — お気に入りに入れた family は、キュレート済みドロップダウンと同様、フィルタ/検索結果の先頭にソートされます。

  • ページングと遅延読み込みのカードグリッド — 1 ページあたり 60 枚のカード(PAGE_SIZE)。スクロールすると IntersectionObserver のセンチネルで次のページが取得されます。各カードは、実際に表示領域の近くまでスクロールされたときにだけ、自身のフォントファイルを遅延読み込みします(もう 1 つの IntersectionObserver、root margin 100px)。そのため、ダイアログを開いた瞬間に約 2,000 件のフォントリクエストが一斉に発火することはありません。読み込みスピナーは、取得が開始から 200ms(SPINNER_DELAY_MS)経ってもまだ保留中のときにだけ表示されます。キャッシュ済み/高速な読み込みでは、スピナーが一瞬も表示されません。

  • フォントの適用 — カードの名前/プレビューをクリックすると、その family が対象のテキストレイヤーにコミットされ(1 つの undo エントリ)、ダイアログが閉じます。すでに適用されている family をクリックした場合は no-op です(幽霊のような undo エントリを積まないよう、スキップされます)。現在適用中の family のカードはハイライトされます。

  • スター付け — 各カードの角にある / のトグルボタンで、その family をお気に入りに追加・削除できます。適用するかどうかとは独立しています。

お気に入り

use-font-favorites.ts は、スターを付けた family をプレーンな文字列配列として zpd.font-favorites.v1 という localStorage キーの下に永続化します。コンポーネントごとの state ではなく、単一のモジュールレベルのストア(useSyncExternalStore)です。そのため、Explorer でフォントにスターを付けると、キュレート済みドロップダウンにも即座に反映され、別のタブからの storage イベント(あるいはストレージをクリアするテスト)は、セットをライブで再読み込みします。この背後にアカウントの仕組みはありません——オートセーブと同じ、ブラウザローカルの範囲です。

Google Font の読み込み

google-font-loader.ts は、実際に必要になったときにだけ family をリクエストします。fonts.googleapis.com/css2?family=…&display=swap を指す <link rel="stylesheet"> を注入し(regular ウェイトのみ——TextLayer にはフォントウェイトのフィールドがありません)、スタイルシートを待ってから、リクエストされたサンプルテキストを渡して document.fonts.load() を呼びます。unicode-range でサブセット化されたフェイス(CJK フォントによくある、スクリプトごとに別ファイルになっているもの)が、デフォルトのラテン文字範囲だけでなく、実際に描画されているグリフを本当に取得できるようにするためです。10 秒のタイムアウト(FONT_LOAD_TIMEOUT_MS)が、呼び出し元がどれだけ待つかの上限になります——それを過ぎると Promise はとにかく解決し、フォールバックフェイスがそのまま描画され続けます。family ごとのスタイルシートリクエストは重複排除されますが、すでにリクエスト済みの family に対して 異なる サンプルを必要とする 2 番目の呼び出し元も、独自の document.fonts.load() 呼び出しを得るため、そのグリフレンジは実際にリクエストされます。