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.