Claude Code 的 /logo skill,是從五次走錯路整理出來的 — SVG 路徑、正規表達式算的邊界框、一個猜錯的漢字

用 AI 代理重新設計這個部落格的 logo 與程式碼字型:壞掉的 SVG 字形、用正規表達式算出的邊界框、猜錯的漢字,以及最後整理出的 /logo skill。

更新

白色卡片上的赭紅色 Claude 星形標誌與黑色字樣
本頁目錄

引言

這次要修的,是一個我不滿意很久的 logo。它是專案建立第一天就自動產生的純文字標記,之後再也沒有人認真想過它該長什麼樣子。

用 AI 代理設計 logo 要跑好幾輪:AI 代理產出候選,我看過之後刪掉一部分並說明理由,然後再跑一輪。我想知道的是,這個迴圈值不值得寫成一個 Claude Code skill。

在這幾輪裡,AI 代理有五次自信滿滿地弄錯了,而我不認為其中任何一次是白費。五次走錯路當中,有四次各自歸納出了一條規則。這些規則現在成了一個 /logo skill 和三支指令碼,下次就能用這個 skill 生成 logo。

最後上線的是一枚刻著桜的朱紅印章,旁邊是用 Fraunces 排的「oharu」;程式碼字型則換成 JetBrains Mono,只用在文章頁。

這個部落格沒有設計,而我一直將就著用

當時的 favicon 是這樣:

舊版 favicon 的三種尺寸一個圓角藍色方塊,中央是白色的小寫 o,分別以 128、32 與 16 像素呈現。字母在每個尺寸都還看得出來,在 16 像素時整體看起來是一個中間有小塊亮形狀的藍色方塊。ooo128px32px16px
一個圓角的 rect,加上一個指定 Helvetica,Arial,sans-serif 字型的 <text> 元素,以 favicon 實際使用的三種尺寸並列。那個字母用的是訪客作業系統提供的字型。

頁首完全沒有標記,只有純文字的網站標題。

排版也是一樣的情況:

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, …;

全是系統字型堆疊:網站根本沒有自己的字型。每位訪客看到的,都是自己作業系統內建的字型。這本身是正當的取捨(零位元組、零版面位移),只是我從來沒有認真考慮過它。

第一次走錯:同一個想法的二十四種變化

我先要求廣度:橫跨純圖示、文字商標與組合標記,共二十四個概念。這些概念要算繪成一份對照清單,每個都同時以 128px、32px 與 16px 呈現。

它產出了二十四個。它們全都是同一個 logo。

八個圖示概念排成格狀,全部用同樣粗的藍色單線筆畫、圓形端點畫成:圓環、圓弧、圓角方形、V 形符號

這是對照清單裡純圖示的那三分之一。光圈、軌道、終端機、圓弧、鳥居、圓角方形、提示符、分割。同一張圖取了八個名字。

每一個都是單線的幾何筆畫:圓、弧、均勻線寬、圓形端點。不論是圖示還是文字商標,骨架都一樣,而這副骨架正是 AI 生成 logo 最常見的預設樣式。我要的是廣度,拿到的卻是換了八個名字的同一個設計。

解決方法是先去看真正的作品。我挑了六個品牌識別,截圖之後逐一仔細看過:

參考對象它的做法第一輪的做法
Linear、Astro實心填色,標記小而低調全是線條,到 16px 就糊成一團
Rauno Freiberg、Josh Comeau完全沒有標記。只有文字,加上一個固定使用的顏色以為一定要有標記
Maggie Appleton展示用襯線體,因為那是寫作類網站幾何無襯線體,也就是範本的預設值
Anthony Fu個人字母組合,帶有手作感純幾何做不出「個人」的感覺

第二輪回來的東西確實不一樣了,因為這次它參考了真實的網站:

十六個概念分成編輯風襯線、grotesk、朱紅印章、文字加顏色,以及字母組合幾組,看得出是五個不同的想法,而不是一個

向 AI 要 N 個選項,它回傳的是同一個分布裡的 N 個樣本。

第二次走錯:從沒被算繪過的字形

手繪的 SVG 字形會有只在算繪後才看得出來的 bug。第一輪的字形一次都沒算繪過,就直接放進了對照清單。最糟的是字母 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」這個字出現兩次:上面那個最後的 u,右邊筆畫只到一半高度就停了;下面同一個字的筆畫,則和相鄰字母一樣升到完整的 x-height

另外兩個 bug 也屬於同一類錯誤:

  1. 一行副標題放在 y=160、縮放為 0.26,上伸部就落在 160 + 0.26 × (−700) = −22,比位在它上方那個字的基線還高。結果 t、h、b、l 都刺穿了名字。
  2. 一個標記畫成 220 單位高,旁邊的字母卻有 700 單位高。它看起來不像 logo,倒像一個多出來的項目符號。

這些問題在瀏覽器裡一眼就能看出來,在路徑資料裡卻完全看不見。

第三次走錯:AI 代理自己假設的一個字

第二輪最有力的概念是判子(hanko),也就是日本的個人印章。這和部落格的本質很接近。朱紅底色、一個字,字以白色鏤空呈現。

名字是「oharu」,所以 AI 代理用了春(haru,春天)。看起來不錯,差一點就照這樣做下去了。

接著它開口問了,因為憑假設把一個字放進別人的品牌識別裡,不是可以默默做的事。我更正了它:「oharu」是桜明。

這個答案改變的是整個設計,不只是一個字符。桜明是兩個字,而兩個字在 16px 的 favicon 裡根本認不出來:在那個尺寸下,十畫的漢字只會變成一團紋理。

六個朱紅印章方案,每個都先放大呈現,再以 32 與 16 像素並列。在 16 像素時,兩字版本都成了看不出輪廓的色塊,只有單字圓印還認得出來

16px 的算繪結果直接給出了答案。直排的桜明是這個名字正確的印章形式,在瀏覽器分頁上卻表現最差。加了外框的版本保住了邊框,兩個字卻都看不見了。所以印章改成了圓形、單字的桜,而選一個字而不是兩個字,是由筆畫數決定的,不是品味。

第四次走錯:用正規表達式算出來的邊界框

到了正式上線的素材,手繪字母換成了從字型烘焙出來的外框:文字商標用 Fraunces,桜用 Noto Serif JP,透過 fontTools 與 HarfBuzz 產生。正確的字距與筆畫粗細對比都直接寫進了路徑資料,所以文字商標是以幾何圖形的形式上線,不需要網頁字型請求,也不會先閃過備用字型。

擺放字符時,需要知道它實際筆畫的邊界。outline.py 把路徑字串裡的每個數字都抓出來,當成交替出現的 x、y 座標對。SVGPathPen 會輸出 H 和 V 簡寫,而這兩個指令只帶一個座標。只要出現一條水平線,後面每個數字的奇偶位置就全部錯開:

路徑 M0,0 H700 L720,50
實際的點 (0,0) (700,0) (720,50)
regex 配對 (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。正規表達式先前一直很有把握地回傳別的數字。

它沒有當掉。它產出了看似合理的結果,人看了一眼就核准了,而且核准了兩次。

第五次走錯:每一頁都載入的 preload

程式碼區塊也換上了自己的字型。技術部落格真正被細讀的就是程式碼區塊,而我的程式碼區塊當時還是用 Menlo 顯示,和內文一樣處於「沒有人做過決定」的狀態。

JetBrains Mono Variable,只取 Latin 子集,40KB,自行託管在固定路徑上,這樣就能以 URL 指定 preload。程式碼部分不會另外抓 CJK 字型:程式碼都是 Latin 字元,所以 CJK 字型堆疊維持原樣。

preload 被放進了共用版面。瀏覽器直接說明了為什麼這樣不對:

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 索引頁上都沒有程式碼。每一頁都下載了 40KB,卻完全沒用到。於是改成只有在 ArticleLayout 設定了 preloadMono prop 時才輸出 preload:

頁面rel="preload" 連結數
dist/index.html0
dist/ja/index.html0
dist/tags/index.html0
dist/blog/<article>/index.html1

一則主控台警告,換來了實際省下的下載量。這是五次走錯路裡,唯一由工具主動回報的一次。其餘的都只能靠算繪、量測或提問才發現。

這幾輪最後變成了 /logo skill

這個 skill 的鐵則,以及每一條的由來:

規則由來
生成任何東西之前,先研究真正的作品來自同一個分布的二十四個概念
先算繪出來親眼看過,再拿給任何人看只有半截字幹的 u
用 16px 來判斷在瀏覽器分頁裡糊成一團的桜明
絕不把關於對方的假設寫死在設計裡春
用真實字型的外框,不要手工畫字形手工搭出來的幾何,以及量測它的那段正規表達式
顏色靠並排比較決定,不靠理論紅色印章旁的藍色文字商標
每一道關卡都由使用者來選迴圈本身

這個 skill 要求 AI 提出方案、並附上推薦理由,然後停下來:可以提出意見,但決定權在使用者手上。本文的每一輪,都停在一道由人類來選的關卡。正因如此,五次走錯路的代價只是一個下午,而不是一整套建立在 AI 猜測上的品牌識別。

機械性的那一半由三支指令碼負責:

  1. gallery.mjs 組出對照清單,並在本機連接埠上提供瀏覽。
  2. outline.py 透過 fontTools 與 HarfBuzz,把字型外框烘焙成路徑資料。
  3. build-brand-lockup.mjs 是實際範例,負責產出 favicon 與頁首元件。

概念與對照清單都寫進暫存目錄,絕不寫進 repo:一輪設計產出的大多是被淘汰的方案,而 repo 不是放淘汰品的地方。

上線的成品

一枚刻著桜的朱紅圓印,旁邊是用 Fraunces 排的「oharu」。它是以兩個 SVG 上線的,而不是一個。印章刻意做得比文字商標實際的筆畫高度矮,所以把整個組合標記裁成正方形,也得不到窄螢幕用的版本。

畫面寬度低於 50rem 時只顯示印章,這也順帶解決了一個舊的版面問題:在手機上,文字商標會和搜尋控制項搶空間。

其中一個顏色,是把選項並排比較之後才決定的。網站的強調色是 #1d4ed8,印章是 #d8452a。這兩個顏色在色相上幾乎正對面,藍色文字商標放在紅色印章旁邊,衝突一眼就看得出來。文字商標改用主題的文字色,藍色只留給連結,所以兩者永遠不會相鄰。

文字商標路徑資料3569 位元組(座標取整數後,從 5145 位元組減少而來)
印章路徑資料1677 位元組
favicon.svg1878 位元組
JetBrains Mono,Latin 子集40KB,只用在文章頁
/ja/ 文章頁的網頁字型請求1 個(JetBrains Mono),不含 CJK 字型

總結

五次走錯路。沒有一次是靠推理程式碼抓到的。

那些千篇一律的概念,要擺在真實的參考對象旁邊,才看得出有多千篇一律。壞掉的字形需要一個瀏覽器。猜錯的漢字需要一次提問。邊界框的 bug 需要一個荒謬到讓人注意的數字。白費的 preload 需要一個主控台。

這五次正是把這件事當成迴圈、而不是單一提示詞來跑的理由。現在產出二十四個精緻的概念只要幾分鐘。也因為這樣,人很容易把數量誤當成廣度,也很容易接受一個建立在猜測上、看起來卻很有把握的成品。

迴圈比單一提示詞多出三件事:算繪、量測,還有提問。設計取決於只有我知道的事實時,AI 代理就該開口問我。

我並不覺得這五次試錯是白費的。因為下次當我設計 logo 時有七條規則和三支指令碼可以遵循,而不是從一個空白的提示詞開始。

分享這篇文章