
讓 Astro 的圖表依自己的尺寸繪製、點一下就能放大 — 原始寬度、原生對話框與 @2x
在 Astro 中 width:100% 為何讓 256px 的螢幕截圖變糊,以及原生對話框如何讓每張圖表在手機上都讀得清楚。修正這個設計的是七次計測。
本頁目錄
引言
我把這個網站自己的目錄側邊欄截圖放進一篇文章裡,結果看起來糟透了。側邊欄本來是一條窄長的 UI,但在文章中被撐到整個內文欄的寬度,糊到裡面的文字都在暈開,高到把其他內容全都擠出畫面。看著看著,我也注意到問題的另外一半:沒有任何辦法把圖表放大。讀者想看仔細一點,唯一能做的就是用雙指縮放整個頁面。
那張截圖已經上線整整一個版本,沒有任何人反映過。截圖本身也沒有問題。造成模糊的,是一條套用在全站每一張圖表上的 CSS 宣告,而只有窄的那些會真的受害。
修掉模糊,最後是比較小的那一半。更大的發現在同一個元件的另一側:**那些從來沒被拉伸過、因此看起來一切正常的圖表,才是沒有人讀得了的。**本文會談這兩件事,以及過程中七個看起來明顯正確、一量就發現錯了的設計決定。
一條宣告把每張圖表都撐到整欄寬
這個網站的圖表全部都會經過同一個元件,而那個元件對它包住的東西只有一條規則:
.frame :global(svg),.frame :global(img) { display: block; width: 100%; height: auto;}對站上大多數內容來說,這條規則是看不出來的。插圖寬度介於 1,100 到 1,774 像素之間,而內文欄是 662 像素,所以這條規則永遠只會把它們縮小,而縮小不需要任何代價。那張側邊欄截圖是寬 256 像素、高 836 像素,於是同一條規則把它放大了 2.48 倍,變成 662 × 2,161。
有意思的是瀏覽器手上到底有什麼。Astro 會依原始檔案產生 srcset,而一個 256 像素的來源只會產生一個候選:
<img src="/_astro/sidebar-rail.DjnEbD0x_Z25K3Dc.webp" srcset="/_astro/sidebar-rail.DjnEbD0x_Z25K3Dc.webp 256w" sizes="(min-width: 256px) 256px, 100vw" width="256" height="836">**也就是說,瀏覽器下載了 256 像素的圖,被 CSS 要求去填滿 662 像素的版面,而且沒有更大的檔案可以抓。**我當時用的筆電裝置像素比是 2,所以真正的需求是 1,324 個裝置像素,而拿來填的實際資料只有 256 個。
**這個倉庫裡沒有任何東西在盯著這件事。**跨語系檢查確認的是:英文版引用到的圖表,日文版與繁體中文版也有引用,但它從不看檔案本身。圖表適配檢查會在瀏覽器中繪製每張圖表、量測標籤有沒有超出方框,但它只量 SVG 元件,因為帶有翻譯文字的是那一類。點陣圖沒有屬於自己的檢查,所以擋在一張被拉伸的截圖與正式站之間的,只有「有沒有人剛好發現」。
讓尺寸被推導出來,而不是被宣告
修正比錯誤本身還小。Astro 會為它處理過的每張圖片輸出 width 與 height 屬性,因為它在建置時就知道檔案的實際尺寸。把 CSS 的寬度設成 auto,瀏覽器就會採用那個數字:
.frame :global(img) { display: block; margin-inline: auto; width: auto; max-width: 100%; max-height: 56rem; height: auto;}另一個做法是在元件上加一個 width 屬性,這個我否決了。這個部落格每篇文章一個資料夾,裡面放三個語系檔案,所以寫在標記裡的寬度會是一個要寫三次、還得靠人工跟素材保持同步的數字,而一致性檢查也得為它多一條規則。用 width: auto 的話,記錄尺寸的地方就只剩下檔案本身,換掉檔案,版面就跟著換。
第一個出錯的是高度上限。智能體一開始給的值是 min(48rem, 85svh),理由是圖表不該比視窗還高。在一個高 724 像素的瀏覽器視窗裡量測,那條規則把寬 256 的截圖壓成了 188 × 615,等於把一張才剛修好「被重新取樣」問題的圖,又重新取樣了一次。**一個跟著可視區域走的上限,會把「絕不放大」變回「一律縮放」。**現在的值是固定的 56rem,剛好放行站上最高的 836 像素圖表,同時仍然擋得住異常的輸出。
最後一點是外觀問題。一張 224 像素寬的截圖被擺在 662 像素的帶框方塊正中央,本身看起來就像個錯誤,所以外框會依點陣圖縮到剛好。元件是透過插槽接收內容的,在伺服器端無法判斷它包住的是照片還是圖表,因此這個判斷交給 CSS:
.figure:has(.frame img) .frame { width: fit-content; max-width: 100%; margin-inline: auto;}@2x,因為 width: auto 分不出「密」和「大」
依原始寬度決定尺寸之後,還有一件事沒解決。**一個打算以 2 倍密度顯示成 224 像素的 448 像素檔案,在 CSS 眼中,和一張想要顯示成 448 像素的 448 像素圖片完全一樣。**訊號必須來自別的地方,而最省事的地方就是檔名:命名為 <name>@2x.webp 的點陣圖,是以預期顯示尺寸的兩倍存放的,元件則用 zoom: 0.5 把它折半。
這條規則裡有兩件事是靠量測而不是靠推理找出來的,而且兩件都是無聲的。
選擇器需要比對兩種寫法。astro build 會把檔名主幹原樣寫進資產名稱,產生 /_astro/sidebar-rail@2x.HASH.webp;但開發伺服器不是這樣,它把圖片路由到一個以路徑當查詢參數的端點,其中**@ 會被百分比編碼成 %402x。只比對原始寫法的規則,在正式站可以運作,在本機則完全沒有作用,這是所有組合裡最糟的一種。一開始就是那樣寫的,而它之所以被抓到,是因為瀏覽器上的數字沒有變。**
這兩個上限在 zoom 之下的行為並不相同,這也是其中一個被加倍、另一個沒有的原因:
| 宣告 | 實際生效的位置 |
|---|---|
max-height: 56rem |
螢幕上的 448 像素。絕對長度是以該元素自己被折半後的像素來計算,所以上限落在字面值的一半。 |
max-width: 100% |
外框的實際寬度。百分比是相對於包含區塊解析的,而包含區塊沒有被 zoom,所以它們本來就是字面上的意思。 |
第一次嘗試把兩個都加倍了。**它們的行為並不一致,代價是圖片從被壓窄的外框裡溢出了 70 像素。**規則能動之後,我要求把原本那張截圖用兩倍解析度重拍。現在它是 448 × 1,672,繪製為 224 × 836,並以 448 個實際像素填滿所需的 448 個裝置像素。
看起來正常的圖表,才是讀不了的那些
以上就是尺寸的部分,而真正受害嚴重的檔案剛好只有一個。另外一半的工作,是從智能體提出「只在圖表確實被縮小時才顯示放大操作」開始的,理由是對一張已經是原尺寸的圖提供放大,等於沒有給讀者任何東西。
我否決了這個提議。一張圖表可以是原尺寸,同時裡面的文字小到讀不了,而這一點套在圖表上至少和套在螢幕截圖上一樣成立。這裡值得把定案的算式寫出來,因為文章其餘部分都建立在這個數字上。
這個倉庫裡的每張圖表都是在 800 單位的 viewBox 上製作的,並繪製進和其他所有東西相同的 662 像素外框。桌機上的倍率是 0.795。在 390 像素的手機上,外框寬度是 324 像素,倍率是 0.405。把全站實際用到的標籤尺寸帶進去:
製圖時的 font-size |
使用次數 | 桌機(0.795 倍) | 手機(0.405 倍) |
|---|---|---|---|
9px |
20 | 7.2 CSS 像素 | 3.6 CSS 像素 |
10px |
136 | 8.0 CSS 像素 | 4.1 CSS 像素 |
11px |
135 | 8.7 CSS 像素 | 4.5 CSS 像素 |
13px |
78 | 10.3 CSS 像素 | 5.3 CSS 像素 |
圖表是這個網站上做得最仔細的東西,而在手機上它們的標籤只有三點五到五像素高。**一個以「這張是不是被縮小了」為條件的判斷,會剛好把放大操作從它們身上藏起來,因為圖表從來不會被縮小。**圖表是被等比縮放,而讓它讀不了的正是那個縮放。
於是放大行為變成了全面適用:每張圖表都能開啟,點陣圖和圖表一樣,在任何可視區域都一樣。正是這個決定,把一張模糊截圖的修正,變成了影響全站 86 張圖表、三種語言的變更。
對話框需要的是兩條規則,不是一條
這個模式站上本來就有。搜尋視窗是一個用 showModal() 開啟的原生 <dialog>,焦點處理、Escape 與 ::backdrop 都不必自己寫。而且它有一個大多數圖片燈箱沒有的性質:因為它位在瀏覽器的頂層,而不是一個定位過的覆蓋層,**雙指縮放在它裡面仍然有效。**這比字面上聽起來更重要,因為這是「上限固定的放大鏡」和「讀者可以一直推下去的放大鏡」之間的差別。
第一版把圖表在兩個軸向上都塞進視窗,這就是燈箱在做的事。打開那張 256 × 836 的截圖,回傳的是 202 × 661,比它在文章裡已經有的 256 × 836 還小。把直式的圖塞進橫式的視窗,本身就是一種縮小,於是第一個放大鏡把東西變小了。
第二版對點陣圖修好了這件事,對圖表卻仍然是錯的。點陣圖改成以檔案實際寬度為上限,比視窗高就捲動,這是對的。圖表沒有可以停下來的原始尺寸,於是被塞進視窗,而在一個寬 500 像素的瀏覽器上,倍率是 1.03 倍。圖表本來就填滿了整欄,把它塞進視窗完全沒有任何收穫。對話框重現了它存在的理由。
**兩次嘗試都漏掉的是:這兩種媒體要的東西剛好相反。**點陣圖的像素數量是固定的,超過之後放大出來的細節都是補的。圖表是向量,沒有上限,卻有一個有意義的下限:**它被製作時所在的 viewBox。**低於 800 單位,它的標籤就會回到當初在文章裡讀不了的那個縮小版本。所以規則被拆開了:
/* A diagram fits the window, but never draws below the grid it was authored on. */.figzoom-frame[data-figzoom-media='vector'] .figzoom-stage { min-width: calc(var(--figzoom-vbw, 800) * 1px);}
/* A raster shows every pixel it has, and pans if it is taller than the window. */.figzoom-frame[data-figzoom-media='raster'] { width: min(var(--figzoom-cap, 95vw), 95vw);}在一單位對一像素的情況下,font-size: 10 的標籤會繪製成 10 CSS 像素,而外框改以平移取代縮小,這和文章本來就為最寬的那些圖表所做的取捨是同一個。在建置好的網站上量測,圖表在桌機上從行內的 662 像素變成對話框中的 1,342 像素,在手機上則從 324 變成 800。在文章裡只有 4.5 CSS 像素高的 font-size: 11 標籤,在對話框中是 11 CSS 像素,而且讀者再用雙指放大下去,也不會變糊。
那個抹掉每張圖表說明的 button
到這個時候,這份工作已經通過了型別檢查、跨語系檢查、正式建置與圖表適配量測。接著對這個分支跑了一次審查,找出兩個缺陷,而兩個都不是建置會抓到的那一類。
第一個出在智能體為觸發元素挑的標記上。用 <button> 把整張圖表包起來,是讓它可點擊最自然的做法,而它的破壞是安靜的:**ARIA 規定 button 的子元素屬於 presentational。**裡面的一切都會從無障礙樹上被移除,而那棵樹是螢幕閱讀器唯一擁有的東西。
這裡被移除的並不是裝飾。每張圖表都帶有 role="img",以及指向標題與說明的 aria-labelledby,每張點陣圖也都帶有 alt。這些是翻譯流程以三種語言維護的字串,而且往往是一張圖表上最長、寫得最仔細的文字。被 button 包起來之後,這些全部消失,螢幕閱讀器剩下的只有 button 上的兩個字,全站每一張圖表都是如此。
修法是不要再包。觸發元素現在是一個放在外框內、圖表旁邊的放大鏡標記,而不是包住圖表的 button,這樣圖片保有自己的語意,標記則負責名稱與彈出提示。使用指標裝置的讀者點外框任何位置仍然會開啟對話框,但沒有任何東西依賴那條路徑。事後再把無障礙樹讀回來,每張圖表都會回報一個帶有完整說明的 image 節點,以及一個獨立的 button「放大圖表」。這本來就該是這個樣子。
那個不是寬度的寬度
第二個缺陷,是一個已經看過、而且被解釋掉的數字。
對話框依點陣圖的實際寬度決定尺寸,而程式碼讀的是 naturalWidth。在響應式圖片上,這個屬性是經過密度校正的:對 Astro 輸出的寬度描述子而言,密度等於候選寬度除以 sizes 解析出來的結果,所以它回傳的是版面寬度,而不是檔案寬度。在 390 像素的可視區域量測,那個 448 像素的檔案回報 naturalWidth: 389。
這把上限壓到差不多等於圖表原本的大小,**而且正好在這個功能存在理由的那些窄螢幕上,把放大關得最徹底。**正確的值是 width 屬性,它本來就以備援的身分寫在程式碼裡,兩者只是順序反了。
比較難受的是,那個 389 在更早的一次量測中就出現過,而智能體把它當成開發伺服器的怪癖給打發了。它不是。一個被解釋掉的量測,價值和從來沒量過一樣少。
數字說了什麼
以下每個數字都是對正式建置量測的,而不是開發伺服器,因為開發用的圖片端點會提供不同的候選,給出看似肯定卻錯誤的讀數。
| 修改前 | 修改後 | |
|---|---|---|
| 截圖繪製尺寸 | 662 × 2,161 | 224 × 836 |
| 需要的裝置像素 | 1,324 | 448 |
| 可用的裝置像素 | 256 | 448 |
| 圖表・桌機 | 662px | 對話框中 1,342px |
| 圖表・手機 | 324px | 對話框中 800px |
font-size: 11 標籤・手機 |
4.5 CSS 像素 | 11 CSS 像素 |
**那張截圖從只填滿被要求像素數的五分之一,變成全部填滿。**在手機上圖表會放大 2.47 倍,而且因為它們是向量本身、不是向量的照片,讀者再往上用雙指放大多少倍,它都維持清晰。
總結
**原本的錯誤是一條宣告:它對我手上剛好有的每一張圖表都是對的,對第一張不符合那個前提的圖表則是錯的。**把它換成 width: auto 之後,決定的位置移到了檔案上,改動方式從編輯三個語系檔案,變成換掉一個東西。
更大的發現在同一個元件的另一側,而它之所以被看見,只是因為第一個提案被否決了。**八十六張圖表一直帶著手機上四像素左右高的標籤在線上,而它們沒有一張被視為問題,因為它們什麼都沒變,也沒有任何東西在量它們。**一張模糊的截圖會自己站出來,一張只是太小的圖表不會。
值得留下的既不是前一個修正,也不是後一個。而是**這份工作裡有七個各自獨立的設計決定,都合理、都站得住腳,而且都是錯的,並且每一個都是靠標上一個數字被抓出來的,不是靠想得更透徹。**跟著可視區域走的高度上限、兩軸都塞進視窗的對話框、只大了百分之三的圖表、在本機完全沒作用的選擇器、被當成行為一致的兩個上限、抹掉無障礙樹的 button,以及回傳錯誤寬度的屬性。它們看起來都不像是錯的。尤其那個高度上限,一路讀起來都明顯正確,直到瀏覽器回答 188 為止。
一次全綠的建置一個都沒報出來。型別檢查、跨語系一致性、正式建置與圖表適配檢查全部回報無誤,而在那同時,無障礙樹是空的,放大鏡回傳的圖片比它要放大的還小。
參考連結
- MDN: the dialog element, including showModal and the top layer
- WAI-ARIA 1.2 on presentational children, which is why a button around a figure hides it
- HTML Standard: naturalWidth, defined as the density-corrected intrinsic width
- Astro images, covering the width and height attributes emitted at build time
- CSS Viewport Module: the zoom property