01-整体架构与流程
OptimalTypography 代码分析总览
概述
OptimalTypography.tsx 是一个基于 Knuth-Plass 算法的高质量文本排版组件,通过 Canvas 渲染实现比浏览器默认行为更优的段落布局。其核心目标是消除文本排版中恼人的"河流"(rivers of white space)现象。
技术栈
| 技术 | 用途 |
|---|---|
| Canvas 2D | 高性能文本渲染 |
| Knuth-Plass 算法 | 最优断行优化(动态规划) |
@chenglou/pretext | 软连字符预处理 |
| 动态规划 DP | 寻找最小成本断点组合 |
核心数据流
flowchart TD
subgraph 输入层
A[PARAGRAPHS 原始文本] --> B[hyphenateWord 断词处理]
B --> C[prepareWithSegments 软连字符]
C --> D[segments + widths 预处理结果]
end
subgraph 算法层
D --> E[optimalLayout Knuth-Plass]
E --> F[breakCandidates 候选断点]
E --> G[DP 动态规划寻优]
G --> H[lines 最优行布局]
end
subgraph 渲染层
H --> I[renderCanvas Canvas渲染]
I --> J[setupCanvas 高清适配]
I --> K[文本填充 + 两端对齐]
K --> L[Canvas 绘制完成]
end
style A fill:#e1f5ff
style E fill:#fff3e0
style I fill:#e8f5e9
核心方法一览
| 方法名 | 职责 | 行号 |
|---|---|---|
hyphenateWord() | 单词连字符拆分(词典+前缀+后缀) | 242-268 |
setupCanvas() | Canvas 高清适配(devicePixelRatio) | 271-280 |
optimalLayout() | Knuth-Plass 核心算法 | 283-407 |
lineBadness() | 行质量评估(惩罚值计算) | 323-341 |
renderCanvas() | 渲染流水线编排 | 410-495 |
断词策略详解
三层断词机制
flowchart LR
A[输入单词] --> B{异常词典<br/>HYPHEN_EXCEPTIONS}
B -->|命中| C[使用词典拆分]
B -->|未命中| D{前缀匹配<br/>PREFIXES}
D -->|匹配且残余≥3| E[前缀切分]
D -->|不匹配| F{后缀匹配<br/>SUFFIXES}
F -->|匹配且残余≥3| G[后缀切分]
F -->|不匹配| H[原样返回]
C --> I[返回拆分数组]
E --> I
G --> I
H --> I
断词优先级
- 异常词典优先 (
HYPHEN_EXCEPTIONS) — 精确控制特定单词的拆分方式 - 前缀匹配次之 — 如 "unexpected" → ["un", "expected"]
- 后缀匹配第三 — 如 "reading" → ["read", "ing"]
- 兜底返回原词 — 长度 < 5 或不符合上述条件
为什么要拆分?
- 拆分后的片段可以在行尾/行首断裂,允许更灵活的断行选择
- 与
prepareWithSegments配合添加软连字符(U+00AD) - 增加断点候选数量,从而找到更优的布局
Knuth-Plass 算法核心思想
Knuth-Plass 是一个全局优化算法,不像浏览器贪婪地从左到右填入每行,而是:
- 构建所有可行断点的图
- 寻找最短路径 — 最小化整体"不良度"
- 考虑段落所有行的联合优化
graph TD
subgraph 图构建
A["segments 序列"] --> B["识别 breakCandidates<br/>单词边界 + 软连字符位置"]
B --> C["为每对候选计算<br/>wordWidth + spaceCount"]
end
subgraph DP求解
C --> D["dp[i] = 0, 其余∞"]
D --> E{"遍历所有候选j"}
E --> F{"遍历回溯i"}
F --> G["计算 lineBadness<br/>info -> cost"]
G --> H{"更新dp[j]"}
H -->|更小| I["dp[j] = cost<br/>prev[j] = i"]
H -->|不小| J["保持"]
I --> K["j++"]
J --> K
E --> K
end
subgraph 回溯
K -->|j达上限| L["从末尾回溯<br/>prev指针链"]
L --> M["得到breaks数组"]
end
style B fill:#fff3e0
style G fill:#ffcdd2
style L fill:#c8e6c9
行质量评估(Badness)
lineBadness() 是算法的核心代价函数,综合考虑:
惩罚项组成
flowchart LR
A[lineBadness] --> B[badness 基本不良度]
A --> C[river 河流惩罚]
A --> D[tight 紧凑惩罚]
A --> E[hyphen 连字符惩罚]
B -->|spacing过松<br/>ratio³×1000| F[总惩罚值]
C --> F
D --> F
E -->|额外+50| F
关键阈值
| 条件 | 处理 |
|---|---|
js < NORMAL_SPACE_W * 0.4 | 间距过紧 → 1e8 惩罚 |
js / NORMAL_SPACE_W > 1.5 | 出现"河流" → 额外 1e4 惩罚 |
| 最后一行超宽 | 允许但加 1e8 惩罚 |
高清渲染策略
flowchart TD
A[设定逻辑尺寸<br/>w × h] --> B[获取 devicePixelRatio]
B --> C[canvas.width = w × dpr]
B --> D[canvas.height = h × dpr]
C --> E[style.width = w + 'px']
D --> F[style.height = h + 'px']
E --> G[ctx.setTransform<br/>dpr, 0, 0, dpr, 0, 0]
F --> G
G --> H[物理像素精确渲染]
文件结构
技术规格与标准/OptimalTypography/
├── 01-整体架构与流程.md ← 本文件:总览 + 流程图
├── 02-断词机制详解.md ← hyphenateWord 深度分析
├── 03-Knuth-Plass算法.md ← optimalLayout + lineBadness
└── 04-渲染管线与复用.md ← renderCanvas + 扩展讨论