Pretext
在使用 @chenglou/pretext 構建創意瀏覽器演示時使用 — 這是一種無需 DOM 的文本佈局方案,適用於 ASCII 藝術、圍繞障礙物的排版流、以文本為幾何形狀的遊戲、動態排版以及由文本驅動的生成藝術。默認生成單文件 HTML 演示。
技能元數據
| 來源 | 捆綁(默認安裝) |
| 路徑 | skills/creative/pretext |
| 版本 | 1.0.0 |
| 作者 | Hermes Agent |
| 許可證 | MIT |
| 平臺 | linux, macos, windows |
| 標籤 | creative-coding, typography, pretext, ascii-art, canvas, generative, text-layout, kinetic-typography |
| 相關技能 | p5js, claude-design, excalidraw, architecture-diagram |
參考:完整 SKILL.md
以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理所看到的指令。
Pretext 創意演示
概述
@chenglou/pretext 是由 Cheng Lou(React 核心成員、ReasonML、Midjourney)開發的一個 15KB、零依賴的 TypeScript 庫,用於無需 DOM 的多行文本測量和佈局。它只做一件事:給定 (text, font, width),返回換行位置、每行寬度、每個字素(grapheme)的位置以及總高度 — 全部通過 canvas 測量完成,無需重排(reflow)。
這聽起來像是底層基礎設施。其實不然。因為它快速且基於幾何計算,所以是一個創意原語:你可以讓段落以 60fps 的速度圍繞移動的角色重新排版,構建關卡幾何形狀由真實單詞組成的遊戲,通過散文驅動 ASCII 徽標,將文本粉碎為具有精確每個字素起始位置的粒子,或者打包收縮包裹的多行 UI 而無需任何 getBoundingClientRect 抖動。
此技能的存在是為了讓 Hermes 能夠用它製作酷炫的演示 — 即人們發佈到 X(原 Twitter)的那種類型。請參閱 pretext.cool 和 chenglou.me/pretext 獲取社區演示合集。
何時使用
當用戶要求以下內容時使用:
- “pretext 演示” / “酷炫的 pretext 作品” / “文本作為 X”
- 文本圍繞移動形狀流動(英雄區域、編輯佈局、動畫長頁面)
- 使用真實單詞或散文的 ASCII 藝術效果,而非等寬柵格
- 遊樂場/障礙物/磚塊由文本構成的遊戲(字母俄羅斯方塊、散文打磚塊)
- 具有每個字形物理效果的動態排版(粉碎、散射、群集、流動)
- 排版生成藝術,尤其是使用非拉丁腳本或混合腳本
- 多行“收縮包裹”UI(仍能容納文本的最小容器寬度)
- 任何需要在渲染之前知道換行位置的情況
不要用於:
- CSS 已解決佈局的靜態 SVG/HTML 頁面 — 直接使用 CSS
- 富文本編輯器、通用內聯格式化引擎(pretext 故意保持狹窄的範圍)
- 圖像轉文本(使用
ascii-art/ascii-video技能) - 沒有文本角色的純 canvas 生成藝術 — 使用
p5js
創意標準
這是在瀏覽器中渲染的視覺藝術。Pretext 返回數字;你來繪製圖形。
- 不要交付“hello world”級別的演示。
hello-orb-flow.html模板只是起點。每個交付的演示必須添加有意的色彩、運動、構圖,以及一個用戶未要求但會欣賞的視覺細節。 - 深色背景、溫暖的核心、考究的調色板。 經典的琥珀色黑底(CRT / 終端)可行,冷白色炭灰底(編輯風格)和低飽和度 pastel 色(risograph 印刷風格)也可行。選擇一種並堅持到底。
- 比例字體是重點。 Pretext 的核心氛圍是“非等寬” — 充分利用這一點。使用 Iowan Old Style、Inter、JetBrains Mono、Helvetica Neue 或可變字體。切勿默認使用無襯線字體。
- 使用真實的來源/文本,而非 lorem ipsum。 語料庫應具有意義。簡短的宣言、詩歌、真實的源代碼、找到的文本、庫自身的 README — 絕不使用
lorem ipsum。 - 首屏卓越表現。 無加載狀態,無空白幀。演示必須在打開瞬間看起來即可發佈。
技術棧
每個演示均為單個自包含的 HTML 文件。無構建步驟。
| 層級 | 工具 | 用途 |
|---|---|---|
| 核心 | 通過 esm.sh CDN 引入的 @chenglou/pretext | 文本測量 + 行佈局 |
| 渲染 | HTML5 Canvas 2D | 字形渲染、每幀構圖 |
| 分割 | Intl.Segmenter(內置) | 用於 emoji / CJK / 組合標記的字素分割 |
| 交互 | 原始 DOM 事件 | 鼠標 / 觸摸 / 滾輪 — 無框架 |
<script type="module">
import {
prepare, layout, // use-case 1: simple height
prepareWithSegments, layoutWithLines, // use-case 2a: fixed-width lines
layoutNextLineRange, materializeLineRange, // use-case 2b: streaming / variable width
measureLineStats, walkLineRanges, // stats without string allocation
} from "https://esm.sh/@chenglou/pretext@0.0.6";
</script>
鎖定版本。撰寫時為 @0.0.6 — 如果演示行為異常,請檢查 npm 獲取最新版本。
兩種用例
幾乎所有情況都可歸結為以下兩種模式之一。掌握兩者。
用例 1 — 測量,然後使用 CSS/DOM 渲染
const prepared = prepare(text, "16px Inter");
const { height, lineCount } = layout(prepared, 320, 20);
你仍然讓瀏覽器繪製文本。Pretext 僅告知你在給定寬度下盒子的高度,無需讀取 DOM。適用場景:
- 包含換行文本的虛擬化列表
- 需要精確卡片高度的瀑布流佈局
- 開發時檢查“此標籤是否適配?”
- 防止遠程文本加載時的佈局偏移
保持 font 和 letterSpacing 與你的 CSS 完全同步。 Canvas ctx.font 格式(例如 "16px Inter"、"500 17px 'JetBrains Mono'")必須與渲染後的 CSS 匹配,否則測量結果會出現偏差。
用例 2 — 自行測量 並 渲染
const prepared = prepareWithSegments(text, FONT);
const { lines } = layoutWithLines(prepared, 320, 26);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26);
}
這是發揮創意的地方。你掌控繪圖過程,因此可以:
- 渲染到 Canvas、SVG、WebGL 或任何座標系
- 替換每個字形的變換(旋轉、抖動、縮放、不透明度)
- 使用行元數據(寬度、字素位置)作為幾何數據
對於 每行可變寬度 的流式佈局(圍繞形狀的文本、甜甜圈環帶中的文本、非矩形列中的文本):
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
let y = 0;
while (true) {
const lineWidth = widthAtY(y); // your function: how wide is the corridor at this y?
const range = layoutNextLineRange(prepared, cursor, lineWidth);
if (!range) break;
const line = materializeLineRange(prepared, range);
ctx.fillText(line.text, leftEdgeAtY(y), y);
cursor = range.end;
y += lineHeight;
}
這是整個庫中最重要的模式。它實現了“文本圍繞被拖動的精靈流動”——即在 X 上病毒式傳播的那個演示效果。
值得了解的輔助函數
measureLineStats(prepared, maxWidth)→{ lineCount, maxLineWidth }—最寬行的寬度,即多行收縮包裹寬度。walkLineRanges(prepared, maxWidth, callback)— 迭代各行而無需分配字符串。適用於不需要字符本身、僅需對字素進行統計/物理計算的場景。@chenglou/pretext/rich-inline— 相同的系統,但適用於混合字體 / 芯片標籤 / 提及內容的段落。從子路徑導入。
演示食譜模式
社區語料庫(參見 references/patterns.md)聚類為少數幾種強大的模式。選擇一種並進行即興創作——除非明確要求,否則不要發明新的類別。
| 模式 | 關鍵 API | 示例想法 |
|---|---|---|
| 圍繞障礙物重排 | layoutNextLineRange + 每行寬度函數 | 圍繞被拖動的光標精靈分開的編輯段落 |
| 文本即幾何遊戲 | layoutWithLines + 每行碰撞矩形 | 每個磚塊都是一個已測量單詞的打磚塊遊戲 |
| 碎裂 / 粒子效果 | walkLineRanges → 每字素 (x,y) → 物理引擎 | 點擊後爆炸成字母的句子 |
| ASCII 障礙物排版 | layoutNextLineRange + 每行測量的障礙物跨度 | 位圖 ASCII 標誌、形狀變形,以及使文本圍繞其實際幾何形狀敞開的可拖動線框對象 |
| 編輯多欄佈局 | 每欄使用 layoutNextLineRange + 共享光標 | 帶有引文的動畫雜誌跨頁 |
| 動態排版 | layoutWithLines + 隨時間變化的每行變換 | 星球大戰滾動字幕、波浪、彈跳、故障藝術效果 |
| 多行收縮包裹 | measureLineStats | 自動調整至最緊湊容器的引用卡片 |
參見 templates/donut-orbit.html 和 templates/hello-orb-flow.html 獲取可用的單文件起步模板。
工作流
- 從上述表格中選擇一種模式,基於用戶的需求簡報。
- 從模板開始:
templates/hello-orb-flow.html— 文本圍繞移動球體重排(圍繞障礙物重排模式)templates/donut-orbit.html— 高級示例:已測量的 ASCII 標誌障礙物、可拖動的線框球體/立方體、變形形狀場、可選中的 DOM 文本以及僅限開發的控件- 使用
write_file將內容寫入/tmp/或用戶工作區中的新.html文件。
- 替換語料庫,使用符合需求簡報有意選取的內容。使用真實散文,10-100 個句子,不要使用 Lorem Ipsum。
- 調整美學風格 — 字體、調色板、構圖、交互。這是核心工作,不要跳過。
- 本地驗證:
cd <dir-with-html> && python3 -m http.server 8765
# then open http://localhost:8765/<file>.html - 檢查控制檯 — 如果
prepareWithSegments被傳入錯誤的 font 字符串,pretext 將拋出錯誤;Intl.Segmenter在所有現代瀏覽器中均可用。 - 向用戶展示文件路徑,而不僅僅是代碼 — 他們想要打開它。
性能說明
prepare()/prepareWithSegments()是開銷較大的調用。對於每個文本+字體組合,僅執行 一次。緩存該句柄。- 在調整大小時,僅重新運行
layout()/layoutWithLines()— 切勿重新準備(re-prepare)。 - 對於文本不變但幾何形狀變化的每幀動畫,在緊密循環中調用
layoutNextLineRange的開銷足夠小,可以在 60fps 下為正常長度的段落每幀執行。 - 當每幀渲染 ASCII 掩碼時,保留一個單元格緩衝區(
Uint8Array/類型化數組),從單元格或投影幾何中推導每行測量的障礙物跨度,合併跨度,然後在繪製文本之前將這些跨度輸入layoutNextLineRange。 - 保持視覺動畫與佈局動畫耦合。如果球體變形為立方體,請使用相同的值對渲染的單元格緩衝區和障礙物跨度進行補間動畫;否則,演示看起來像是畫上去的,而不是物理重排的。
- 對於淡入淡出效果,優先使用圖層不透明度,而不是改變字形強度或障礙物縮放。將瞬態 ASCII 精靈放在單獨的 Canvas 上,並使用 CSS/GSAP 不透明度淡化該 Canvas,這樣幾何形狀就不會顯得縮小。
- Canvas
ctx.font設置出乎意料地慢;如果字體不變,請每幀設置 一次,而不是每次fillText調用都設置。
常見陷阱
-
CSS/Canvas 字體字符串不一致。
ctx.font = "16px Inter"已測量,但 CSS 聲明為font-family: Inter, sans-serif; font-size: 16px。如果 Inter 字體加載成功,這沒問題。但如果 Inter 返回 404,CSS 會回退到 sans-serif,導致測量值產生 5-20% 的偏差。務必對字體使用preload,或使用 Web 安全字體族。 -
在動畫循環中重複準備(prepare)。 只有
layout*操作是低開銷的。每幀重新調用prepare會嚴重損害性能。請將準備好的句柄保持在模塊作用域內。 -
忘記使用
Intl.Segmenter進行字素(grapheme)分割。 對於 Emoji、組合標記、CJK 字符——"é".split("")會得到兩個字符。在採樣單個可見字形時,請使用new Intl.Segmenter(undefined, { granularity: "grapheme" })。 -
使用
break: 'never'的芯片(chip)未設置extraWidth。 在rich-inline中,如果對原子芯片/提及(mention)使用break: 'never',還必須為藥丸狀內邊距提供extraWidth——否則芯片的裝飾部分會溢出容器。 -
從
unpkg引入僅包含 TypeScript 入口的@chenglou/pretext。 請使用esm.sh——它會自動將 TS 導出編譯為瀏覽器可用的 ESM。unpkg會返回 404 或提供原始 TS 文件。 -
等寬字體回退 silently 抹殺了整體效果。 用戶看到類似等寬字體的輸出,通常是因為 CSS
font-family回退到了monospace。請通過開發者工具驗證實際渲染的字體。 -
環繞形狀排版時,跳過行 vs 調整寬度。 如果當前行的通道太窄而無法容納一行文本,請跳過該行(
y += lineHeight; continue;),而不是向layoutNextLineRange傳遞極小的 maxWidth——否則 pretext 會返回看起來破碎的單字素行。 -
發佈未經打磨的演示。 默認的首屏繪製效果看起來像教程級別。請添加:暗角效果、細微的掃描線、空閒自動運動、一個精心選擇的交互響應(拖拽、懸停、滾動、點擊)。如果沒有這些,“酷炫的 pretext 演示”會被視為“README 的實習生復現版”。
驗證清單
- 演示是一個獨立的
.html文件——可通過雙擊或python3 -m http.server打開 - 通過
esm.sh引入@chenglou/pretext並固定版本 - 語料庫是真實的散文,而非 Lorem Ipsum,且與演示概念相符
- 傳遞給
prepare的字體字符串與 CSS 字體完全匹配 -
prepare()/prepareWithSegments()僅調用一次,而非每幀調用 - 深色背景 + 經過考究的調色板——而非默認的白色畫布
- 至少有一個交互響應(拖拽 / 懸停 / 滾動 / 點擊)或空閒自動運動
- 已在本地使用
python3 -m http.server測試並確認無控制檯錯誤 - 在中端筆記本電腦上達到 60fps(或已記錄優雅降級方案)
- 一個用戶未要求的“額外用心”細節
參考:社區演示
克隆這些項目以獲取靈感/模式(均為類 MIT 許可證,鏈接來自 pretext.cool):
- Pretext Breaker —— 使用單詞磚塊的打磚塊遊戲 ——
github.com/rinesh/pretext-breaker - Tetris × Pretext ——
github.com/shinichimochizuki/tetris-pretext - Dragon animation ——
github.com/qtakmalay/PreTextExperiments - Somnai editorial engine ——
github.com/somnai-dreams/pretext-demos - Bad Apple!! ASCII ——
github.com/frmlinn/bad-apple-pretext - Drag-sprite reflow ——
github.com/dokobot/pretext-demo - Alarmy editorial clock ——
github.com/SmisLee/alarmy-pretext-demo
官方遊樂場:chenglou.me/pretext —— accordion, bubbles, dynamic-layout, editorial-engine, justification-comparison, masonry, markdown-chat, rich-note.