Daniel's Blog
4 min read

给博客页眉加一个双摆动画:混沌系统与用户体验的平衡

Table of Contents

TL;DR

做了什么:在博客页眉添加了一个双摆(Double Pendulum)动画,替代原来的 emoji。

技术亮点

  • 使用拉格朗日力学推导运动方程,四阶龙格-库塔积分保证数值稳定性
  • 全局单例模式保存状态,页面导航时双摆从当前位置继续运动
  • 进入文章页时非线性减速停止(easeOutCubic),避免分散阅读注意力

设计理念:动画应该是锦上添花,而不是喧宾夺主。


为什么是双摆?

双摆是混沌理论的经典演示系统。两个简单的摆锤串联在一起,却能产生极其复杂、永不重复的运动轨迹。它完美诠释了「简单规则产生复杂行为」的哲学——这也是我对编程和写作的理解。

而且,双摆很好看。


第一阶段:让它动起来

物理建模

双摆的运动方程可以通过拉格朗日力学推导。设两个摆的角度分别为 θ₁ 和 θ₂,角速度为 ω₁ 和 ω₂,则角加速度为:

function derivatives(θ1, θ2, ω1, ω2) {
  const Δ = θ2 - θ1;
  const sinΔ = Math.sin(Δ), cosΔ = Math.cos(Δ);
  const sin1 = Math.sin(θ1), sin2 = Math.sin(θ2);
  const M = m1 + m2;
  const d = m1 + m2 * sinΔ * sinΔ;

  const α1 = (m2 * g * Math.sin(θ2) * cosΔ
    - m2 * sinΔ * (l1 * ω1 * ω1 * cosΔ + l2 * ω2 * ω2)
    - M * g * sin1) / (l1 * d);

  const α2 = (M * (l1 * ω1 * ω1 * sinΔ - g * sin2 + g * sin1 * cosΔ)
    + m2 * l2 * ω2 * ω2 * sinΔ * cosΔ) / (l2 * d);

  return { α1, α2 };
}

这些方程看起来复杂,但本质上就是牛顿第二定律在旋转系统中的应用。

数值积分:龙格-库塔法

简单的欧拉法(θ += ω * dt)在双摆这样的混沌系统中会快速积累误差,导致能量不守恒。我使用了四阶龙格-库塔(RK4)积分:

const k1 = derivatives(θ1, θ2, ω1, ω2);
const k2 = derivatives(θ1 + 0.5*dt*ω1, θ2 + 0.5*dt*ω2, ...);
const k3 = derivatives(...);
const k4 = derivatives(...);

ω1 += (dt/6) * (k1.α1 + 2*k2.α1 + 2*k3.α1 + k4.α1);
ω2 += (dt/6) * (k1.α2 + 2*k2.α2 + 2*k3.α2 + k4.α2);

RK4 通过在每个时间步内多次采样斜率,大大提高了精度。

第一个问题:动作不流畅

最初我限制了 30fps 以「节省性能」,结果动画看起来很卡。双摆的运动速度变化很大,低帧率会让快速运动的部分显得跳跃。

解决方案:使用固定时间步长的物理模拟 + 60fps 渲染。

const physicsStep = 0.008; // 固定物理步长
let accumulator = 0;

function animate(currentTime) {
  const deltaTime = (currentTime - lastTime) / 1000;
  accumulator += deltaTime;

  // 物理模拟使用固定步长
  while (accumulator >= physicsStep) {
    step(physicsStep);
    accumulator -= physicsStep;
  }

  draw(); // 渲染使用实际帧率
  requestAnimationFrame(animate);
}

这样物理模拟的精度不受帧率影响,而渲染始终保持流畅。


第二阶段:让它在合适的时候停下来

问题:动画分散注意力

双摆动画在首页很酷,但当用户点进一篇文章准备阅读时,一个不停晃动的东西就变成了干扰。

我希望:

  • 在首页/列表页:双摆正常摆动
  • 进入文章页:双摆逐渐停止
  • 返回首页:双摆恢复摆动

非线性停止

直接让双摆「冻结」会很突兀。我选择了 easeOutCubic 缓动函数实现非线性减速:

function easeOutCubic(t) {
  return 1 - Math.pow(1 - t, 3);
}

// 在物理模拟中应用阻尼
if (isStopping) {
  stopElapsed += dt;
  const progress = Math.min(stopElapsed / 1.5, 1.0); // 1.5秒停止
  damping = 1.0 - easeOutCubic(progress);

  ω1 = ω1 * damping;
  ω2 = ω2 * damping;
}

easeOutCubic 的特点是「快速开始,缓慢结束」——双摆会迅速减速,然后慢慢悠悠地停在某个姿态。这比线性减速更符合物理直觉(想象一下钟摆在空气阻力下停止的样子)。

状态持久化的挑战

Astro 的页面导航会重新执行脚本,双摆的状态(角度、角速度)会丢失。最初我尝试用 sessionStorage 保存状态:

// 导航前保存
sessionStorage.setItem('pendulum-state', JSON.stringify({θ1, θ2, ω1, ω2}));

// 新页面加载后恢复
const saved = JSON.parse(sessionStorage.getItem('pendulum-state'));

问题sessionStorage 是同步操作,在 astro:before-preparation 事件中写入会阻塞导航,让页面切换感觉变慢。

解决方案:使用全局单例模式,状态保存在 window 对象上:

const G = window as any;
if (!G.__pendulum) {
  G.__pendulum = {
    θ1: Math.PI * 0.75,
    θ2: Math.PI * 0.5,
    ω1: 0,
    ω2: 0,
    // ...
  };
}
const P = G.__pendulum;

因为 Astro 的 ClientRouter 是 SPA 导航,window 对象在页面间是共享的。这样状态自然就持久化了,而且没有任何 I/O 开销。


第三阶段:性能优化

问题:事件监听器泄漏

每次页面导航后,astro:page-load 事件会触发脚本重新执行,导致事件监听器被重复添加。十几次导航后,一个点击事件可能触发十几个处理函数。

解决方案:用标志位确保监听器只注册一次:

if (!P.initialized) {
  P.initialized = true;

  document.addEventListener('astro:before-preparation', ...);
  document.addEventListener('astro:page-load', ...);
}

优化:页面隐藏时暂停

用户切换到其他标签页时,没必要继续计算和渲染:

function animate(currentTime) {
  if (document.hidden) {
    requestAnimationFrame(animate);
    return; // 跳过计算和渲染
  }
  // ...
}

优化:完全停止后停止动画循环

当双摆完全停止且轨迹消失后,继续运行 requestAnimationFrame 是浪费:

if (P.isStopping && P.ω1 === 0 && P.ω2 === 0 && P.trail.length === 0) {
  draw(); // 最后画一帧
  P.animId = 0;
  return; // 停止动画循环
}

设计思考:动画与注意力

动画是配角,不是主角

我见过太多网站把动画做成「炫技展示」——全屏粒子特效、无处不在的视差滚动、加载时的 3D 旋转 Logo。这些在技术上很厉害,但往往让用户感到疲惫。

双摆动画的设计原则是:你可以注意到它,但你不必注意它

  • 尺寸很小(28px),不会抢占视觉焦点
  • 颜色跟随主题,不会突兀
  • 透明度 75%,hover 时才变为 100%
  • 进入阅读模式时自动停止

非线性停止的 UX 考量

为什么要做非线性停止,而不是直接消失?

  1. 连续性:突然消失会让用户产生「刚才那个东西去哪了」的疑惑
  2. 物理直觉:现实中没有东西会瞬间静止,非线性减速更符合预期
  3. 优雅降级:即使停止动画的代码出 bug,双摆也只是会一直摆动,不会崩溃

一些心得

1. 物理模拟比看起来难

双摆方程推导不难,但让它在浏览器里稳定运行需要考虑:数值精度、帧率波动、状态持久化、性能优化。每一个都是坑。

技术上我可以让双摆一直转、轨迹更长、颜色更鲜艳。但这些都会让它从「有趣的装饰」变成「烦人的干扰」。好的动画是克制的。

4. 小细节值得打磨

这个双摆动画大概只有 1% 的访客会注意到。但正是这些 1% 的细节,构成了一个网站的「质感」。


最终效果

如果你现在看向页面左上角,应该能看到一个小小的双摆在摆动。点击任意一篇文章,它会慢慢停下来。返回首页,它又会恢复摆动。

就这样。一个小小的、不打扰人的、物理正确的双摆。