Pretext API 记录

0 阅读4分钟

Pretext API 记录

仓库地址: chenglou/pretext

许可证: MIT

描述: 高性能纯 JavaScript/TypeScript 库,用于多行文本测量和布局计算。通过绕过 DOM 测量(如 getBoundingClientRect)来避免昂贵的布局重排。


目录


核心 API 列表

基础测量 API

API说明返回值
prepare(text, font, options?)准备工作,标准化文本并测量,返回不透明句柄PreparedText
layout(prepared, maxWidth, lineHeight)基于已准备文本计算高度(纯算术操作){ height, lineCount }
prepare 参数
参数类型说明
textstring要测量的文本
fontstring字体字符串(与 Canvas Context font 属性格式一致,如 '16px Inter'
optionsobject?可选配置
options.whiteSpace'normal' | 'pre-wrap''normal'(默认)保留空格和换行;'pre-wrap' 保留空格、Tab 和换行符
layout 参数
参数类型说明
preparedPreparedTextprepare() 返回的句柄
maxWidthnumber最大宽度(像素)
lineHeightnumber行高(像素,需与 CSS line-height 同步)

进阶手动布局 API

API说明返回值
prepareWithSegments(text, font, options?)类似 prepare,返回更丰富的分段结构PreparedTextWithSegments
layoutWithLines(prepared, maxWidth, lineHeight)固定宽度的布局{ height, lineCount, lines: LayoutLine[] }
walkLineRanges(prepared, maxWidth, onLine)底层 API,遍历行范围(无字符串构建)number
layoutNextLine(prepared, start, maxWidth)迭代器风格,逐行布局(支持动态宽度)LayoutLine | null

辅助函数

API说明返回值
clearCache()清除内部共享缓存void
setLocale(locale?)设置本地化环境(自动调用 clearCachevoid

类型定义

// 单行文本信息
type LayoutLine = {
  text: string       // 该行的完整文本内容
  width: number      // 测量得到的宽度
  start: LayoutCursor // 起始游标
  end: LayoutCursor   // 结束游标
}

// 行范围信息(不含 text 字符串)
type LayoutLineRange = {
  width: number
  start: LayoutCursor
  end: LayoutCursor
}

// 游标位置
type LayoutCursor = {
  segmentIndex: number  // 片段索引
  graphemeIndex: number // 片段内的字素索引
}

使用示例

示例 1:测量文本高度(不触碰 DOM)

适用于虚拟化列表、防止布局偏移等场景。

import { prepare, layout } from '@chenglou/pretext'

// 1. 准备文本(只需执行一次,除非文本或字体改变)
const prepared = prepare('AGI 春天到了. 시작했다 🚀', '16px Inter')

// 2. 计算高度(纯算术,可在 resize 事件中频繁调用)
const { height, lineCount } = layout(prepared, textWidth, 20)

// 结果即为文本在给定宽度和行高下的精确高度

示例 2:保留格式的文本(类似 Textarea)

适用于需要保留 \t\n 和空格的场景。

const prepared = prepare(textareaValue, '16px Inter', {
  whiteSpace: 'pre-wrap'  // 保留 \t, \n 和空格
})

const { height } = layout(prepared, textareaWidth, 20)

示例 3:手动渲染到 Canvas

适用于需要精确控制每一行绘制的情况。

import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments('AGI 春天到了.开始了 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26) // 320px 宽, 26px 行高

// 遍历并绘制
for (let i = 0; i < lines.length; i++) {
  ctx.fillText(lines[i].text, 0, i * 26)
}

示例 4:文本绕排(动态宽度)

适用于文本周围有浮动图片等复杂布局。

import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'

const prepared = prepareWithSegments(text, font, options)
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0

while (true) {
  // 假设图片占据了一定高度和宽度,根据当前 y 决定行宽
  const width = y < image.bottom ? columnWidth - image.width : columnWidth

  const line = layoutNextLine(prepared, cursor, width)
  if (line === null) break

  ctx.fillText(line.text, 0, y)
  cursor = line.end // 更新游标到下一行开头
  y += 26
}

示例 5: speculative testing(二分查找最佳宽度)

使用 walkLineRanges 在不构建文本字符串的情况下遍历行宽和游标。

import { prepare, walkLineRanges } from '@chenglou/pretext'

const prepared = prepare(text, '16px Inter')

// 二分查找最佳宽度
let low = 100
let high = 500

while (high - low > 1) {
  const mid = Math.floor((low + high) / 2)

  walkLineRanges(prepared, mid, (lineRange) => {
    // 处理每一行的范围信息
    console.log(lineRange.width)
  })

  // 根据实际需求调整 low/high
  // ...
}

注意事项

1. 性能基准

操作耗时(500 个文本批次)
prepare()~19ms
layout()~0.09ms

2. 多语言支持

  • 支持几乎所有语言(包括 Emoji 和混合双向文本)
  • 使用前可调用 setLocale() 设置本地化环境

3. macOS 系统字体问题

在 macOS 上,system-ui 可能会导致 layout() 精度问题,建议使用命名字体(如 '16px Inter')。

4. 字体同步

lineHeight 参数需要与 CSS 的 line-height 保持同步,否则计算结果可能不准确。


安装

npm install @chenglou/pretext

yarn add @chenglou/pretext