Skip to content

FLIP Animation

menglingyu.213 edited this page Mar 15, 2026 · 1 revision

FLIP 动画原理

FLIP 是这个库实现平滑过渡动画的核心技术。

动画的难题

你想让一个元素从位置 A 平滑移动到位置 B:

位置 A                            位置 B
┌─────────┐                      ┌─────────────────┐
│  300x200 │  ───── 动画 ─────►  │     600x400      │
│  (100,200)│                     │    (400,50)      │
└─────────┘                      └─────────────────┘

直觉做法是用 JavaScript 算每一帧的中间值。但这样性能差——JS 计算 + 布局重排,每帧都在主线程上跑。

FLIP 的思路完全反过来:让浏览器用 GPU 加速的 CSS 来做动画。

F-L-I-P 四个步骤

First — 记录起始状态

const first = element.getBoundingClientRect();
// { left: 100, top: 200, width: 300, height: 200 }

Last — 让元素到达终态

直接把元素放到最终位置(不需要动画,瞬间完成):

targetContainer.appendChild(element);
const last = element.getBoundingClientRect();
// { left: 400, top: 50, width: 600, height: 400 }

Invert — 用 transform "假装"还在原位

计算差值,用 CSS transform 把元素"拉回"起始位置:

const deltaX = first.left - last.left;   // 100 - 400 = -300
const deltaY = first.top - last.top;     // 200 - 50 = 150
const deltaW = first.width / last.width; // 300 / 600 = 0.5

element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaW})`;
// 视觉上元素还在原位

Play — 去掉 invert,让浏览器补间

element.style.transition = 'transform 0.3s ease';
element.style.transform = ''; // 回到真实位置
// 浏览器自动做 GPU 加速的补间动画!

这个库的变体

标准 FLIP 用 transform,但这个库用的是 position: fixed + left/top/width/height 过渡。原因是:

  1. 元素需要脱离原容器 — 动画期间元素不属于任何容器,fixed 定位最合适
  2. 尺寸要真的变transform: scale 只是视觉缩放,不会改变元素实际布局尺寸,而播放器的控件需要按真实尺寸排列

实际流程:

1. 记录元素当前 rect                    { left:100, top:200, w:300, h:200 }
2. 从容器中取出,放到 <body>
3. position: fixed 在原位               left:100px, top:200px, w:300px, h:200px
4. 算出目标容器的 rect                   { left:400, top:50, w:600, h:400 }
5. 加 CSS transition
6. 改 left/top/width/height 到目标值     浏览器自动动画
7. transitionend 事件触发后
8. 去掉 fixed,appendChild 到目标容器

为什么要 force reflow

代码里有一行看起来奇怪的:

element.offsetHeight; // 强制重排

这是必需的。浏览器会把多次样式修改合并成一次重排。如果你先设 left: 100px 再立即改成 left: 400px,浏览器只会看到最终值 400px,不会有动画。

element.offsetHeight 强制浏览器在这个点做一次布局计算,相当于告诉它"把当前的 left: 100px 先画出来",然后再改成 400px 才会触发 transition。

兜底机制

transitionend 事件有时不触发(比如元素不可见、transition 属性无效、或者被其他样式覆盖)。所以有一个超时兜底:

const fallbackTimer = setTimeout(settle, 3000);

element.addEventListener('transitionend', () => {
  clearTimeout(fallbackTimer);
  settle();
}, { once: true });

3 秒内如果 transitionend 没触发,强制完成。不至于让元素永远浮在页面上。