Claude Codeで/logoスキルを5つの誤りから作る — SVGパス、正規表現のバウンディングボックス、間違った漢字

AIエージェントと一緒に、このブログのロゴとコード用フォントを作り直した記録です。壊れたSVGの字形、正規表現で計算したバウンディングボックス、間違った漢字、そしてそこから生まれた/logoスキルを扱います。

更新

白いカードにテラコッタ色のClaudeの星形ロゴと黒のロゴタイプ
目次

はじめに

長いあいだ気に入っていなかったロゴを、ようやく直すことにしました。このプロジェクトが初日に自動生成したテキストだけのマークで、それ以来、このマークをどうするか誰も決めていませんでした。

AIエージェントとロゴをデザインするには、何ラウンドも重ねる必要があります。エージェントが候補を出し、私が見て、削り、その理由を伝え、もう一度回す。知りたかったのは、このループをClaude Codeのスキルとして書き残す価値があるかどうかでした。

エージェントはラウンドを重ねるなかで5回、自信満々に間違えましたが、どれも無駄だったとは思っていません。5つの誤りのうち4つは、それぞれルールを1つ残しました。そのルールは今では/logoスキルと3本のスクリプトになり、次のマークの出発点になっています。

公開したのは、桜の字を入れた朱色の印と、その隣にFrauncesで組んだ「oharu」、そして記事ページだけで読み込むJetBrains Monoです。

デザインのないブログを、そのまま使い続けていた

ファビコンはこうなっていました。

以前のファビコンを3つのサイズで角の丸い青い四角の中央に白い小文字の o が入ったマークを、128・32・16ピクセルで並べたもの。文字はどのサイズでも判別でき、16ピクセルでは中央に小さな明るい形のある青い四角に見える。ooo128px32px16px
角の丸いrectと、Helvetica,Arial,sans-serifを指定した<text>要素1つだけでできたマークを、ファビコンが使われる3つのサイズで並べています。文字の形は、訪問者のOSが用意したフォントで決まります。

ヘッダーにはマークすらなく、サイトタイトルがテキストで置いてあるだけでした。

タイポグラフィも同じ状況でした。

src/styles/global.css
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, …;
--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, …;

システムフォントのスタックだけです。フォントは1つも指定されていませんでした。訪問者は全員、自分のOSに入っているフォントで読んでいたことになります。これは正当なトレードオフです(転送量ゼロ、レイアウトシフトゼロ)。ただし、誰かが判断して選んだ結果ではありませんでした。

誤り1: 1つのアイデアの24バリエーション

最初は幅広さを求めました。アイコンのみ、ワードマーク、両者を組み合わせたマークにまたがる24案を、それぞれ128px・32px・16pxで同時に見られるギャラリーにまとめてもらいました。

24案はそろいました。ところが、すべて同じロゴでした。

8つのアイコン案を並べたグリッド。どれも端の丸い、太く均一な青い線で描かれている。リング、円弧、角の丸い四角、山形

これはシートのうち、アイコンだけの3分の1です。アパーチャ、軌道、ターミナル、円弧、鳥居、スクワークル、プロンプト、分割。1つの絵に名前が8つ付いているだけでした。

どれも均一な線で描いた幾何学的な図形で、円と円弧、一定の線幅、丸い線端でできています。アイコンでもワードマークでも骨格は同じで、その骨格は自動生成されたロゴによくあるデフォルトです。幅を求めたのに、届いたのは同じ案に8通りの名前を付けたものでした。

解決策は、生成する前に実在するデザインを見ることでした。6つのサイトのビジュアルアイデンティティをスクリーンショットに撮り、実際に観察しました。

参照先何をしているかラウンド1でやっていたこと
Linear、Astroベタ塗りで、マークは小さく控えめすべて線で描いており、16pxでは潰れる
Rauno Freiberg、Josh Comeauマークを持たない。書体と、これと決めた1色だけマークは必須だと思い込んでいた
Maggie Appletonディスプレイ用のセリフ体。文章を書くサイトだからテンプレートのデフォルトである幾何学的なサンセリフ
Anthony Fu個人のモノグラムと、手書き風の文字純粋な幾何学からは「個人らしさ」が出てこない

実際のサイトから引き出した制約を与えたので、ラウンド2では本当に違う案が返ってきました。

16案が、エディトリアルなセリフ体、グロテスク、朱色の判子、色を添えた文字、モノグラムに分けて並んでいる。1つではなく5つの異なるアイデアであることが一目で分かる

AIにN案を求めると、1つの分布から取り出したN個のサンプルが返ってきます。

誤り2: 一度もレンダリングされなかった字形

手で描いたSVGの字形には、レンダリングしないと見えないバグが潜んでいます。ラウンド1の字形は、一度もレンダリングされないままギャラリーに並びました。いちばんひどかったのはuの字です。

u(描いたまま)
M9,49 L9,99 A41,41 0 0 0 91,99 L91,140

ペンの動きとして読んでみます。左のステムをx-heightからy=99まで下ろす。ボウルの円弧で(91,99)まで渡る。そこからベースラインまで線を引く。円弧が終わるのはy=99なので、右のステムは99から140までしかありません。上半分がまるごと欠けています。結果として、右側が途中で切れたuがレンダリングされます。

修正では、右のステムを独立したサブパスとして、x-heightの高さいっぱいに描きます。

u(修正後)
M9,49 L9,99 A41,41 0 0 0 91,99
M91,49 L91,140

「oharu」の文字が2つ。上は最後のuの右のストロークが高さの半分で止まっている。下は同じ語で、ストロークが隣の文字と同じx-heightまで伸びている

残る2つのバグも、同じ種類の誤りでした。

  1. y=160に置いて0.26倍に縮小したサブタイトルは、アセンダが160 + 0.26 × (−700) = −22の位置に来ました。これはサブタイトルの上にある語のベースラインよりも、さらに上です。そのためt、h、b、lが名前の文字を突き抜けていました。
  2. 高さ700ユニットの文字の隣に置いたマークは、高さが220ユニットしかありませんでした。ロゴではなく、紛れ込んだ箇条書きの記号にしか見えませんでした。

どれもブラウザで見れば一目瞭然なのに、パスデータを読んでいるだけでは見えません。

誤り3: エージェントが決めつけた漢字

ラウンド2でいちばん強かったコンセプトは判子(hanko)、つまり個人の印でした。名前を押す印は、個人ブログという場の性格によく合います。朱色の地に、1文字を白抜きで入れる案です。

名前が「oharu」なので、エージェントは春(haru)の字を使いました。見た目は良く、あやうくそのまま仕上げてしまうところでした。

ところが、その手前でエージェントが確認してきました。人のアイデンティティに関わる文字を、思い込みのまま黙って入れるべきではないからです。私は訂正しました。「oharu」は桜明です。

この答えで変わったのはグリフ1つではなく、デザインそのものでした。桜明は2文字です。そして16pxのファビコンでは、2文字は判読できません。そのサイズでは、10画の漢字は潰れてただの模様になってしまいます。

朱色の印の案が6つ。それぞれ大きく表示され、さらに32ピクセルと16ピクセルでも並んでいる。16ピクセルでは2文字の案は輪郭のない塊になり、1文字の丸印だけがまだ読める

16pxのレンダリングを見れば答えは出ます。桜明を縦に並べた案は、この名前の判子としては正しいものの、ブラウザのタブではいちばん読めませんでした。輪郭線だけの案は、枠は残っても2文字とも消えてしまいます。そこで印は丸型で1文字の桜にしました。2文字ではなく1文字にするという判断は、好みではなく画数で決まりました。

誤り4: 正規表現で計算したバウンディングボックス

本番用のアセットでは手描きの文字をやめ、fontToolsとHarfBuzzを使って書体のアウトラインを焼き込みました。ワードマークにはFraunces、桜にはNoto Serif JPを使っています。正しいカーニングと線の強弱が、そのままパスデータとして固定されます。そのため、ワードマークはウェブフォントのリクエストもフォールバック書体のちらつきもない図形として配信されます。

グリフを配置するには、実際にインクが乗る範囲が必要です。outline.pyはパス文字列から数値をすべて抜き出し、x、yが交互に並んだ組として扱っていました。ところがSVGPathPenはHとVの短縮形を出力し、これらのコマンドが持つ座標は1つだけです。水平線が1本あるだけで、それ以降のすべての数値でxとyの割り当てが入れ替わります。

パス M0,0 H700 L720,50
実際の点 (0,0) (700,0) (720,50)
正規表現の組 (0,0) (700,720) (50, …)

このバグは、ギャラリーで何度見ても「少しずれている」程度にしか見えず、そのまま見過ごされました。疑いようがなくなったのは、幅がおよそ3200ユニットになるはずのロックアップが、1802で出力されたときです。

修正は、文字列の解析をやめてライブラリに測らせることです。

outline.py
from fontTools.pens.boundsPen import BoundsPen
bounds = BoundsPen(glyph_set)
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
xform = (scale, 0, 0, -scale, (x + pos.x_offset) * scale, 0)
glyph_set[name].draw(TransformPen(pen, xform))
glyph_set[name].draw(TransformPen(bounds, xform)) # same transform

ワードマークの実際の境界は2456.4 × 758.6でした。正規表現はそれまでずっと、自信ありげに別の値を返していたわけです。

正規表現はクラッシュしませんでした。もっともらしい出力を返し、人間がそれをちらっと見て承認したのです。それも2回。

誤り5: 全ページに入ったプリロード

コードブロックにも専用の書体を用意しました。技術ブログで読者がいちばんじっくり読むのはコードブロックです。それなのに、このブログではMenloで表示されていて、本文と同じく「誰も決めていない」状態でした。

選んだのはJetBrains Mono VariableのLatinサブセットだけで、サイズは40KBです。URLでプリロードできるよう、固定のパスにセルフホストしました。コードはLatin文字で書かれます。そのためコード用にCJKフォントは取得せず、CJKのフォントスタックもそのままです。

プリロードは共有レイアウトに入れました。それが間違いだった理由は、ブラウザが教えてくれました。

The resource http://localhost:4321/fonts/jetbrains-mono-latin.woff2 was
preloaded using link preload but not used within a few seconds from the
window's load event. Please make sure it has an appropriate `as` value and
it is preloaded intentionally.

トップページ、タグ一覧、CJKの2つのインデックスページには、コードが1つもありません。どのページも40KBをダウンロードし、まったく使っていませんでした。そこで、ArticleLayoutがpreloadMono propを渡したときだけプリロードを出力するようにしました。結果は次のとおりです。

ページrel="preload"のリンク数
dist/index.html0
dist/ja/index.html0
dist/tags/index.html0
dist/blog/<article>/index.html1

コンソールの警告1つが、実際の削減につながりました。5つの誤りのうち、ツールが自分から知らせてくれたのはこれだけです。それ以外はすべて、レンダリングするか、計測するか、質問して初めて表に出ました。

ラウンドから生まれた/logoスキル

スキルが必ず守るルールと、それぞれの由来は次のとおりです。

ルール由来
何かを生成する前に、実在するデザインを研究する1つの分布から出てきた24案
誰かに見せる前に、レンダリングして自分の目で確かめるステムが半分しかないu
16pxで判断するブラウザのタブで潰れた桜明
相手についての思い込みを成果物に入れない春
手描きの字形ではなく、実在する書体のアウトラインを使う手組みの図形と、それを測った正規表現
色は理論ではなく、並べて比べて決める赤い印の隣に置いた青いワードマーク
どのゲートでも、選ぶのはユーザーループそのもの

スキルには、案を提示し、理由を添えて推薦し、そこで止まるように書いてあります。意見は役に立つが、決めるのはあなたではない。本記事のどのラウンドも、私が選ぶゲートで終わっています。5つの誤りの代償が半日で済み、推測を土台に仕上げたアイデンティティを丸ごと捨てずに済んだのは、そのためです。

機械的な作業は3本のスクリプトが受け持ちます。

  1. gallery.mjsはコンタクトシートを組み立て、ローカルのポートで配信します。
  2. outline.pyはfontToolsとHarfBuzzを使い、書体のアウトラインをパスデータに焼き込みます。
  3. build-brand-lockup.mjsは、ファビコンとヘッダーコンポーネントを出力する実例です。

コンセプトとシートは作業用の一時ディレクトリに書き出し、リポジトリには入れません。デザインのラウンドで出る案はほとんどがボツで、ボツ案はリポジトリに置くものではありません。

公開したもの

桜を入れた朱色の丸印と、その隣にFrauncesで組んだ「oharu」です。SVGは1つではなく2つに分けて配信しています。印は、ワードマークのインクの高さより意図的に低くしてあります。そのため、ロックアップ全体を正方形に切り抜いても、狭い画面向けの版は作れません。

画面幅が50rem未満のときは、印だけを表示します。これで古いレイアウトの問題も1つ解決しました。スマートフォンでは、テキストのワードマークが検索コントロールと場所を取り合っていたのです。

色の判断の1つは、候補を並べて見比べることで決まりました。サイトのアクセントカラーは#1d4ed8、印は#d8452aです。この2色は色相環でほぼ反対側にあり、青いワードマークを赤い印の隣に置くと、目に見えて喧嘩します。ワードマークにはテーマの文字色を使い、青はリンクだけに残しました。これで2色が隣り合うことはありません。

ワードマークのパスデータ3569バイト(座標を整数に丸め、5145バイトから削減)
印のパスデータ1677バイト
favicon.svg1878バイト
JetBrains Mono、Latinサブセット40KB、記事ページのみ
/ja/の記事でのウェブフォントリクエスト1件(JetBrains Mono)、CJKフォントはなし

まとめ

誤りは5つ。そのどれも、コードについて推論するだけでは見つかりませんでした。

ありきたりなコンセプトがありきたりだと分かるには実在する参照先が、壊れた字形にはブラウザが、間違った漢字には質問が、バウンディングボックスのバグには気づけるほど不自然な数値が、無駄なプリロードにはコンソールが必要でした。

この5つこそが、ロゴ作りを1回のプロンプトで済ませず、ループとして回す理由です。いまや仕上がった24案を出すのに数分しかかかりません。だからこそ、量を幅と取り違えたり、推測の上に作られた自信満々の成果物を受け入れたりしやすくなっています。

1回のプロンプトにはなくてループにあるのは、レンダリングすること、計測すること、そしてデザインが私しか知らない事実に左右されるときに質問することです。

5つのどれも、当時は無駄だと感じませんでしたし、今読み返しても無駄には見えません。次のマークは、白紙のプロンプトではなく、7つのルールと3本のスクリプトから始まります。

この記事をシェア