震惊!字符串还能这么玩!

效果预览

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

预览地址:liush.top/textString/

核心原理

这个效果主要基于 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/


有需要代码可以留言或私信我

相关推荐
Setsuna_F_Seiei5 小时前
前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力)
前端·agent
刘发财7 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理7 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡8 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai9 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
计算机魔术师10 小时前
特朗普上台打给黄仁勋:AI末日论是骗局,我们绝不让它发生
前端
troy12810 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析
计算机魔术师10 小时前
CEO说要慢下来,黑客说别做梦了——同一篇论文,两种命运
前端
kyriewen11 小时前
我花3天抓了一个幽灵bug,凶手藏在第4层
前端·javascript·程序员
IT_陈寒12 小时前
Java里用Stream.parallel()翻车实录,这性能还不如单线程
前端·人工智能·后端