展示螢幕

循環播放的全螢幕頁面,用在活動大螢幕、店面櫥窗或展場。它的設計目標是無人看管連續運作多天,限制見文末的「已知限制」。和簡報一樣,它是一個離線 HTML 檔案。

規則為什麼是這樣,以及出處:程式碼儲存庫的 plans/research-screen.md。

做一個展示螢幕

npx slidehtml screen new my-hall         # 產生 src/screens/my-hall/index.tsx
npx slidehtml screen dev my-hall         # 預覽
npx slidehtml screen check my-hall       # 字型、型別檢查、建置、QA,最後一行是 PASS 或 FAIL
npx slidehtml screen build my-hall       # 輸出 dist/screen/my-hall.html

寫完要跑到 PASS,並看過 qa/screen/my-hall/ 裡的每張圖。

check 需要 Playwright 的 Chromium(npx playwright install chromium)。其中的字型步驟需要 Python 3、fonttools、brotli 和 Noto CJK 的 .ttc 檔案(apt install fonts-noto-cjk fonts-noto-cjk-extra,或設定 NOTO_CJK_DIR)。缺少這些時,check 會以 FAIL 結束。build 不跑字型步驟,也不跑 QA,所以要在 check 之後用,不能取代它。

import { BigNumber, Hero, defineScreen } from '../../screen'

export const screen = defineScreen({
  title: '我的展場',
  theme: 'cobalt',
  face: 'editorial',                  // editorial | grotesk | heavy
  atmosphere: 'stars',                // stars | grid | none
  orientation: 'landscape',           // landscape | portrait | both
  venue: { diagonalIn: 98, distanceM: 5, baselineM: 1.4, panel: 'lcd' },
  clock: { date: true },
  reloadAt: '04:30',
  scenes: [
    { id: 'open', label: '開場', seconds: 12, render: () => <Hero title={['一個訊息']} source="來源:…" /> },
  ],
})

字級由場地決定

venue 必填。寫明面板大小(diagonalIn,LED 牆用 longEdgeMm)、希望從多遠讀到(distanceM)、文字離地多高(baselineM)。引擎據此算出所有字級,所以沒有哪個範本能設定字級。欄位放不下,就刪字或拆場景。

QA 測量字形的真實高度:

文字規則
拉丁字母、數字2010 ADA Standards Table 703.5.5,按大寫 I 測量
中文同一數值 × 1.54(Zhang et al. 2020)
小字(來源、署名)在 nearM(預設 1.5 公尺)處 16 弧分,且至少 20 設計 px

舉例:98 吋面板從 5 公尺讀,中文字級約 80 設計 px。5 公尺寬的 LED 牆從 10 公尺讀、安裝位置較高時,約 77 設計 px。

範本

每個範本都有自己的橫向和直向版式。下表的上限針對範例場地,真正的上限以你的場地下 QA 的結果為準。

範本用途欄位
Hero一個訊息kicker?、title[](≤ 3 行)、portraitTitle[]?、sub?、art?、source、credit?
BigNumber一個數字kicker?、value(≤ 7 個字元)、unit?、label、note?、art?、source、credit?
Moments3–5 個事件,依序點亮kicker?、title[]、items[{ when, at?, label }]、source
Quote某人說過的一句話kicker?、lines[]、original?、by、source
Pair兩半,後一半是重點kicker?、items[2]{ big, small }、source
Board固定的小表格kicker?、title、rows[{ left, right }](≤ 5)、source
NowNext正在進行和下一場,由時鐘決定source
Agenda今日議程,由時鐘決定title?、source

art 寫成 { src, mode: 'disc' | 'ground' },src 是用 import 匯入的 decor- 或 photo- 圖片,並在 credit 裡寫明。

中文可以在任意兩個字之間換行,所以較長的 sub 或 note 可能把詞組拆開。要保持完整,用 JS 字串寫換行:sub={'第一句,\n第二句。'}。一般的 sub="..." 屬性裡,JSX 會把 \n 當成字面文字。議程標題來自資料,不能寫換行,要寫得夠短,或檢查直向的 NowNext,那裡會以最大字級換行。

不要嵌入 kit 投影片。它們的內文字級是給坐在室內的人設計的,在任何真實的展示場地都過不了距離規則。

內容規則

  • 一個場景一個訊息。三個數字就是三個場景。
  • source 必填,以小字顯示。查不到出處的事實不要寫。
  • 虛構資料:在螢幕上設定 sample。它以內文字級顯示在每個場景上,任何場景少了它 QA 都會 FAIL。
  • seconds 要夠讀完:2 秒 + 字元數 ÷ 每分鐘 260 + 單字數 ÷ 每分鐘 238,最少 6 秒。

時間

timezone 是 IANA 名稱,不寫就用播放裝置自己的時區。下面的內容都跟隨它。

{ id: 'lunch', label: '午餐', seconds: 9, when: { from: '12:00', to: '13:30' }, render: … }  // 只在這段時間播放
{ when: { from: '22:00', to: '02:00' } }                                      // 跨午夜
{ when: [{ from: '09:00', to: '18:00', days: [1, 2, 3, 4, 5] }] }             // 平日;0 是週日
{ when: { from: '08:00', to: '20:00', dates: ['2026-11-14'] } }               // 某一天

要顯示正在進行和下一場,寫 agenda: (data) => sessions。每場有 start、end、title、who?、room?、date?、isBreak?。NowNext、Agenda 和 items 為函式的 ticker 都跟隨時鐘,不需要重新載入。states 列出 QA 要拍照的時刻。

即時資料

data: { initial: SAMPLE, parse: parseEvent, everySec: 30, staleAfterSec: 120, source: 'event-data.js' }

設定 source,或在 URL 裡寫 ?data=。

  • 本機檔案必須是放在頁面旁邊的 .js 檔案,用相對路徑。內容是一次呼叫:window.__screenData({ ... })。從 file:// 開啟的頁面讀不了 JSON。
  • URL 必須回傳 JSON,如果頁面在 file:// 上,還要允許 CORS。遠端 .js 會被拒絕,因為它會在頁面裡當成程式碼執行。

parse 遇到格式錯誤的內容應該拋出例外:上一份正確的資料會保留,重新載入後也還在。超過 staleAfterSec 沒有拿到正確資料時,螢幕上顯示 数据已过期 · 最后更新 14:32,資料恢復後消失。

安裝

把這一個 HTML 檔案複製到播放裝置,然後:

# Linux 或樹莓派。參數來自樹莓派的 kiosk 教學。
chromium --kiosk --noerrdialogs --disable-infobars --no-first-run --start-maximized \
  "file:///home/signage/my-hall.html"

瀏覽器之外必須有一個監控程式。 頁面能接住自己的錯誤,但重新啟動不了當掉的算繪程序,也發現不了自己卡住的主執行緒。下面的 unit 只是起點,沒有在真實的播放裝置上跑過;使用者服務還需要你的圖形工作階段環境,所以活動之前要測試,包括拔電源。

# /etc/systemd/user/signage.service
[Service]
ExecStart=/usr/bin/chromium --kiosk --noerrdialogs --disable-infobars --no-first-run --start-maximized file:///home/signage/my-hall.html
Restart=always
RestartSec=5
[Install]
WantedBy=default.target

裝置上還要做這些:

  • 在作業系統裡關閉螢幕變黑和 DPMS,關閉面板內建的省電計時器。頁面會請求 wake lock,但瀏覽器只把它當成建議。
  • 把面板的畫面尺寸設為 1:1 / Just Scan / 不過掃描。
  • 讓系統時鐘透過 NTP 同步。排程準不準,取決於裝置時鐘準不準。
  • 樹莓派要開啟 overlay 檔案系統,這樣斷電不會損壞記憶卡。
  • 直立面板:盡量在作業系統裡旋轉。做不到的話,在 URL 加 ?rotate=90 或 ?rotate=270。面板可以往哪邊轉,查說明書,各廠商不一樣。

操作按鍵

不按鍵時什麼都不顯示。滑鼠游標是隱藏的。

按鍵作用
F全螢幕
Space暫停,10 分鐘後自動繼續
N / →,P / ←下一個,上一個
I狀態:時鐘、運作時間、記憶體、wake lock、資料、錯誤記錄
R重新載入

URL 參數

參數意思
?now=14:30,?now=2026-11-14T14:30讓螢幕時鐘假定現在是這個時間
?speed=60讓時間跑快,用來預覽
?pin=<scene id>停在一個場景上
?rotate=90在沒有旋轉的輸出上旋轉畫面
?data=<路徑或 url>資料來源

出問題時會怎樣

故障行為
場景拋出例外或什麼都沒算繪記錄並跳過,不會黑畫面。休息 5 分鐘,加倍直到 1 小時,然後再試
所有場景都失敗,或沒有場景在排程內顯示帶標題的等待卡,每 30 秒重試
找不到責任場景的錯誤記錄,循環繼續
資料輪詢失敗或格式錯誤上一份正確的資料保留。退避到 10 分鐘一次。到設定時間後顯示過期提示
堆積記憶體超過 memoryLimitMb(預設 400)在下一次場景切換時重新載入,30 分鐘內最多一次。僅限 Chromium
reloadAt每天在下一次場景切換時重新載入一次。透過 http 提供時,只有 HEAD 檢查成功才會載入
算繪程序當掉或卡住,或機器重新開機頁面無能為力:這是監控程式的工作

烙印

底色接近純黑,不用純白。時鐘、名稱和來源每次切換場景時移動幾個像素,每 10 分鐘左右互換(burnIn: { flipMinutes, shift })。panel: 'oled' 把移動幅度加倍,'led' 關閉移動。如果完整循環一遍後,畫面上有超過 1% 的區域始終高於 25% 亮度,QA 會 FAIL。這只能降低風險,不能消除風險。例如 LG 的 B2B 保固不包含殘影。請讀你自己的保固條款。

QA 檢查什麼

scripts/screen-qa.mjs 針對建置好的檔案執行,按 orientation 在 1920×1080 和 3840×2160,和/或 1080×1920 和 2160×3840 上檢查:

溢位、裁切、重疊和 90% 安全區。按字形檢查文字大小是否符合場地。相對真實像素的對比度至少 4.5:1。退回系統字型。來源是否存在,範例標註。每個場景的時間。每個時間區段的邊界和即時時鐘換日。場景拋出例外、在 effect 裡失敗、什麼都沒算繪。所有場景失敗,然後恢復。非同步錯誤。操作按鍵。資料來源、錯誤資料、檔案遺失、過期狀態、恢復、斷線期間重新載入、遠端指令碼。夜間重新載入和記憶體重新載入。烙印像素。800 次場景切換的 soak 測試,堆積記憶體、DOM 或監聽器無限增長時 FAIL。沒有主控台錯誤和網路請求。

已知限制

  • soak 測試是把時間加速跑 800 次場景切換,不等於在裝置上跑幾天,也測不到 GPU 記憶體、驅動程式和面板。活動之前,在真實的播放裝置上跑幾天。
  • 頁面重新啟動不了當掉的算繪程序,也發現不了自己卡住的主執行緒,所以瀏覽器之外必須有監控程式。上面的 systemd unit 只是起點,沒有在真實的播放裝置上跑過。
  • wake lock 只是建議。裝置上也要關閉螢幕變黑和省電。
  • 排程準不準,取決於裝置時鐘準不準。
  • 記憶體重新載入只在 Chromium 裡有效。
  • 防烙印措施只能降低風險,不能消除風險。
  • 資料過期和暫停的提示只有簡體中文,寫了 lang: 'en' 也一樣。