手淘年貨節舞龍揭幕動畫實戰
手淘用戶這幾天應該看到了年貨節版本,不知道剛打開首頁有沒有被一陣鑼鼓聲、鞭炮聲給嚇倒。為了營造一種過年的氣氛出來。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