手淘年貨節舞龍揭幕動畫實戰
手淘用戶這幾天應該看到了年貨節版本,不知道剛打開首頁有沒有被一陣鑼鼓聲、鞭炮聲給嚇倒。為了營造一種過年的氣氛出來。PD們給年貨節上了一個舞龍的揭幕動畫,而這個任務就落在了小生的頭上,為了將 .gif 動效在稱動端上實現,著實費勁。那么今天就來介紹這個動畫效果是如何實現的?
動畫效果
Web動畫在PC上已不是難事,而且客戶端自己帶的動畫特效也是非常的流暢,那么要將下面這種 .gif 動畫效果在移動端上實現,我還是第二次經歷(前一次是圣誕節的揭幕動畫)。
一開始看到這個效果,有點心虛也有點醉了。其實最開始打算直接上 .gif 動效圖,但使用 .gif 動效圖存在兩個問題:
- 文件過大(幀數越多,文件越大),可有可能造成應用卡死
- 動效與音樂的匹配
那要怎么做呢?帶著嘗試的心情,開始了這個動效之旅。
動效分析
整個動畫分為兩個場景。那么先簡單剖析這兩個場景:
動畫首屏
揭幕動畫一進來是一個靜態的蒙層:
在這個屏有以下幾個動作:
- 默認靜音按鈕不選擇(這個是可配置時間段),用戶點擊之后可以處于選中靜音狀態
- 點擊整個云彩開始轉入動畫第二場,在這個過程中第一場漸漸隱去,到達第二場
- 點擊關閉按鈕,不進入動畫第二場,并且整個動畫蒙層關閉
動畫第二場
動畫進入到第二場時整個動畫會有以下幾個動作:
- 龍會有十個舞動動作,而且它會不斷重復
- 鞭炮扭動并且逐漸消失
- 云彩飄揚
- 如果靜音按鈕沒選中,在第二場中會有音樂播放,反之不會有音樂播放
動畫實現原理
整個動畫使用CSS Animation中的 animation 屬性完成。在這里主要使用了 animation 中的 steps() 的 animation-timing-function 。其實就是一個多步動畫,而多步動畫中最主要使用到的是雪碧圖,因為雪碧圖和 animation 中的 steps() 配合能讓我們輕松實現下面這樣的動畫效果:
我樣可以看到整個動畫人特一直在運動,而且動作與動作之間的變動是非常的協調。
動畫制作
了解了整個動畫場景以及其實現原理,接下來我們看看具體制作過程又是怎么樣的,并且在制作過程中碰到什么樣的坑。
動畫DEMO
別的先不說,先把整個動畫的效果向大家展示一下,用你的手機猛掃下面的二維碼:
(^_^)可別被鑼鼓聲給嚇壞了。
創建模板
把整個動畫放在一個場景中,就把它稱之為“舞臺”吧,并且把這個舞臺命名為 dragon-poplayer :
<div class="dragon-poplayer"></div>
動畫有兩個場景,把這個場景稱之為“容器”:
<div class="dragon-poplayer" id="dragon-poplayer">
<div class="dragon-section dragon-ready-play" id="dragon-ready-play">
<div class="dragon-play">
<!-- 第一場景 -->
</div>
</div>
<div class="dragon-section dragon-playing" id="dragon-playing">
<!-- 第二場景 -->
</div>
</div> 為了能讓用戶更好的控制整個動畫,畢竟不是所有用戶都喜歡,在舞臺的同級,添加了一個關閉按鈕:
<div id="close"></div>
前面也說過了,第一場景中主要有一個靜音按鈕和觸發到第二場景的動作按鈕(暫且把它稱為播放按鈕吧)。另外就是把音樂 <audio> 也丟在這個容器中。
為了讓靜音按鈕更能個性化,這里采用了模擬 checkbox (具體制作方法,可以參考《 CSS3制作iPhone的Checkbox 》)。
<div class="dragon-poplayer" id="dragon-poplayer">
<div class="dragon-section dragon-ready-play" id="dragon-ready-play">
<div class="dragon-play">
<div class="music">
<input type="checkbox" name="music" id="music-control">
<label for="music-control">聲音</label>
</div>
<div id="music">
<audio src="http://gw.alicdn.com/tfscom/TB1Ydd2LpXXXXaUXFXXsKFbFXXX.mp3" loop="loop" preload="load"></audio>
</div>
</div>
</div>
<div class="dragon-section dragon-playing" id="dragon-playing">
<!-- 第二場景 -->
</div>
<div id="close"></div>
</div> 第二場景先來看舞動的龍,整條龍有五個部分,分別有五個小朋友舉著,為了更好的控制龍更好舞動,將整條龍分成五個部分,分別由一個 div 來控制:
<div class="dragon-wrap">
<div class="dragon-content">
<div class="dragon dragon1"></div>
<div class="dragon dragon2"></div>
<div class="dragon dragon3"></div>
<div class="dragon dragon4"></div>
<div class="dragon dragon5"></div>
</div>
</div> 在龍的周邊還有三朵云彩在飄,同樣將每朵云放置在一個獨立的 <section> 里:
<div class="dragon-wrap">
<div class="dragon-content">
<div class="dragon dragon1"></div>
<div class="dragon dragon2"></div>
<div class="dragon dragon3"></div>
<div class="dragon dragon4"></div>
<div class="dragon dragon5"></div>
<section class="cloud"></section>
<section class="cloud"></section>
<section class="cloud"></section>
</div>
</div> 還有兩串鞭炮,不用多說,用兩個 div 來放置:
<div class="firecrackers firecrackers-left"></div> <div class="firecrackers firecrackers-right"></div>
最終的HTML就長成這樣:
<div class="dragon-poplayer" id="dragon-poplayer">
<div class="dragon-section dragon-ready-play" id="dragon-ready-play">
<div class="dragon-play">
<div class="music">
<input type="checkbox" name="music" id="music-control">
<label for="music-control">聲音</label>
</div>
<div id="music">
<audio src="http://gw.alicdn.com/tfscom/TB1Ydd2LpXXXXaUXFXXsKFbFXXX.mp3" loop="loop" preload="load"></audio>
</div>
</div>
</div>
<div class="dragon-section dragon-playing" id="dragon-playing">
<div class="dragon-wrap">
<div class="dragon-content">
<div class="dragon dragon1"></div>
<div class="dragon dragon2"></div>
<div class="dragon dragon3"></div>
<div class="dragon dragon4"></div>
<div class="dragon dragon5"></div>
<section class="cloud"></section>
<section class="cloud"></section>
<section class="cloud"></section>
</div>
</div>
<div class="firecrackers firecrackers-left"></div>
<div class="firecrackers firecrackers-right"></div>
</div>
<div id="close"></div>
</div> 樣式
整個舞臺是充滿整屏的,首先將 html 、 body 和舞臺 dragon-poplayer 設置為全屏模式:
html,body {
height: 100vh;
min-width: 10rem;
margin-left: auto;
margin-right: auto;
background: transparent;
}
body {
min-height: 100%;
background: url(http://gw.alicdn.com/mt/TB1.sknLXXXXXbEXpXXXXXXXXXX-750-1333.png) no-repeat;
background-size: 10rem 100%;
}
.dragon-poplayer,
.dragon-section {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 10rem;
height: 100%;
overflow: hidden;
} 其實第一場景的樣式很簡單,這里就不做過多闡述,將代碼貼出來供大家參考:
.dragon-play{
width: 10rem;
height: 10.946667rem; //821px
background: url('//gw.alicdn.com/mt/TB13eupLpXXXXaGXXXXXXXXXXXX-750-821.png') no-repeat center;
background-size: 10rem 10.946667rem;
position: absolute;
z-index: 10;
.music {
position: absolute;
width: 1.866667rem; //140
height: 0.533333rem; //40px
top: 3.6rem; //270px
left: 4.266667rem; //320px
z-index: 12;
input[type="checkbox"]{
opacity: 0;
&:checked + label:before {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAoCAMAAABZ/...');
}
}
label {
white-space: nowrap;
display: block;
position: absolute;
top: -0.026667rem; //2px
left: 0;
font-size: 0;
width: 100%;
height: 0.533333rem; //40px
&:before {
content: "";
display: inline-block;
width: 0.626667rem; //47px
height: 0.533333rem; //40px
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8A...') no-repeat;
background-size: 0.626667rem 0.533333rem; //47px 40px
}
}
}
@at-root #music {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
cursor: pointer;
}
} 用戶點擊播放之后,會從第一場景進入到第二場景,在這個過程中會有一個動畫效果,就是第一場景慢慢淡出 fadeOut ,第二場景慢慢淡入 animation :
.dragon-ready-play{
z-index: 100;
&.is-animationed {
animation: fadeOut 1.5s ease-in both;
}
}
.dragon-playing {
opacity: 0;
&.is-animationed{
animation: fadeIn 1s ease both;
}
} 動畫是通過 keyframes 制作:
// 淡出
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
// 淡入
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
} 在這個過程僅通過CSS我們還有點難度的,需要通過JavaScript來觸發,至于怎么觸,后面的JavaScript部分來介紹。
其實難度在第二場景,因為在這個場景中我們涉及到三個部分的動畫。我們來先看最難的一部分吧,就是龍。
前面也說過了,龍就要是分為五段,每段我們是通過CSS Sprites配合 steps() 完成。那么在這個過程需要將龍的每一部分拼合出來,如下圖所示:
至于樣式如下:
.dragon {
position: absolute;
height: 2.453333rem; //184px
top: 0;
}
.dragon1{
width: 2.373333rem; //178px
height: 2.506667rem; //188px
left: 0;
z-index: 5;
background: url('//gw.alicdn.com/mt/TB16t_sIFXXXXaXapXXXXXXXXXX-1780-188.png') no-repeat;
background-size: 23.733333rem 2.506667rem; //1780px 188px
} 動畫的 keyframes :
@keyframes dragon-1 {
to {
background-position: -23.733333rem; //1780px
}
} 觸發動畫:
.dragon-playing {
opacity: 0;
&.is-animationed{
animation: fadeIn 1s ease both;
.dragon{
animation-duration: 1s;
animation-timing-function: steps(10);
animation-iteration-count: infinite;
}
.dragon1{
animation-name: dragon-1;
}
} 其它幾個部分就不做詳細闡述。在做龍的時候碰到兩個坑。
第一個坑就是設計師希望將龍和小人分開來,這樣有利于龍的更換(就是隨時更換龍的設計效果)。聽起來很有吸引力,但在實際制作過程中,才發現龍和小人的配合是非常難以達到一致。最后只好又更換到讓他們合成在一起。
第二個坑就是,CSS Sprites的拼合。剛開始將其按縱向拼合,通過更改 background-position-y 的值。但動畫效果非常生硬,才更換成水平排列。在排列Sprites時還有一個細節,就是每個區域(幀)大小一致,不然在播放時候,龍會亂幀。
第二個效果就是云彩飄動,其實這個效果非常簡單,就是通過 transform 的 translate3d() 更換他們的 X 軸位置:
@keyframes colud {
0%,40%,100% {
transform: translate(0,0); //0
}
20%, 50%, 80% {
transform: translate(0.266667rem,0); //20px
}
60% {
transform: translate(-0.266667rem,0); //20px
}
} 第三個動效果是鞭炮的播放。最開始使用的是鞭炮和禮花合在一起,同樣通過Sprites來實現,再配合 translate3d 將整個鞭炮往 Y 拉。雖然效果出來了,但PD同學說太假了,這不是在放鞭炮,整個鞭炮是在往上拉。想想也是,對于有追求的同學來說,還是很有必要來修改的。而在修改這個效果其實比舞龍動效還難。
最后的思路是把鞭炮和禮花拆分出來,為了動效更生動,鞭炮同樣使用Sprites:
這兩個要配合在一起,而且每個部分都采用了多個動畫。
在這個過程最難的,也可以說是坑吧有兩個:
- 鞭炮慢慢變短,逐漸消失
- 鞭炮和禮花位置的配合
鞭炮的逐漸消失,在這個過程嘗試了很多種方案,都未見效。使用 transform 的話就會回到當初的效果,如果修改 hieght 的話,鞭炮會一閃而過。最后在無意中嘗試修改鞭炮的 max-height 。簡單點說就是慢慢變為 0 :
@keyframes bianpao2 {
from {
max-height: 4.426667rem; //332px
}
to {
max-height: 0;
}
} 當然這種方案的效果也并不完全完美,怎么看度部都有一種被截取的效果。
另外就是鞭炮和禮花的配合。初始采用移動,但時間無法達到配合。情急之下,就只對禮花做定位處理:
.firecrackers {
width: 2.213333rem; //166px;
height: 4.426667rem; //332px;
background: url('//gw.alicdn.com/mt/TB1zoB3LpXXXXbCXXXXXXXXXXXX-332-332.png') no-repeat;
background-size: 4.426667rem 4.426667rem; //332px 332px
position: absolute;
top: -0.213333rem; //16px
&.firecrackers-left{
//left: 0.133333rem; // 10px
left: 0;
}
&.firecrackers-right {
//right: 0.133333rem; // 10px
right: -0.533333rem; //40px
}
&:after {
content: "";
width: 1.626667rem; //122px;
height: 1.2rem; //90px;
position: absolute;
bottom: -0.706667rem; //-53px;
left: 0.066667rem; //5px;
background: url('data:image/png;base64,B...') no-repeat;
background-size: 2.986667rem 1.2rem; //224px 90px;
}
} 居然看上去也還是能勉強接受。
最后還有一個效果需要特別提出來,就是龍的位置。因為手淘首頁在龍的下面就已嵌入了一個進入年貨節主會場的按鈕(這個是Native同學配置的)。而我們要處理的是動畫的層必須先遮蓋住。
.dragon-wrap {
width: 10rem;
height: 2.986667rem; //224px
background:url('//gw.alicdn.com/mt/TB17q71LXXXXXbWXpXXXXXXXXXX-750-224.png') no-repeat center;
background-size: 10rem 2.986667rem;
position: absolute;
top: 5.2rem;//390px
} 但坑來了,手淘在不同的終端設備中,頂部的距離都不一樣。這下就煩了,在實在沒辦法的情況下,只做了手淘的iOS設備做了處理:
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
.dragon-wrap {
top: 5.2rem;//390px
}
}
// iphone5 & 5s
@media only screen
and (min-device-width : 320px)
and (max-device-width : 568px) {
.dragon-wrap {
top: 5.2rem;//390px
}
}
// iphone6
@media only screen
and (min-device-width : 375px)
and (max-device-width : 667px) {
.dragon-wrap {
top: 4.8rem; //360px
}
}
// iphone6 +
@media only screen
and (min-device-width : 414px)
and (max-device-width : 736px) {
.dragon-wrap {
top: 4.666667rem; //350px
}
} 在手貓中還是會有一點遮住手焦。在安卓設備下就更會錯位嚴重了。到目前為止沒找到更好的解決方案。
觸發動畫
樣式效果已處理完成。但整個動畫我們還是需要JavaScript來觸發。而且還有一些其他需要處理的。比如說時間的設置、音樂的控制等。
JavaScript做了以下幾件事情:
音樂的播放
// 控制音樂的播放
function musicPlayer (){
var dragonStage = document.getElementById('dragon-poplayer'),
switcher = document.getElementById('music'),
media = switcher.getElementsByTagName('audio')[0],
chooseMusic = document.getElementById('music-control'),
wantedDragonDance = document.getElementById('dragon-ready-play'),
dragonDanceStar = document.getElementById('dragon-playing'),
firecrackers = document.querySelector('.firecrackers');
// 獲取舞龍音樂選中開始時間
var musicStartTime = pageData['startTime'];
// 獲取舞龍音樂選中結束時間
var musicStopTime = pageData['endTime'];
// 將設置的時間字符串(按冒號)拆分為兩部分
var timeStart = musicStartTime.split(':');
var timeEnd = musicStopTime.split(':');
// 設置限制的開始時間
var limitStart = new Date();
limitStart.setHours(timeStart[0]);
limitStart.setMinutes(timeStart[1]);
// 設置限制的結束時間
var limitEnd = new Date();
limitEnd.setHours(timeEnd[0]);
limitEnd.setMinutes(timeEnd[1]);
// 獲取系統當前時間
var nowTime = new Date();
// 如果系統時間在 限制時間之間,checkbox不選中,否則自動選中
chooseMusic.checked = nowTime < limitStart || nowTime > limitEnd;
switcher.addEventListener ('click', function (){
var currentStatus = media.paused ? 'pause' : 'play';
var wantedStatus = currentStatus === 'pause' && !chooseMusic.checked ? 'play' : 'pause';
media[wantedStatus]();
// 如果wantedDragonDance 沒有is-animationed類名,就添加,反之什么也不做
if(!wantedDragonDance.classList.contains('is-animationed')){
wantedDragonDance.classList.add('is-animationed');
}
}, false);
// 監聽wantedDragonDance的webkitAnimationEnd
// 如果wantedDragonDance的動畫完成,給dragonDanceStar 添加類名is-animationed
wantedDragonDance.addEventListener('webkitAnimationEnd', function(){
dragonDanceStar.classList.add('is-animationed');
});
//監聽鞭炮的動作,如果動畫播放完,音樂停止,并且刪除整個舞臺和關閉Poplayer
firecrackers.addEventListener('webkitAnimationEnd', function(e){
media.pause();
document.body.removeChild(dragonStage);
window.WindVane.call('WVPopLayer', 'close', {});
}, false);
} 禁止用戶滑動屏幕
// 禁止滑動
function cancleDocumentScroll () {
document.addEventListener('touchmove', function (e) {
e.preventDefault();
return false;
}, false);
} 關閉音樂和Poplayer
// 關閉WVPopLayer 和 音樂
function closeAll () {
var colseBtn = document.getElementById('close'),
switcher = document.getElementById('music'),
media = switcher.getElementsByTagName('audio')[0];
colseBtn.addEventListener('click', function () {
window.WindVane.call('WVPopLayer', 'close', {});
media.pause();
var source = appname === 'TM' ? 2 :1 ;
goldlog('/nhj.1.4','','from='+ source,'H1703624');
}, false);
} 執行函數
function init (){
window.WindVane.call('WVPopLayer', 'display', {});
window.WindVane.call('WVPopLayer', 'increaseReadTimes', {}, function(s){
// do something when success;
}, function(e) {
// do something when failed;
});
musicPlayer ();
cancleDocumentScroll ();
closeAll ();
}
// 開始執行函數
document.addEventListener('DOMContentLoaded', init, false); POPLAYER
雖然我們整個動畫是使用CSS和JavaScript完成的,也可以說是一個Web Animation。那么要放到APP中,還是需要特殊處理的。在這里我們使用了一種技術: POPLAYER 。
有關于POPLAYER相關的介紹可以閱讀《 POPLAYER起來HIGH~~ 》一文。如果你無法理解,就簡單的把他當作是一個WebView或者是一個 iframe 吧。至于怎么做POPLAYER,偶也不懂。
總結
閱讀到這里是不是有點累了,內容偏長。整篇文章主要介紹了揭幕動畫的制作過程。簡單點說就是如何時通過Web Animation將一個 gif 動畫轉換成Web動畫。在整個制作過程主要采用了CSS的 animation 屬性,并且配合CSS Sprites。當然這種效果也存在一定的缺陷,性能在APP中還是有所局限性,特別是在POPLAYER中,我們暫時無法開啟設備的3D加速器。而且在一些性能較差的設備會有顯得更明顯。希望我們在以后的技術沉淀中能把這方面做得更好。
大漠
常用昵稱“大漠”,W3CPlus創始人,目前就職于手淘。中國Drupal社區核心成員之一。對HTML5、CSS3和Sass等前端腳本語言有非常深入的認識和豐富的實踐經驗,尤其專注對CSS3的研究,是國內最早研究和使用CSS3技術的一批人。CSS3、Sass和Drupal中國布道者。2014年出版《 圖解CSS3:核心技術與案例實戰 》。
來自: http://www.w3cplus.com/animation/dragon-dance-opening-animation.html