最近,群里面的同學(xué)發(fā)了這么一個非常有意思是動畫效果:
原效果地址 -- CodePen Demo -- Letter Hop
當(dāng)然,原效果,主要使用了 GSAP 動畫庫以及一個 3D 文字 JavaScript 庫:
import { Those3DTexts } from "https://cdn.skypack.dev/that-3d-text-library";
import { gsap } from "https://cdn.skypack.dev/gsap";
import { MotionPathPlugin } from "https://cdn.skypack.dev/gsap/MotionPathPlugin";
gsap.registerPlugin(MotionPathPlugin);
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
// ....
// 剩余代碼
但是,這個效果,其實本身并不復(fù)雜。
本文,我們將不借助任何動畫庫,嘗試用最簡單的 CSS 和 JavaScript 代碼還原一個類似的 Emoji 3D 表情動畫。
我們的目標(biāo)是實現(xiàn)這么一個效果:
實現(xiàn) 3D Emoji 表情
首先,一個比較大的難點(diǎn)就是,我們?nèi)绾问褂?CSS 實現(xiàn)一個 3D 的 Emoji 表情?像是這樣:
這里,其實使用的是個障眼法。核心就在于,使用多層 Emoji 表情的疊加,當(dāng)疊加的間距合適的情況下,當(dāng)觀看角度處于一定的合理范圍內(nèi)時,視覺上就能得到一種 3D 的效果。
什么意思呢?之前在這篇文章中 -- 哇哦,巧用視覺障眼法,還原 3D 文字特效,有介紹過這個技巧。我們快速回顧一下:
合理的利用距離、角度及光影構(gòu)建出不一樣的 3D 效果??纯聪旅孢@個例子,只是簡單是設(shè)置了三層字符,讓它們在 Z 軸上相距一定的距離。
簡單的偽代碼如下:
<div>
<span class='C'>C</span>
<span class='S'>S</span>
<span class='S'>S</span>
<span></span>
<span class='3'>3</span>
<span class='D'>D</span>
</div>
$bright : #AFA695;
$gold : #867862;
$dark : #746853;
$duration : 10s;
div {
perspective: 2000px;
transform-style: preserve-3d;
animation: fade $duration infinite;
}
span {
transform-style: preserve-3d;
transform: rotateY(25deg);
animation: rotate $duration infinite ease-in;
&:after, &:before {
content: attr(class);
color: $gold;
z-index: -1;
animation: shadow $duration infinite;
}
&:after{
transform: translateZ(-16px);
}
&:before {
transform: translateZ(-8px);
}
}
@keyframes fade {
// 透明度變化
}
@keyframes rotate {
// 字體旋轉(zhuǎn)
}
@keyframes shadow {
// 字體顏色變化
}
簡單捋一下,上述代碼的核心就是:
- 父元素、子元素設(shè)置
transform-style: preserve-3d
- 用
span
元素的兩個偽元素復(fù)制兩個相同的字,利用translateZ()
讓它們在 Z 軸間隔一定距離 - 添加簡單的旋轉(zhuǎn)、透明度、字體顏色變化
可以得到這樣一種類似電影開片的標(biāo)題 3D 動畫,其實只有 3 層元素,但是由于角度恰當(dāng),視覺上的銜接比較完美,看上去就非常的 3D。
為什么上面說需要合理的利用距離、角度及光影呢?
還是同一個動畫效果,如果動畫的初始旋轉(zhuǎn)角度設(shè)置的稍微大一點(diǎn),整個效果就會穿幫:
可以看到,在前幾幀,能看出來簡單的分層結(jié)構(gòu)。又或者,簡單調(diào)整一下 perspective
,設(shè)置父容器的 perspective
由 2000px
改為 500px
,穿幫效果更為明顯:
也就是說,在恰當(dāng)?shù)木嚯x,合適的角度,我們僅僅通過很少的元素,就能在視覺上形成比較不錯的 3D 效果。
上述的完整代碼,你可以猛擊這里:CSS 靈感 -- 3D 文字出場動畫
我們把上述的效果,套用到一個 Emoji 表情上:
<div class="g-emoji">
<div class="g-foo"></div>
<div class="g-bar"></div>
<div class="g-baz"></div>
</div>
.g-emoji {
position: relative;
width: 200px;
height: 200px;
perspective: 2000px;
transform-style: preserve-3d;
font-size: 200px;
animation: rotate 2s alternate infinite ease-in-out;
&::before,
&::after {
content: "\1F600"
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
}
&::after {
transform: translate(-50%, -50%) translateZ(-4px);
}
.g-foo,
.g-bar,
.g-baz{
position: absolute;
inset: 0;
transform-style: preserve-3d;
}
.g-foo::before,
.g-foo::after,
.g-bar::before,
.g-bar::after,
.g-baz::before,
.g-baz::after{
content: "\1F600";
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 200px;
}
.g-foo::before {
transform: translate(-50%, -50%) translateZ(-8px);
opacity: .95;
}
.g-foo::after {
transform: translate(-50%, -50%) translateZ(-12px);
opacity: .9;
}
.g-bar::before {
transform: translate(-50%, -50%) translateZ(-16px);
opacity: .85;
}
.g-bar::after {
transform: translate(-50%, -50%) translateZ(-20px);
opacity: .8;
}
.g-baz::before {
transform: translate(-50%, -50%) translateZ(-24px);
opacity: .75;
}
.g-baz::after {
transform: translate(-50%, -50%) translateZ(-28px);
opacity: .7;
}
}
@keyframes rotate {
0% {
transform: rotateY(-45deg);
}
100% {
transform: rotateY(45deg);
}
}
這里做了什么事情呢:
- 利用元素的偽元素,生成了多個同樣的 Emoji 表情,也就是這一句
content: "\1F600"
,其中\1f600
表示是的笑臉的 Emoji 表情 - 多個相同的 Emoji 表情,疊加在一起,但是設(shè)置了不同的
translateZ
以及不同的透明度
這里需要提一句,Emoji 表情是有特定的編碼范圍的,Emoji 表情的編碼范圍通常是指 Unicode 字符集中專門用于表示 Emoji 圖形的范圍,一個常見的范圍是從 U+1F600 到 U+1F64F。
這樣,我們讓整個容器在一定角度下繞 Y 軸旋轉(zhuǎn)起來,就可以得到 3D 效果:
這里設(shè)定了旋轉(zhuǎn)角度為 -45deg ~ 45deg
。
如果我們把角度調(diào)大,就能清晰的看到效果效果穿幫(下面效果的旋轉(zhuǎn)角度為 -85deg ~ 85deg
):
完整的代碼,你可以戳這里了:CodePen Demo - 3D Emoji Demo
彈跳動畫
好,有了 3D Emoji,接下來,就是實現(xiàn)一個自由落體的彈跳動畫。
這個相對而言比較簡單,當(dāng)然,為了效果逼著,在下落的過程中需要讓元素受到擠壓變形。
關(guān)于一個動畫原則及技巧,建議同學(xué)們可以看看我的這篇文章 -- Web 動畫原則及技巧淺析
核心借助緩動函數(shù),以及 transform,我們在上述 DEMO 的基礎(chǔ)上,實現(xiàn)彈跳動畫效果:
<div class="g-emoji">
<div class="g-foo"></div>
<div class="g-bar"></div>
<div class="g-baz"></div>
</div>
body, html {
width: 100%;
height: 100%;
display: flex;
background: conic-gradient(#fff, #fff 90deg, #ddd 90deg, #ddd 180deg, #fff 180deg, #fff 270deg, #ddd 270deg);
background-size: 50px 50px;
}
.g-emoji {
position: relative;
width: 200px;
height: 200px;
margin: auto;
perspective: 2000px;
transform-style: preserve-3d;
font-size: 200px;
animation:
rotate 2s alternate infinite ease-in-out,
fall .6s alternate infinite cubic-bezier(.22,.16,.04,.99) forwards;
//...
}
@keyframes fall {
0% {
scale: 1.25 0.75;
translate: 0 150px;
}
25% {
scale: 1 1;
}
100% {
scale: 1 1;
translate: 0 0;
}
}
與上面代碼不一樣的是,這里新增了 fall
動畫效果,此效果完成了兩件事:
- 利用
translate
實現(xiàn)了下落動畫 - 利用
scale
實現(xiàn)了形變變化
當(dāng)然,由于是自由落地,選取了一個與自由落體速率相近的 cubic-bezier(.22,.16,.04,.99)
緩動函數(shù),并且,利用了 alternate infinite
讓整個動畫效果,反向無限運(yùn)行。
這樣,我們就能得到這么一個效果:
嘿,是不是有那么點(diǎn)效果了。
解決彈起瞬間切換 Emoji 表情
OK,接下來,我們要解決另外一個難點(diǎn)。
如何在 Emoji 表情彈起的瞬間,替換一個新的 Emoji 表情呢?
此處的麻煩之處在于,上面列出的兩個動畫效果,都是 infinite
無限動畫。熟悉 CSS 動畫的同學(xué)應(yīng)該都知道,在 JavaScript 中,我們可以利用 animationstart
和 animationend
兩個事件,監(jiān)聽 CSS 動畫的開始與結(jié)束。
然而,上面也說了,由于本例中的 CSS 動畫都是無限動畫,我們無法通過這兩個事件去獲取譬如動畫彈起和下落的一些關(guān)鍵事件節(jié)點(diǎn)。
因此,這里我使用了 requestAnimationFrame
去完成這個事情。步驟大致如下:
- 改造一下代碼,DEMO 中 CSS 代碼中使用的 Emoji 表情,通過 JavaScript 寫入元素的
style
標(biāo)簽內(nèi),通過 CSS 變量獲取 - 通過
requestAnimationFrame
監(jiān)聽頁面渲染的每一幀,計算每一幀元素當(dāng)前的位置與上一幀的位置的一些關(guān)系 - 如果發(fā)現(xiàn)上一幀中,元素在下降(計算相對位置變化得到),而最新的一幀中,元素開始上升,則找到了元素從下落到上升轉(zhuǎn)換的這一幀
- 而(3)這一幀的
requestAnimationFrame()
,可以理解為是一個 HOOK,我們在這一幀中,實現(xiàn)?Emoji 表情的隨機(jī)生成與寫入元素的 Style 屬性中
上面一段步驟代碼,需要好好理解,代碼大致如下:
<div class="g-emoji">
<div class="g-foo"></div>
<div class="g-bar"></div>
<div class="g-baz"></div>
</div>
.g-emoji {
position: relative;
animation: rotate 2.3s alternate infinite ease-in-out,
fall .6s alternate infinite cubic-bezier(.22,.16,.04,.99) forwards;
&::before,
&::after {
content: var(--emoji, "\1F600");
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
}
.g-foo::before,
.g-foo::after,
.g-bar::before,
.g-bar::after,
.g-baz::before,
.g-baz::after{
content: var(--emoji, "\1F600");
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 200px;
}
// ...
}
最為核心的 JavaScript 代碼:
const emoji = document.querySelectorAll('.g-emoji')[0];
let curTranslate = 0;
let lastTranslate = 0;
let diff = 0;
function aniFun() {
curTranslate = window.getComputedStyle(emoji, null).getPropertyValue("translate").split(' ')[1].slice(0, -2) - 0;
// 翻轉(zhuǎn)
if (diff > 0 && (curTranslate - lastTranslate < 0)) {
emoji.style = `--emoji: "${generateRandomEmoji()}"`;
}
window.requestAnimationFrame(aniFun);
diff = curTranslate - lastTranslate;
lastTranslate = curTranslate;
}
function generateRandomEmoji() {
// 開始的 Emoji 編碼
var emojiStart = 0x1F600;
var emojiStart2 = 0x1F900;
// 結(jié)束的 Emoji 編碼
var emojiEnd = 0x1F64F;
var emojiEnd2 = 0x1F9FF;
var randomCode = Math.random() > 0.5
? Math.floor(Math.random() * (emojiEnd - emojiStart + 1)) + emojiStart
: Math.floor(Math.random() * (emojiEnd2 - emojiStart2 + 1)) + emojiStart2;
var emoji = String.fromCodePoint(randomCode);
return emoji;
}
window.requestAnimationFrame(aniFun);
這樣,我們就成功的拿到了動畫從下落轉(zhuǎn)向上升的那一幀。
經(jīng)讀者提醒,這里還可以使用 animationiteration 去監(jiān)聽無限動畫的每一次輪回,當(dāng)然,使用 animationiteration 會有兩個節(jié)點(diǎn),需要判斷是從下落轉(zhuǎn)向上升的那一個節(jié)點(diǎn),其余代碼和上述的代碼類似。
并且,在這一幀中,利用 generateRandomEmoji()
,隨機(jī)生成了一個 Emoji 表情的 Unicode 編碼,插入元素中。這樣,我們能得到這樣一個效果:
效果還是很不錯的。主體動畫已經(jīng)實現(xiàn)了,接下來,我們進(jìn)行下一個環(huán)節(jié)。
增加隨機(jī)背景
好,到這里,基本上最為核心的部分我們已經(jīng)實現(xiàn)了。
接下來,就是讓整個動畫更加的豐滿有特色的一些輔助工作。
下一個非常有意思點(diǎn),如何添加隨機(jī)背景動畫?可以看到最上面的 DEMO 圖,在 Emoji 表情變化的瞬間,背景圖也在變化。
這個也好做,上面我們既然已經(jīng)拿到了從下落轉(zhuǎn)向上升的那一幀。那么我們就可以在這一幀中,做更多事情。
隨機(jī)背景的做法就是:
- 事先基于
<body>
,使用 CSS 實現(xiàn)多個不同的背景效果,每個效果,都賦予一個單獨(dú)的 className - 在表情切換的瞬間,也隨機(jī)切換一個背景效果,其本質(zhì)就是給
body
再添加一個事先定義好的范圍內(nèi)的隨機(jī)的 className,表現(xiàn)為不同的背景效果
body.a {
background-image: conic-gradient(#fff, #fff 90deg, #ddd 90deg, #ddd 180deg, #fff 180deg, #fff 270deg, #ddd 270deg);
background-size: 50px 50px;
}
body.b {
background-image:
linear-gradient(0deg, transparent 9%,
rgba(255, 255, 255, .2) 10%, rgba(255, 255, 255, .2) 12%, transparent 13%, transparent 29%,
rgba(255, 255, 255, .1) 30%, rgba(255, 255, 255, .1) 31%, transparent 32%, transparent 49%,
rgba(255, 255, 255, .1) 50%, rgba(255, 255, 255, .1) 51%, transparent 52%, transparent 69%,
rgba(255, 255, 255, .1) 70%, rgba(255, 255, 255, .1) 71%, transparent 72%, transparent 89%,
rgba(255, 255, 255, .1) 90%, rgba(255, 255, 255, .1) 91%, transparent 92%, transparent),
linear-gradient(90deg, transparent 9%,
rgba(255, 255, 255, .2) 10%, rgba(255, 255, 255, .2) 12%, transparent 13%, transparent 29%,
rgba(255, 255, 255, .1) 30%, rgba(255, 255, 255, .1) 31%, transparent 32%, transparent 49%,
rgba(255, 255, 255, .1) 50%, rgba(255, 255, 255, .1) 51%, transparent 52%, transparent 69%,
rgba(255, 255, 255, .1) 70%, rgba(255, 255, 255, .1) 71%, transparent 72%, transparent 89%,
rgba(255, 255, 255, .1) 90%, rgba(255, 255, 255, .1) 91%, transparent 92%, transparent);
background-size:50px 50px;
}
body.c {
background-image: linear-gradient(rgba(0, 255, 0, .7) .1em, transparent .1em), linear-gradient(90deg, rgba(0, 255, 0, .7) .1em, transparent .1em);
background-size: 3em 3em;
}
body.d {
background: repeating-linear-gradient(45deg, #444 0 20px, #c0466f 0 40px);
}
body.e {
background: repeating-radial-gradient(circle at 50% 50%, #fff, #9C27B0 20px, #FF5722 21px, #9C27B0 40px, #000000 41px, #256b8f 60px, #fff 61px);
}
body.f {
background: conic-gradient(#333 0 45deg, #fff 0 360deg);
background-position: -50% -50%;
background-size: 30px 30px;
}
body.g {
&::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 3s ease infinite;
}
}
body.h {
background: linear-gradient(30deg, #000 0, #000 49.9%, #fff 50%);
}
body.i {
background: #000;
&::before,
&::after {
content: '';
position: absolute;
inset: 0 50% 0 0;
background: linear-gradient(
45deg,
#00f376 10%,
transparent 10%,
transparent 50%,
#00f376 50%,
#00f376 60%,
transparent 60%,
transparent 100%
);
background-size: 40px 40px;
animation: move 0.3s linear infinite;
}
&::after {
inset: 0 0 0 50%;
transform: rotateY(180deg);
}
}
body.j {
&::before {
content: "";
position: absolute;
inset: 0;
background: conic-gradient(#fff 0, transparent 30%, #fff);
}
}
body.k {
&::before {
content: "";
position: absolute;
inset: -100vmax;
background: conic-gradient(#fff 0, transparent 45%, #fff);
animation: bgrotate 2s infinite linear;
}
}
const body = document.querySelectorAll('body')[0];
const container = document.querySelectorAll('.g-container')[0];
const emoji = document.querySelectorAll('.g-emoji')[0];
const bgArr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'];
let curTranslate = 0;
let lastTranslate = 0;
let diff = 0;
function aniFun() {
curTranslate = window.getComputedStyle(emoji, null).getPropertyValue("translate").split(' ')[1].slice(0, -2) - 0;
// 翻轉(zhuǎn)
if (diff > 0 && (curTranslate - lastTranslate < 0)) {
emoji.style = `--emoji: "${generateRandomEmoji()}"`;
body.style = `--bg: ${generateRandomColor()}`;
body.setAttribute('class', bgArr[Math.floor(Math.random() * bgArr.length)]);
}
window.requestAnimationFrame(aniFun);
diff = curTranslate - lastTranslate;
lastTranslate = curTranslate;
}
function generateRandomColor() {
var red = Math.floor(Math.random() * 256);
var green = Math.floor(Math.random() * 256);
var blue = Math.floor(Math.random() * 256);
var color = "rgb(" + red + ", " + green + ", " + blue + ")";
return color;
}
function generateRandomEmoji() {
// 開始的 Emoji 編碼
var emojiStart = 0x1F600;
var emojiStart2 = 0x1F900;
// 結(jié)束的 Emoji 編碼
var emojiEnd = 0x1F64F;
var emojiEnd2 = 0x1F9FF;
var randomCode = Math.random() > 0.5
? Math.floor(Math.random() * (emojiEnd - emojiStart + 1)) + emojiStart
: Math.floor(Math.random() * (emojiEnd2 - emojiStart2 + 1)) + emojiStart2;
var emoji = String.fromCodePoint(randomCode);
return emoji;
}
window.requestAnimationFrame(aniFun);
上面,我們定義了?body.a ~body.k 的多個隨機(jī)背景效果。
在表情切換的瞬間,也隨機(jī)切換一個背景效果,其本質(zhì)就是給 body
再添加一個事先定義好的范圍內(nèi)的隨機(jī)的 className。
效果如下:
這樣,整個動畫就基本完成了?;谏鲜龅暮诵牟襟E,可以再做一些細(xì)節(jié)的增強(qiáng):
- 伴隨?Emoji 表情落體運(yùn)動,下方可以增添陰影的變化
- Emoji 表情旋轉(zhuǎn)方向的變化優(yōu)化
- 豐富不同的背景效果
- 等等等等
這樣,最終整個效果就完成啦,效果如下:
看似很復(fù)雜的一個動畫效果,經(jīng)過拆解后,一步一步實現(xiàn),其實也不難。
完整的動畫效果,你可以戳這里:CodePen Demo -- Random 3D Emoji
總結(jié)一下
好了,本文到此結(jié)束,希望對你有幫助 ??
更多精彩 CSS 技術(shù)文章匯總在我的 Github -- iCSS ,持續(xù)更新,歡迎點(diǎn)個 star 訂閱收藏。
如果還有什么疑問或者建議,可以多多交流,原創(chuàng)文章,文筆有限,才疏學(xué)淺,文中若有不正之處,萬望告知。文章來源:http://www.zghlxwxcb.cn/news/detail-620314.html
最后,我的小冊 《CSS 技術(shù)揭秘與實戰(zhàn)通關(guān)》上線了,想了解更多有趣、進(jìn)階、系統(tǒng)化的 CSS 內(nèi)容,可以猛擊 - LINK。文章來源地址http://www.zghlxwxcb.cn/news/detail-620314.html
到了這里,關(guān)于【動畫進(jìn)階】有意思的 Emoji 3D 表情切換效果的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!