效果预览
一个充满趣味的文字交互效果:文字像绳子一样串联在一起,你可以拖拽末端的文字,像拉绳子一样把整个文字串拉乱。按 F 键还能触发"解开"动画,让文字在重力作用下自然散落。

核心原理
这个效果主要基于 Verlet 积分 物理模拟算法,配合 距离约束 来保持文字之间的连接关系。
1. 数据结构定义
每个字符都是一个物理粒子,包含位置、速度、锁定状态等属性:
typescript
typescript
interface Letter {
ch: string; // 字符内容
w: number; // 字符宽度
x: number; // 当前位置 X
y: number; // 当前位置 Y
ox: number; // 原始位置 X(约束目标)
oy: number; // 原始位置 Y(约束目标)
px: number; // 上一帧位置 X(用于计算速度)
py: number; // 上一帧位置 Y
readingIdx: number; // 在阅读顺序中的索引
locked: boolean; // 是否锁定(锁定则不受物理影响)
}
2. 蛇形排列算法
为了让文字像绳子一样有自然的串联顺序,但视觉上又是正常的阅读顺序,使用了蛇形(Zig-Zag)排列:
ini
typescript
function buildZigzagMapping(maxWidth: number) {
// 先按行分组
const lineIndices: number[][] = [];
// ... 分行逻辑
// 奇数行反转,形成蛇形
for (let li = 0; li < lineIndices.length; li++) {
const reversed = needFlip ? li % 2 === 0 : li % 2 === 1;
if (reversed) {
stringOrder.push(...[...lineIndices[li]].reverse());
} else {
stringOrder.push(...lineIndices[li]);
}
}
return stringOrder;
}
这样,物理连接顺序是蛇形的,但每个字符记录了自己的 readingIdx,渲染时放在正确的视觉位置。
3. Verlet 积分物理模拟
核心物理循环使用 Verlet 积分,相比欧拉积分更稳定:
ini
typescript
// Verlet integration
for (let i = 0; i < letters.length; i++) {
const l = letters[i];
if (l.locked || isDragged(i)) continue;
// 计算速度(当前位置 - 上一帧位置)
const vx = (l.x - l.px) * DAMPING; // 阻尼系数 0.97
const vy = (l.y - l.py) * DAMPING;
// 更新上一帧位置
l.px = l.x;
l.py = l.y;
// 应用速度和重力
l.x += vx;
l.y += vy + GRAVITY; // 重力 0.15
}
4. 距离约束(绳子效果)
这是保持"绳子"感觉的关键。每帧迭代多次,强制相邻字符保持固定距离:
ini
typescript
// Distance constraints
for (let iter = 0; iter < ITERATIONS; iter++) { // 迭代 12 次
for (let i = 0; i < letters.length - 1; i++) {
const a = letters[i], b = letters[i + 1];
// 计算两字符中心点距离
const dist = Math.hypot(bx - ax, by - ay);
const diff = (dist - restLengths[i]) / dist;
// 根据锁定状态调整位置
// 一个锁定:只移动另一个
// 都未锁定:各移动一半
// 都锁定:跳过
}
}
5. 碰撞检测
防止非相邻字符重叠:
ini
typescript
// Letter-letter collision
const RADIUS = 8;
for (let i = 0; i < letters.length; i++) {
for (let j = i + 1; j < letters.length; j++) {
if (Math.abs(i - j) === 1) continue; // 跳过相邻的(由距离约束处理)
// 圆形碰撞检测,分离重叠字符
}
}
6. 交互逻辑
拖拽:使用 Pointer Events API 实现鼠标/触摸拖拽
ini
typescript
const handlePointerDown = (e: PointerEvent) => {
const idx = els.indexOf(e.target as HTMLSpanElement);
if (idx === -1 || letters[idx].locked) return;
drags.set(e.pointerId, {
idx,
offsetX: e.clientX - rect.left - letters[idx].x,
offsetY: e.clientY - rect.top - letters[idx].y,
});
};
解锁传播:拖拽一个字符时,如果拉得足够远,会"拉断"连接,解锁相邻字符:
ini
typescript
const dist = Math.hypot(dx, dy);
if (dist > restLengths[i] + UNLOCK_THRESHOLD) {
a.locked = false; // 解锁!
}
关键技术点总结
| 技术 | 用途 |
|---|---|
| Verlet 积分 | 稳定的位置-based 物理模拟 |
| 距离约束迭代 | 保持绳子般的连接感 |
| 蛇形排列 | 物理顺序与视觉顺序分离 |
| Pointer Events | 统一的鼠标/触摸交互 |
| 固定时间步长 | 120Hz 物理模拟保证一致性 |
参考
参考实现:pushmatrix.github.io/textstring/
有需要代码可以留言或私信我