手淘年貨節舞龍揭幕動畫實戰

jopen 8年前發布 | 14K 次閱讀 CSS 前端技術

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!