文件型網頁的響應式靠重排(reflow):欄位換行、字級縮放、side nav 變 hamburger。遊戲畫面沒有這個選項——一張 1920×1080 的戰場不能「換行」。遊戲的響應式只能靠縮放(scale)+取景(framing):先固定一個設計解析度,再決定螢幕比例不合時要補黑邊、要裁切、還是要放寬視野。這是兩套完全不同的技術,混用就會做出「在 iPhone 上底部按鈕被 home indicator 蓋住、canvas 糊成一片、轉橫向後跑版」的東西。
本專欄只處理一個具體目標:一塊佔滿整個螢幕寬高的遊戲畫面,在手機、平板、桌機上都正確、清晰、不跑版、不掉幀。每篇原子筆記給可貼上就跑的 CSS/JS,並標明哪些是穩定標準、哪些是平台相依的坑。
三個必須先接受的事實
一、100vh 在行動瀏覽器是錯的,而且錯得不一致。 vh 綁的是大視口(large viewport),也就是工具列收起時的高度。工具列展開時 100vh 比實際可視區大,底部內容被吃掉;而工具列的展開/收合又由捲動手勢驅動,所以同一頁在同一支手機上會有兩種高度。正解是 CSS Values 4 的 svh/lvh/dvh 三組單位,2025 年 6 月起已是 Baseline Widely available。
二、CSS 像素不是螢幕像素,canvas 必須自己補上 DPR。 <canvas> 有兩套尺寸:style.width/height(版面佔多大)與 canvas.width/height(backing store 有幾個像素)。只設前者,瀏覽器會把低解析度的 bitmap 放大,在 DPR 3 的手機上就是三倍模糊。必須 canvas.width = cssWidth × devicePixelRatio,再用 ctx.scale(dpr, dpr) 把座標系換回 CSS 像素。
三、瀏海與 home indicator 預設會蓋住你的內容,除非你主動要求。 加了 viewport-fit=cover 才能畫到螢幕邊緣,但同時 env(safe-area-inset-*) 才會變成非零值——你必須自己把 UI 推進安全區。不加 cover 則永遠拿不到滿版;加了不處理 inset 則按鈕被切掉。兩者是綁在一起的一組決策。
能力階段
| 階段 | 你已經能做到 | 對應筆記 |
|---|---|---|
| 觀念正確 | 說得出重排與縮放的差別、知道遊戲該用哪一套 | 01 |
| 視口設定對 | meta viewport 寫對、知道 svh/lvh/dvh 各該用在哪 | 02 |
| 容器真的滿版 | 沒有捲軸、沒有橡皮筋、safe-area 有處理 | 03 |
| 會縮放畫布 | 能實作 letterbox/cover/expand 三種取景並選對 | 04 |
| 畫面是清晰的 | DPR、backing store、render scale 都算對 | 05 |
| 尺寸變化不會壞 | ResizeObserver 重算、轉向不跑版、無 layout thrash | 06–07 |
| 跨裝置 | 直橫向、觸控 vs 鍵鼠、平板桌機的 UI 重排 | 08 |
| 沉浸體驗 | Fullscreen、鎖定方向、防縮放防長按,且有 iOS 降級 | 09 |
| 輸入不擋畫面 | 鍵盤彈出用 visualViewport 正確處理 | 10 |
| 效能達標 | 依裝置調 render scale、動態解析度、分層渲染 | 11 |
| 會排錯 | 看到症狀能直接定位原因 | 12–13 |
「我想做到⋯⋯」索引
- 我只要一塊 16:9 的畫面永遠置中、完整可見、周圍補黑 → 畫布縮放策略 的 letterbox,純 CSS 版只要
aspect-ratio+max-width/max-height+ 置中容器。 - 我要真正滿版、不要黑邊,可以裁掉一點邊緣 → 同篇的 cover 策略,配合「安全框(safe frame)」規劃 UI 位置。
- 我要像現代手遊那樣:UI 貼齊四角,視野隨螢幕變寬 → 同篇的 expand(彈性視野),這是 16:9 到 20:9 差距這麼大的今天最實用的一種。
- 我的 canvas 在 iPhone 上很糊 → DPR 與清晰度:backing store 沒乘
devicePixelRatio,或canvas.width設完之後忘了 context 狀態已被重設。 - 底部按鈕被 Safari 工具列/home indicator 蓋住 → 視口單位 的
svh+ 滿版容器 的env(safe-area-inset-bottom)。 - 手機上整頁會被拖動、出現橡皮筋回彈 → 滿版容器 的
position: fixed根容器 +overscroll-behavior+touch-action: none。 - 轉橫向之後版面錯位,要轉兩次才對 → 重算迴圈:
orientationchange/resize讀到的是舊尺寸,改用ResizeObserver。 - 我要全螢幕並鎖橫向 → 全螢幕與沉浸模式,含 iPhone 上做不到時的旋轉 stage 降級方案。
- 輸入框一點下去鍵盤把整個遊戲頂上去 → 鍵盤與 visualViewport。
- 低階 Android 掉幀 → render scale 與效能 的動態解析度演算法。
- 我想直接查症狀 → 常見錯誤與排錯。
學習路徑
- 響應式的真義:重排 vs 縮放,遊戲屬於哪一邊
- 視口設定:meta viewport 與 svh/lvh/dvh 的正確用法
- 真正滿版的容器:safe-area、無捲軸、無橡皮筋
- 畫布縮放策略:letterbox/cover/expand 的數學與實作
- DPR 與清晰度:backing store、render scale、像素藝術
- 重算迴圈:ResizeObserver、轉向時序與 layout thrash
- CSS transform 縮放 vs 直接 resize:取捨與輸入座標換算
- 方向與裝置:直橫向、觸控 vs 鍵鼠、平板與桌機的 UI 重排
- 全螢幕與沉浸模式:Fullscreen API、鎖定方向、iOS 的天花板
- 鍵盤與 visualViewport:不讓輸入法頂爛版面
- 效能:render scale、動態解析度與分層渲染
- 常見錯誤與排錯(症狀導向)
- 主張 vs 可佐證:哪些是標準、哪些是平台相依
與站內其他筆記的關係
- Dark Theme 最佳實踐:同一個「行動端視口」問題的另一半。那篇處理顏色與主題(
theme-color、安全區域白邊、WebView 強制反色),本專欄處理幾何與尺寸;viewport-fit=cover造成的安全區白邊在兩邊都會出現,見 mobile web 專篇。 - 前端設計模式:本專欄處理的是「畫面幾何」,那篇處理的是「狀態與架構」。遊戲的 resize/orientation/fullscreen 三態切換,本質上就是那篇講的 State Machine 適用場景。
- Agentic 產品的串流 UIUX:兩者共用同一個難題——UI 尺寸在執行期會變,版面必須從「一次算好」改成「持續協調」。
- 可維護技術棧與 SEO/無障礙基本盤:本專欄多處用到
touch-action: none、user-scalable=no這類會傷害無障礙的手段,取捨原則見那篇與本專欄 13。
🔍 待解問題 / 持續追蹤
- Safari 何時原生支援
interactive-widget?在它落地前,iOS 上鍵盤彈出只能靠visualViewport手動補償,而這條路在捲動中的中間狀態仍有抖動。 - iPhone 上任意元素的 Fullscreen API 狀態反覆(caniuse 長期標為 partial、部分版本需開旗標),是否已可在 2026 年的量產程式碼中依賴?目前結論仍是「必須 feature-detect + 準備降級」。
- Safari 的 canvas 記憶體上限(單一 canvas 面積與行程總量)沒有公開文件,只有社群實測值。多層 canvas + 高 DPR 的組合在 iOS 上何時會被回收,缺乏可預測的模型。
dvh的更新被瀏覽器節流甚至 debounce(web.dev 明確說明不保證 60fps),用它做遊戲主容器高度在快速捲動時會抖。目前的實務解法是「版面用svh固定、視覺滿版靠position: fixed」,但這是繞道而非正解。- 折疊機(
env(viewport-segment-*))上的滿版遊戲該怎麼取景?跨越鉸鏈的裁切區目前沒有業界共識。
🕒 更新紀錄
- 2026-08-14:初版,建立 hub + 13 篇原子筆記(視口單位、滿版容器、畫布縮放三策略、DPR、重算迴圈、transform vs resize、方向與裝置、全螢幕、鍵盤、效能、排錯、證據檢視)。