拆解 3D 政見網站「HEART 政策島」:Three.js、單一 JSON,以及程式碼裡的 AI 協作痕跡
台北市長參選人競選官網的 3D 政策頁,自寫程式約 7,400 行、沒有打包工具、全部內容在一個 159 KB 的 JSON。資料檔裡的 Claude 輸出目錄、待確認狀態欄位,以及被官網收錄的外掛在 GitHub 的提交紀錄,顯示它在資料整理、視覺規範與外掛收錄三個環節都用了 AI 協作。本文只談技術,不評論政見。
本文重點
- 拆解台北市長參選人競選官網的 3D 政策頁,分析其技術架構與 AI 協作痕跡。
- 自寫程式約 7,400 行,內容存於 159 KB 的 JSON,資料檔留有 Claude 輸出目錄。
- 無 GPU 環境效能極差,第三方外掛收錄缺乏明確授權條款,仿效時需注意。
- 本站上線
- 2026/10/24 下午11:51
- 首次報導
- 2026/10/24 下午11:51

台北市長參選人沈伯洋的競選官網上有一頁「HEART 政策島」:14 項政策主題排成 14 塊六邊形小島,拼成台北市的輪廓,島上有建築、行人、日夜與季節,點任何一棟建築就看到對應的政見細項與出處。本站站長看到後的第一個反應是「這也是用 AI 寫的吧」。我們把整頁公開的程式碼與資料檔抓下來拆開,從架構、資料、渲染到效能逐項看,並查到了它與 AI 協作的直接證據。以下只談技術,不評論任何政見內容;實測影片公開後會嵌在本文最下方。
這一頁解決什麼問題
一份市長政見通常是幾十頁的清單,讀者看不出項目之間的關係。這一頁把 94 個細項分到 14 個主題,再用 20 條「協作線」標出哪些主題互相支援(例如「先替公務員減壓,再推動各項政策」這條線連到 13 座島),每條線與每棟建築都附出處類型:官網政見、候選人受訪、或政策主題串聯。訪客不必讀完清單,也能從一個改變看到它牽動哪些其他項目。
| 面向 | 傳統政見清單頁 | 政策島 |
|---|---|---|
| 呈現 | 文字分類清單 | 3D 城市,一島一主題,一建築一細項 |
| 關係 | 沒有 | 20 條跨主題協作線,可逐條點亮 |
| 出處 | 通常沒有 | 每個細項與每條線標明來源與證據等級 |
| 無法用 3D 時 | 不適用 | 右側清單可完整文字操作,手機版改成底部抽屜 |
架構:沒有打包工具,一個 JSON 驅動整座島
整頁是靜態檔案。HTML 只有 11 KB,用瀏覽器原生的 importmap 指向自帶的 Three.js r160,所有程式都是原生 ES module,沒有 Webpack、Vite 這類打包步驟,也沒有 React 之類的框架。競選官網的其他頁面是 Vite 建置的,這一頁刻意獨立出來,連統計事件的程式都註明「這頁在 Vite 之外,所以另外寫一份」。
自寫的程式共 13 個檔案、約 7,400 行,分工如下:
| 檔案 | 行數 | 負責什麼 |
|---|---|---|
| app.js | 2,302 | 入口:介面、相機、卡片、搜尋、導覽、計時 |
| decor.js | 1,431 | 裝飾物:樹、碼頭、小島、岸邊小物 |
| people.js | 752 | Q 版行人:13 種角色、走路動畫、搬運物件 |
| world.js | 704 | 六邊形島體、橋、行人路網、建築配置 |
| scene-models.js | 674 | 每項政策的象徵建築(例如「彈性上下班鐘塔」) |
| environment.js | 557 | 天空、光、水、雲、地形、日夜變化 |
| kit.js | 133 | 零件庫:同類幾何合成一個 InstancedMesh |
| icons.js、analytics.js、models-extra.js、decor-items.js | 少量 | 圖示、GA4 事件、額外模型與清單 |
| ui.css | 1,545 | 全部樣式,含手機版與減少動態效果 |
內容全部在一個 159 KB 的 policies.json 裡:14 座島、每座島的建築、20 條協作線、3 段受訪引述、一天的時段腳本、任期時間軸、78 秒導覽的分鏡,以及調色盤與版面規則。文字欄位一律是中英雙語物件,所以在網址加上 lang=en 就是英文版;另一個參數 ratio=9x16 會把版面改成直式,顯然是為了錄短影音準備的。
程式碼與資料的分離做得很徹底:建築用明確的 id 對應政策細項,檔案註解寫明「用明確 ID,避免陣列順序改變就悄悄換掉政策的視覺意義」。每個細項帶 facts、phase(時程與依據)、sources、status;每條協作線帶 evidenceTier(證據等級)與引述編號。換句話說,「這一條有沒有查證過」是寫在資料層的欄位,不是寫在程式裡的判斷。
渲染:Instancing、合批與固定亂數
場景裡有上百棵樹、幾十棟建築、60 個會走路的行人,但繪製次數壓得很低,靠的是三個做法:
- 零件庫合批:kit.js 把所有靜態零件依幾何種類(方塊、圓柱、圓錐、球)收集起來,每一種只畫一次 InstancedMesh。
- 角色合併:行人的身體零件用 mergeGeometries 合成少數幾個網格,共用一個頂點色材質;換裝時不重新編譯材質。
- 固定種子亂數:裝飾物的散佈用 mulberry32 這類可重現的亂數,每次載入的島長得一樣,也方便對照截圖。
地形用 SimplexNoise 產生,日夜是一組按分鐘排的關鍵影格(天空頂色、地平線、太陽色與強度、半球光、水色),拖動「台北人的一天」滑桿就在影格之間內插。文字標籤走 CSS2DRenderer,所以 14 個島名是真正的 HTML 元素,可以被選取、也能被搜尋引擎讀到。系統設定「減少動態效果」時,自動旋轉與導覽會停。
效能與相容性
我們用 Playwright 量測:
| 項目 | 結果 |
|---|---|
| 首次載入 | 27 個請求、傳輸 523 KB(解壓後 2.2 MB),其中 Three.js 佔 266 KB |
| 1080p 畫面更新率 | Intel 內建顯示(Arrow Lake)59 fps |
| 純軟體繪圖(無 GPU 的無頭瀏覽器) | 1 fps,幾乎不能用 |
結論是它需要 GPU,但不需要獨立顯卡;一般筆電與手機的內建繪圖足夠。手機版把右側清單改成可拉的底部抽屜,工具列只留全景、角色、清單、時間四個按鈕。無障礙上,3D 畫布標成 role="img" 並說明「完整文字操作請用政策清單」,清單本身是原生的 details 與 button,鍵盤可以完整操作。
它是用 AI 寫的嗎:找到的證據
我們沒有拿到官網的原始碼倉庫,所以主程式是不是 AI 寫的無法從外部百分之百確認。但公開檔案裡留下了四組痕跡:
1. 資料檔直接提到 Claude 的輸出目錄。 policies.json 的 renderPlan.paletteSource 欄位寫著「專案競選 CI:Claude outputs/…/交付說明_README.md」,也就是調色盤的依據是一份由 Claude 產出的交付說明。
2. 資料檔裡留著工作流程的狀態欄位。 meta.disclosure 是還沒填的「{{DISCLOSURE}}」佔位符;meta.stage 是「layout_and_data_awaiting_confirmation」;導覽分鏡的 status 是「storyboard_only」;受訪引述的 verification 是「provided_transcript_audio_unchecked」;協作線的 evidenceTier 有「transcript_pending」。18 個來源各自標了 status(14 個「read_official_page」、2 個「read_news_report」)與 confidence。這是典型的「模型先產生草稿、再由人逐項確認」流程留下的欄位,而且這份檔案是帶著這些狀態上線的。
3. 每個模組檔頭都有同一種「免責」註解。 decor.js、world.js、environment.js、people.js、scene-models.js 的第一、二行都在說明「這裡的東西純屬裝飾,不代表任何政策、地點或時程的主張」。這種在程式碼裡逐檔宣告邊界的寫法,是給 AI 代理的規格文件常見的要求,人工手寫的專案很少這樣做。
4. 被官網收錄的外掛,提交紀錄直接標了 Claude。 頁面上的「我的角色」功能(2,181 行的 heart-me.js)原本是第三方使用者腳本,作者 readper 於 10 月 23 日在 GitHub 發布,單一提交 2,118 行,提交訊息標註「Co-Authored-By: Claude Sonnet 5.5」並附 Claude Code 的工作階段連結;當天官網就取得作者同意收錄,檔頭還註明差異寫在 openspec 目錄下的規格檔。OpenSpec 是一套讓 AI 編碼代理照規格改程式的工作流程,這表示官網這邊的修改也是以規格驅動 AI 代理的方式進行。
同一個競選團隊在 10 月 23、24 日辦了線上「市政 debug 松」,座談題目就叫「AI 寫得出程式,寫不出城市」。綜合來看,這一頁至少在資料整理、視覺規範與外掛收錄三個環節確定用了 AI 協作;主程式的寫法(規格化註解、資料驅動、狀態欄位)也與這種流程一致。
照這個方法做一個,要做哪些事
這個案子值得學的不是 3D,而是分工方式。若要用 AI 助理做一個同類的互動資料頁,順序是:
1. 先定資料格式,再寫程式。 把內容做成一個 JSON:每個項目有穩定的 id、雙語文字、來源清單與證據等級,再加上工作流程的狀態欄位(待確認、僅分鏡、未查核)。這份格式就是給 AI 的規格,程式只負責把它畫出來。
2. 用原生 ES module 與 importmap。 不裝打包工具,AI 產生的每個模組可以直接在瀏覽器載入測試,檔案邊界清楚,之後要改哪一塊就只給哪一檔。
3. 每個模組檔頭寫明它可以做什麼、不可以做什麼。 這一頁的「純屬裝飾、不代表政策」就是例子;AI 在改裝飾物時不會順手去動政策資料。
4. 效能靠合批,不靠減內容。 同類幾何用 InstancedMesh,角色合併網格、共用材質,固定種子亂數讓結果可重現。
5. 留文字版與減少動態效果的路徑。 3D 只是加分,清單才是底線;這也是搜尋引擎與無障礙工具實際讀到的內容。
6. 把查證狀態留在資料裡,而不是刪掉。 上線時哪些還沒確認,資料檔自己會說;這比在程式裡硬寫判斷更容易由非工程人員維護。
人要做的部分很明確:確認每一條內容的出處與狀態。這份資料檔上線時還留著「待確認」的欄位,說明 AI 能把結構和草稿鋪好,但「這句話有沒有講過、出處對不對」仍然是人的工作。
限制
- 它是純前端頁面,政策文字在 JSON 載入後才畫進清單;搜尋引擎能讀到的是標題、描述與島名標籤,細項內容不一定被收錄。
- 沒有 GPU 的環境(部分遠端桌面、舊款虛擬機)會慢到不能用。
- 第三方外掛被官網收錄的做法很有趣,但上游專案沒有附授權條款,只在檔頭註明作者透過社群訊息同意;要仿效的話,授權要先講清楚。
本文的程式碼與資料分析來自頁面公開的靜態檔案,量測在本站的機器上以 Playwright 完成;本站與該競選團隊沒有任何關係,文中不涉及政見內容的評價。
來源:佐證 0 筆,另有背景參考 3 筆
背景參考正文沒有引用,只是相關背景,不計入佐證數
- puma.taipei · HEART 政策島(競選官網頁面)
- github.com · readper/heart-island-me(被官網收錄的使用者腳本)
- puma.taipei · 市政 debug 松(活動頁)
這篇對你有用嗎?
登入會員後可以按反應與留言。登入
留言 0
- 還沒有留言。
留言公開顯示暱稱,不顯示你的真實姓名與信箱。含多個連結或疑似廣告的留言會先經編輯確認;檢舉達 3 次的留言會自動隱藏。隱私政策
發現錯誤?
回報會進入待處理清單;核實後更正,並在文中註記更正紀錄。