FLIP 动画入门了解

最近看到一个词 “FLIP 动画”,我还以为是什么新奇的技术,点击去看了一眼,原来是 CSS3 的相关技术的应用。

具体解释,我就从其他地方 Copy 的一段:

CSS FLIP 动画是一种使用 CSS 过渡和变换属性来创建流畅动画效果的技术。FLIP 是 “First, Last, Invert, Play” 的缩写,它描述了一种优化动画性能的方法。

FLIP 动画的基本原理是在动画开始和结束时,通过获取元素的初始状态和最终状态的位置、大小和其他属性,然后通过 CSS 变换和过渡来实现平滑的动画效果。

具体步骤如下:

  • First(首次):获取元素的初始状态(位置、大小、样式等)。
  • Last(最后):获取元素的最终状态(位置、大小、样式等)。
  • Invert(反转):通过 CSS 变换将元素的最终状态反转到初始状态。
  • Play(播放):应用 CSS 过渡,将元素从初始状态平滑地过渡到最终状态。

通过使用 FLIP 技术,可以减少动画期间的重排和重绘操作,提高动画的性能和流畅度。这种技术特别适用于需要在页面布局改变时进行动画的情况,如元素的位置变化、大小变化或样式变化等。

请注意,FLIP 动画需要一些 JavaScript 代码来计算元素的初始状态和最终状态,并应用相应的 CSS 类和样式

简而言之,言而简之!就是利用 CSS3 的动画特性,通常只要把起始、结束两个状态告诉浏览器,渲染引擎(浏览器)会自动控制动画序列,从而实现动画,无需额外 JS

FLIP 动画 为了得到起始、结束的状态需要 JS 配合,初始状态 容易得到,但是为了得到 结束状态 ,就得让元素放置到 结束状态 的位置,正常情况下,直接放置是不会有任何动画,而且元素直接到了 结束状态

怎么让元素其实已经放置到 结束状态 ,但是看起来还是初始状态呢?这就是其中一个关键点,其实实现起来也很简答,利用 CSS3 位移属性,计算两种状态的 偏移距离, 然后利用 transform: translate(X px, Y px) 在移动回去, 让视觉上看起来实际没有移动。(这其实就是所谓的 “初始状态”)

接下来就是让动画动起来,就是要设置 结束动画属性,当前位置不就是结束位置么?压根不用动,直接把 位移值设置为 0 - transform: translate(0px, 0px) 即可. 开始-结束 都有了,动画不就自动开始了。

这个时候你如过直接设置,可能会发现毫无效果,为啥呢??你这样直接设置,肯定会在同一帧内渲染,根本看不到效果(具体为啥,去了解 JS 阻塞渲染 相关知识),我们可以利用一些异步方式,或者强制渲染一次,然后在第二次渲染的时候 设置结束属性,例如: requestAnimationFrame, setTimeout 等都可以。

具体 demo 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flip 动画实现</title>
<style>
body {
font-family: -apple-system, "Helvetica Neue", "Arial", "PingFang SC",
"Hiragino Sans GB", "STHeiti", "Microsoft YaHei",
"Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC",
"Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC",
"Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif;
}

.container {
width: 320px;
height: 320px;
background-color: blanchedalmond;
}

.item {
width: 80px;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 30px;
}

.item[data-key="1"],
.item[data-key="6"],
.item[data-key="11"],
.item[data-key="14"] {
background-color: tomato;
color: #fff;
}

.item[data-key="2"],
.item[data-key="7"],
.item[data-key="16"] {
background-color: #999933;
color: #fff;
}

.item[data-key="3"],
.item[data-key="10"],
.item[data-key="12"] {
background-color: #ff0033;
color: #fff;
}

.item[data-key="4"],
.item[data-key="9"],
.item[data-key="15"] {
background-color: #003366;
color: #fff;
}

.item[data-key="8"],
.item[data-key="13"] {
background-color: #0099cc;
color: #fff;
}

.item[data-key="8"],
.item[data-key="15"],
.item[data-key="5"] {
background-color: #996600;
color: #fff;
}

/* grid 布局 */
.grid-wrap {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
</style>
</head>

<body>
<h1>
最新看来一个新东西(对我而言是新东西)Flip
动画,秉着好奇的心理,打开看了一眼,原来是这样的!
</h1>
<p></p>
<hr />
<button id="change1">变化</button>
<div class="container normal-wrap">
<div class="item" data-key="1">1</div>
<div class="item" data-key="2">2</div>
<div class="item" data-key="3">3</div>
<div class="item" data-key="4">4</div>
</div>

<hr />
<button id="change2">变化</button>
<div class="container grid-wrap">
<div class="item" data-key="1">1</div>
<div class="item" data-key="2">2</div>
<div class="item" data-key="3">3</div>
<div class="item" data-key="4">4</div>
<div class="item" data-key="5">5</div>
<div class="item" data-key="6">6</div>
<div class="item" data-key="7">7</div>
<div class="item" data-key="8">8</div>
<div class="item" data-key="9">9</div>
<div class="item" data-key="10">10</div>
<div class="item" data-key="11">11</div>
<div class="item" data-key="12">12</div>
<div class="item" data-key="13">13</div>
<div class="item" data-key="14">14</div>
<div class="item" data-key="15">15</div>
<div class="item" data-key="16">16</div>
</div>
</body>
<script>
// const item = document.querySelector('.item[data-key="1"]')
const wrap = document.querySelector(".normal-wrap");
const btn = document.getElementById("change1");

const itemsCache = new Map();

btn.addEventListener("click", () => {
raffle();
});

function delay(d = 1000) {
const n = Date.now();
while (Date.now() - n < d) {}
}

function calcPos(dom) {
return dom.getBoundingClientRect();
}

function item(wrap, index) {
if (!itemsCache[index]) {
itemsCache[index] = wrap.querySelector(`.item[data-key="${index}"]`);
}
return itemsCache[index];
}

function itemV2(dom) {
return (k) => {
return dom.querySelector(`.item[data-key="${k}"]`);
};
}

let index = [1, 2, 3, 4];
function raffle() {
const posBegin = {};
const postLast = {};

// 初始位置
index.forEach((k) => {
posBegin[k] = calcPos(item(wrap, k));
});

// 移动元素
index = index.sort(() => Math.random() - 0.5);
index.forEach((k) => {
wrap.append(item(wrap, k));
});

// Invert 计算位移
const diff = {};
index.forEach((k) => {
// 计算移动后位置
postLast[k] = calcPos(item(wrap, k));

// 获取偏移量
diff[k] = {
x: postLast[k].left - posBegin[k].left,
y: postLast[k].top - posBegin[k].top,
};
});

Object.keys(diff).forEach((k) => {
// 设置偏移样式
item(wrap, k).style.transform = `translate(${-diff[k].x}px, ${-diff[k]
.y}px) scale(0.9)`;
item(wrap, k).style.transition = "none";
});

window.requestAnimationFrame(() => {
index.forEach((k) => {
item(wrap, k).style.transform = `translate(0,0)`;
item(wrap, k).style.transition = "transform .5s";
});
});
}

// v2 grid 布局
let index2 = Array.from({ length: 16 })
.fill(0)
.map((v, i) => i + 1);
const wrap2 = document.querySelector(".grid-wrap");
const btn2 = document.querySelector("#change2");

btn2.addEventListener("click", () => {
raffle2();
});

function raffle2() {
const _item = itemV2(wrap2);
const posBegin = {};
const postLast = {};

// 初始位置
index2.forEach((k) => {
posBegin[k] = calcPos(_item(k));
});

// 移动元素
index2 = index2.sort(() => Math.random() - 0.5);
index2.forEach((k) => {
wrap2.append(_item(k));
});

// Invert 计算位移
const diff = {};
index2.forEach((k) => {
// 计算移动后位置
postLast[k] = calcPos(_item(k));

// 获取偏移量
diff[k] = {
x: postLast[k].left - posBegin[k].left,
y: postLast[k].top - posBegin[k].top,
};
});

Object.keys(diff).forEach((k) => {
// 设置偏移样式
_item(k).style.transform = `translate(${-diff[k].x}px, ${-diff[k]
.y}px) scale(0.9)`;
_item(k).style.transition = "none";
});

window.requestAnimationFrame(() => {
index2.forEach((k) => {
_item(k).style.transform = `translate(0,0)`;
_item(k).style.transition = "transform .5s";
});
});

console.log("a");
}
</script>
</html>

这是一个简易 demo,没有通用性,只是为了演示具体原理。好多大佬封装了一些通用库来实现这个功能,可去 Github 上找找。

作者

Fat Dong

发布于

2023-10-12

更新于

2023-10-12

许可协议