如何用 CSS 和 GSAP 創作有多個關鍵幀的連續動畫
效果預覽
線上演示按下右側的“點選預覽”按鈕可以在當前頁面預覽,點選連結可以全屏預覽。
https://codepen.io/comehope/pen/eLMKJG
可互動視訊
此視訊是可以互動的,你可以隨時暫停視訊,編輯視訊中的程式碼。
請用 chrome, safari, edge 開啟觀看。
https://scrimba.com/p/pEgDAM/cdDRmH9
原始碼下載
本地下載每日前端實戰系列的全部原始碼請從 github 下載:
https://github.com/comehope/front-end-daily-challenges
程式碼解讀
定義 dom,容器中包含 10 個 div
子元素,每個 div
中包含 1 個 span
元素:
<figure class="container"> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> <div><span></span></div> </figure>
居中顯示:
body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: lightyellow;
}
定義容器的尺寸和樣式:
.container { width: 400px; height: 400px; background: linear-gradient(45deg, tomato, gold); border-radius: 3%; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); }
畫出容器裡的 1 個元素,它有一個外殼 div
,裡面是一個白色的小方塊 span
:
.container {
position: relative;
}
.container div {
position: absolute;
width: inherit;
height: inherit;
display: flex;
align-items: center;
justify-content: center;
}
.container div span {
position: absolute;
width: 40px;
height: 40px;
background-color: white;
}
為容器中的元素定義下標變數,並讓元素的外殼依次旋轉,圍合成一個圓形,其中 outline
是輔助線:
.container div {
outline: 1px dashed black;
transform: rotate(calc((var(--n) - 1) * 36deg));
}
.container div:nth-child(1) { --n: 1; }
.container div:nth-child(2) { --n: 2; }
.container div:nth-child(3) { --n: 3; }
.container div:nth-child(4) { --n: 4; }
.container div:nth-child(5) { --n: 5; }
.container div:nth-child(6) { --n: 6; }
.container div:nth-child(7) { --n: 7; }
.container div:nth-child(8) { --n: 8; }
.container div:nth-child(9) { --n: 9; }
.container div:nth-child(10) { --n: 10; }
至此,子元素繪製完成,接下來開始寫動畫指令碼。
引入 GSAP 庫:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
定義一個變數,代表子元素選擇器:
let elements = '.container div span';
宣告一個時間線物件:
let animation = new TimelineMax();
先設定入場方式為由小(第1幀)變大(第2幀),其中並沒有第 2 幀的程式碼,它是隱含在語義中的:
animation.from(elements, 1, {scale: 0});
讓子元素變成豎長條,向四周散開(第3幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25});
讓豎長條旋轉著變成小方塊(第4幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180});
讓小方塊變成橫長條,圍成一個圓形(第5幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1});
注意,因 scrimba 在錄製過多幀時會崩潰,所以第 6 幀至第 11 幀沒有在視訊中體現。
讓圓形向內收斂,同時線條變細(第6幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1});
讓線條向左擺動(第7幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'});
再讓線條向右擺動(第8幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'});
再把橫線變為豎線,造型與第 3 幀相似,只是線更細,更向內收斂(第9幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1});
再把豎線變為橫線,造型與第 5 幀相似,但線短一些(第10幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1})
.to(elements, 1, {scaleX: 0.5, scaleY: 0.1})
橫線稍向外擴散,變為圓點(第11幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1})
.to(elements, 1, {scaleX: 0.5, scaleY: 0.1})
.to(elements, 1, {y: '-80px', scaleY: 0.5, borderRadius: '50%'});
讓圓點變形為豎線,並向內收縮,這個變化的距離長,所以動畫時間也要長一些(第12幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1})
.to(elements, 1, {scaleX: 0.5, scaleY: 0.1})
.to(elements, 1, {y: '-80px', scaleY: 0.5, borderRadius: '50%'})
.to(elements, 1, {y: '-10px', scaleX: 0.1, scaleY: 0.5, borderRadius: '0%', rotation: 0});
讓豎線從中心向外快速擴散,擴散前稍停片刻,好像線條都被髮射出一樣(第13幀):
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1})
.to(elements, 1, {scaleX: 0.5, scaleY: 0.1})
.to(elements, 1, {y: '-80px', scaleY: 0.5, borderRadius: '50%'})
.to(elements, 1, {y: '-10px', scaleX: 0.1, scaleY: 0.5, borderRadius: '0%', rotation: 0})
.to(elements, 1, {y: '-300px', delay: 0.5});
用時間尺度縮放函式讓動畫播放速度加快一倍:
animation.from(elements, 1, {scale: 0})
.to(elements, 1, {y: '-100px', scaleX: 0.25})
.to(elements, 1, {scaleY: 0.25, rotation: 180})
.to(elements, 1, {scaleX: 1})
.to(elements, 1, {y: '-60px', scaleY: 0.1})
.to(elements, 1, {x: '-30px'})
.to(elements, 1, {x: '30px'})
.to(elements, 1, {x: '0', scaleX: 0.1, scaleY: 1})
.to(elements, 1, {scaleX: 0.5, scaleY: 0.1})
.to(elements, 1, {y: '-80px', scaleY: 0.5, borderRadius: '50%'})
.to(elements, 1, {y: '-10px', scaleX: 0.1, scaleY: 0.5, borderRadius: '0%', rotation: 0})
.to(elements, 1, {y: '-300px', delay: 0.5})
.timeScale(2);
修改宣告時間線的程式碼,使動畫重複播放:
let animation = new TimelineMax({repeat: -1, repeatDelay: 1});
至此,動畫完成。
隱藏容器外的內容,並刪掉輔助線;
.container {
overflow: hidden;
}
.container div {
/* outline: 1px dashed black; */
}
最後,裝飾一下頁面的角落:
body {
overflow: hidden;
}
body::before,
body::after {
content: '';
position: absolute;
width: 60vmin;
height: 60vmin;
border-radius: 50%;
background: radial-gradient(
transparent 25%,
gold 25%, gold 50%,
tomato 50%
);
}
body::before {
left: -30vmin;
bottom: -30vmin;
}
body::after {
right: -30vmin;
top: -30vmin;
}
大功告成!
原文地址:https://segmentfault.com/a/1190000016362691