昨天講完 caption,那是整條流程裡唯一沒有版型管的自由文字。今天回到程式這一側,看圖卡本身是怎麼算出來的,還有 Day 19 留的那個問題:1080×1350 這個數字,為什麼我說它不是美觀考量。
先講機制。render_cards.py 沒有用任何繪圖函式庫去畫圖。它做的事是:把 deck.json 裡的每張 slide 組成一份 HTML,開一個 Chromium,把視窗設成 1080×1350,截圖存成 PNG。
組 HTML 分三層。最外面是 _shell.html,只有一個 <div class="card">,是所有卡片共用的殼。殼裡放 base.css 填完顏色之後的內容,再把對應版型的 HTML 塞進去。版型是 cover.html、word.html、spectrum.html、quiz.html 四選一,看 slide 的 type。
佔位符的做法很土:模板裡寫 {{WORD}}、{{EXPLAIN}},程式用字串取代把值換進去。CSS 也是同一套,{{BG}}、{{ACCENT}} 這些從 config.yaml 的 brand 區塊來。沒有模板引擎,沒有什麼框架,一個 fill() 函式二十行不到。
截圖那段是這樣:
page = browser.new_context(
viewport={"width": W, "height": H}, device_scale_factor=1
).new_page()
for i, hp, png in pages:
page.goto(hp.resolve().as_uri())
page.wait_for_timeout(300) # 等字型與 emoji
page.screenshot(path=str(png), clip={"x": 0, "y": 0, "width": W, "height": H})
每張卡先寫成 card-01.html 這種暫存檔,截完圖就刪掉,最後留在資料夾裡的只有 PNG。另外有個 --html-only 參數,只吐 HTML 不開瀏覽器,要調 CSS 的時候,可以直接用瀏覽器開那份 HTML 來看,不用每次等截圖。
用網頁當畫布,對不會設計的人有個實際的好處:字太大就改 CSS 裡的一個數字,不用學任何繪圖 API。
render_cards.py 開頭有一行 W, H = 1080, 1350,視窗跟截圖範圍都吃這兩個值。base.css 裡 html, body 跟 .card 也各寫死了 width: 1080px; height: 1350px;。三處對得上,截出來才不會多一條邊或少一截。
這也代表我要是哪天想改尺寸,得同時動 Python 跟 CSS 兩邊,沒有單一來源。目前沒改過,就先這樣,但這是我讀程式時看到的一個小地雷。
這個數字的比例是 4:5。render_cards.py 開頭的註解寫得很直接:全部輸出同一個尺寸,不是美觀考量,IG 會把輪播裡所有圖裁成第一張的比例。
這句話的重點是「同一個尺寸」,不是「4:5 比較好看」。輪播是一組圖,IG 只認第一張的比例,後面每一張都照它裁。要是我的封面是 4:5,結果某張單字卡不小心是正方形,那張就會被切。切掉的地方可能剛好是例句,小孩看到半句英文。
references/ig-api.md 把這件事列在限制裡,叫「最常見的翻車點」。排錯表裡「輪播圖被裁掉」的原因也只有一個:各張比例不一致,處理方式是重跑 render_cards.py,全部 1080×1350。
所以這個尺寸對我來說是一條規格,不是設計選擇。不過程式碼跟文件只說了「要統一」,沒有說為什麼挑 4:5 而不是別的比例,這個我沒辦法替它補理由。能確定的是,統一比 4:5 本身重要。
另外有兩處在守這條規格。build_preview.py 的預覽頁,圖片容器 CSS 用 aspect-ratio: 4/5,所以我在預覽看到的框,形狀跟 IG 上會看到的一樣。預覽頁上也放了一行字:所有圖會被裁成第一張的比例,全部 1080×1350 才不會被切。
尺寸固定之後,版型裡很多數字就有了依據。
.card 的 padding 是上下 76px、左右 72px,右上角固定一個 46px 的圓點。單字卡的字級大概是這樣:單字本體 108px,解釋 44px,英文例句 40px,中文例句 34px。這些數字都是對著 1080 寬的畫布量出來的,換了尺寸就得重調。
排版用的是 flex。卡片是直向的 flex 容器,單字卡的插圖區塊用 flex: 1,吃掉剩下的高度,最小高度 380px;例句框用 margin-top: auto 推到底部。意思是文字少的時候,插圖區會撐大,文字多的時候,插圖區會縮到 380px 為止。
這個設計有一個我自己要承認的限制。html, body 設了 overflow: hidden,高度又是死的 1350px。所以要是 AI 寫的解釋太長,插圖縮到最小還是塞不下,多出來的東西會直接被切掉,不會報錯,也不會自動縮字。Day 20 講過欄位字數沒有程式在檢查,這就是後果發生的地方:圖照樣算出來,只是底部少一塊。我現在靠第 5 階段看預覽,用眼睛抓這種圖。
字型跟 emoji 是這支程式唯一依賴運行環境的地方。config.yaml 預設的字型是 'Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei', 'Segoe UI Emoji', sans-serif,Chromium 是用機器上實際裝了的字型去算。機器上沒有,就退回 sans-serif,圖算得出來,只是字長得不一樣。設定檔裡我也特別註記:要填這台機器上真的裝了的字型。所以截圖前有一句 wait_for_timeout(300),註解是「等字型與 emoji」,簡單粗暴,但夠用。
另一個是 device_scale_factor=1,輸出就是剛好 1080×1350 個像素,沒有放大成兩倍。檔案不會變大,也不會跟 IG 要的尺寸對不上。
圖卡是 HTML 加 CSS 組出來、用 Chromium 截成 PNG 的。尺寸固定 1080×1350,因為 IG 輪播以第一張的比例為準,其他張一不一致就會被切。這個數字在 Python、CSS、預覽頁三個地方各守一次,版型的字級跟間距也都是為它量的。已知的洞是文字太長會被默默切掉,現在靠預覽把關。
明天講安全設計:教材是資料不是指令,以及發布前的兩道閘門。