在文本布局发生变化时重新构建行组,让 SplitText 保持准确。对支持的触发条件使用自动拆分,为排版变化添加显式重建,并在 Mac 桌面预览中与未拆分的浏览器文本进行比较。

本指南面向使用 GSAP SplitText 编辑浏览器页面并采用流体标题的用户。任务是让每个生成的行包装元素始终与当前属于该视觉行的单词保持一致,包括标题宽度固定而字号变化的情况。

为什么流体排版会让行组过时?

生成的行包装元素会包住特定的一组单词。可用空间或排版发生变化后,可能需要重新构建这组单词。GSAP 文档说明,当拆分内容包含行时,autoSplit: true 可在字体加载完成和宽度变化时触发拆分。这些已说明的触发条件并不代表存在能监测所有排版变化的通用侦听器。SplitText 文档

考虑一个标题,其中包含 font-size: clamp(2rem, 4vw, 4rem),且容器宽度上限为 42rem。MDN 将 clamp() 定义为受最小值和最大值约束的首选值。在这里,即使容器达到宽度上限,首选字号仍会随视口宽度变化。clamp() CSS 函数

以下计算采用 16 CSS 像素的示例根字号,并假设可用空间足以让容器达到宽度上限。这些是计算得出的尺寸,并非浏览器测量结果或预测的行数。

视口宽度 容器宽度上限 计算所得字号
900 CSS px 672 CSS px 36 CSS px
1200 CSS px 672 CSS px 48 CSS px
1600 CSS px 672 CSS px 64 CSS px

容器宽度保持不变,而字母逐渐变大。由此可见,仅监测宽度无法涵盖这种情况。将计算所得的字号变化也视为需要重建的理由,然后检查单词的排布结果。

在相同尺寸和样式下,以浏览器未拆分文本的渲染结果作为准确性的基准。不要要求标题保持原有行数:流体布局可以生成不同的行。

更改动画前先建立未拆分的参照

在本地预览中创建两个相同的简短示例标题。为它们设置相同的宽度、字体、字号、字重、字距和换行规则。保持第一个标题原样,对第二个应用 SplitText。这个临时对比能让过时的分组一目了然,无需猜测某个单词应该在哪里换行。

先使用常规空白字符和换行规则。W3C CSS Text 规范定义了 white-space 如何控制空白折叠和软换行,也定义了独立的断行和间距规则。重建时应保留预期规则,而不是用手动插入的换行符替代。CSS Text Module Level 3

如果页面使用 Web 字体,请在首次拆分前等待 document.fonts.ready。MDN 文档说明,此 Promise 会在相关字体加载和布局操作完成后解析。这会建立初始测量基准,但不会锁定后续布局变化。FontFaceSet: ready 属性

同时检查浏览器实际使用的字体。MDN 区分已使用的字体和所有声明的字体:例如,某个可选字体如果错过了加载时机,就可能不会被使用。等待字体就绪不代表所有声明的字体都已成为实际渲染字体。Document: fonts 属性

实现自动拆分,再显式检查流体字号

以下完整的浏览器模块会创建一个未拆分的参照、一个动画副本和一个“切换字距”按钮。在现有 JavaScript 项目中使用一个临时预览页面,要求项目的打包器能解析 npm 导入、支持顶层 await,并在文档 body 存在后运行浏览器入口模块。请在 Mac 上安装 Node.js 和 npm。

在项目目录中运行以下命令安装 GSAP。此回调工作流要求使用 GSAP 3.13 或更高版本。GSAP 的安装文档介绍了 npm 安装和显式插件注册。安装

npm install gsap

用以下代码替换临时预览的浏览器入口模块。初始化方式遵循文档中的 SplitText.create() 模式。淡入效果使用 gsap.from(),从零不透明度渐变到原有外观。SplitText.create()、gsap.from()

import { gsap } from 'gsap';
import { SplitText } from 'gsap/SplitText';

gsap.registerPlugin(SplitText);

const style = document.createElement('style');
style.textContent = `
.flow-demo {
  width: min(100%, 42rem);
  font-family: system-ui, sans-serif;
}
.flow-demo h2 {
  font-size: clamp(2rem, 4vw, 4rem);
  line-height: 1.15;
  font-weight: 600;
  margin: 1rem 0;
  white-space: normal;
  text-wrap: wrap;
}
.flow-line { outline: 1px solid #aaa; }
`;
document.head.append(style);

const host = document.createElement('section');
host.className = 'flow-demo';
const reference = document.createElement('h2');
reference.textContent =
  'Panel Alpha holds a series of soft shapes beside Panel Beta.';
const target = reference.cloneNode(true);
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Toggle spacing';
host.append(reference, target, button);
document.body.append(host);

await document.fonts.ready;

const split = SplitText.create(target, {
  type: 'lines',
  linesClass: 'flow-line',
  autoSplit: true,
  onSplit(self) {
    return gsap.from(self.lines, {
      opacity: 0,
      duration: 0.3,
      stagger: 0.04
    });
  }
});

let timer;
let fontSize = getComputedStyle(target).fontSize;

function checkFluidSize() {
  clearTimeout(timer);
  timer = setTimeout(() => {
    const next = getComputedStyle(target).fontSize;
    if (next !== fontSize) {
      fontSize = next;
      split.split();
    }
  }, 150);
}
window.addEventListener('resize', checkFluidSize);

let spaced = false;
button.addEventListener('click', () => {
  spaced = !spaced;
  reference.style.letterSpacing = target.style.letterSpacing =
    spaced ? '0.06em' : 'normal';
  split.split();
});

动画在 onSplit() 中创建并返回,以便 SplitText 在重建时清理动画并保持同步。GSAP 文档说明,此回调工作流适用于 3.13 及更高版本。SplitText 文档

添加的 resize 处理程序只检查计算所得的字号,并在调整大小暂停后请求重建。150 毫秒的延迟是示例实现中的一个选择。此示例旨在让调整大小完成后行组保持准确;它不保证在每个中间调整帧中都能正确分组。

字距按钮演示了第二个显式触发条件。它会更改两个副本,然后调用 split.split()。GSAP 文档说明,必要时此方法会先还原现有拆分。在这个小示例中,宽度变化和字号变化也可能触发重叠的重建请求;如果性能分析发现较大页面存在不必要的工作,请将这些请求合并处理。split()

该模块已通过 JavaScript 语法检查,示例尺寸计算也已执行。这里没有运行浏览器布局和动画;请在本地预览中完成下方的视觉检查。

根据布局变化选择重建触发条件

示例会监测一个流体字号表达式。改用此方法时,请找出会更改文本的代码或布局事件,并在该变更发生的位置请求重建。不要假设示例能检测到无关的类名变化、容器驱动的排版变化或持续动画的字体属性。

页面中发生的变化 建议处理方式
文本元素宽度 启用文档说明的自动拆分行为,并检查结果。
固定文本宽度下由视口驱动的字号变化 调整大小后比较计算所得的字号,并在其变化时重建。
控件更改字距、字重或字体设置 控件应用新样式后请求重建。
排版取决于周围容器 监测提供尺寸输入的容器,并检查相关计算所得的排版属性。
组件在隐藏后变为可见 等组件达到预期的渲染尺寸后再初始化或重建。

对于由容器驱动的布局,ResizeObserver 可独立于视口调整报告元素尺寸变化。请监测提供尺寸输入的容器。仅监测文本框本身,不能视为能通知所有计算样式变化的机制。ResizeObserver

避免在每次监测到高度变化时都无条件重建。重建本身可能会影响布局。MDN 文档说明了 resize-observer 循环,并解释避免反馈循环需要控制依赖关系;将工作延迟到另一帧并不能消除潜在的无限循环。再次请求重建前,请先比较相关输入。ResizeObserver 观察错误

如果标题文本发生变化,请先还原旧拆分,再替换内容,然后根据新源文本初始化新实例。GSAP 文档说明,revert() 会恢复拆分前的原始内容。编辑生成的包装元素后再还原,可能会丢弃这些编辑。revert()

验证换行时不要改变浏览器的文字塑形

在 Mac 浏览器中打开本地预览,缓慢调整窗口大小,然后松开鼠标。等重建和淡入效果完成后,比较两个副本每行的最后一个单词。在容器达到宽度上限时,以及容器窄到需要收缩时,都重复检查。每种尺寸下都点击“切换字距”。

如果两个副本不一致,请按以下顺序排查:

  1. 确认两个副本的可用宽度和计算所得的排版属性相同。
  2. 在当前尺寸下重建。如果问题因此消失,请查找缺失的触发条件。
  3. 如果仍不一致,请移除生成的行包装元素上的样式后再测试。
  4. 恢复未拆分文本,并确认底层 CSS 会产生预期的换行。
  5. 一次更改一个布局输入,直到可以稳定复现差异。

让浏览器的文本引擎负责字符塑形。W3C 规范说明了哪些情况下塑形必须跨越行内边界继续进行,哪些情况下必须中断,包括使用行内 padding、边框或 margin 将其分隔的情况。这意味着包装元素样式会影响测量对象。不要在单个字符框之间添加间距,只为复现先前的截图。跨元素边界的塑形

不要通过在换行符处拆分 innerText 来计算视觉行数。WHATWG 将 innerText 定义为渲染文本,并规定了文本、显式换行符和结构边界的收集规则;它并不会提供软换行视觉行的数组。请将浏览器未拆分的渲染结果保留为对比基准。innerText 和 outerText 属性

请遵守库文档所述的限制:GSAP 建议拆分时避免使用 text-wrap: balance,并说明 SplitText 并非为 SVG 文本节点设计。对于均衡换行标题,请保持文本未拆分并为整个元素添加动画;对于行包装元素,请在 HTML 文本元素上使用常规换行。SplitText 提示与限制

当拆分副本在实际宽度和排版变化下都能与未拆分副本保持一致时,即可完成。将显式重建放在需要它的变更附近,排查时保留参照,并在将标题整合到最终页面前移除临时副本和行轮廓。

来源