Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

手淘年货节舞龙揭幕动画实战 #36

Open
airen opened this issue Jan 15, 2016 · 1 comment
Open

手淘年货节舞龙揭幕动画实战 #36

airen opened this issue Jan 15, 2016 · 1 comment

Comments

@airen
Copy link
Contributor

airen commented Jan 15, 2016

手淘用户这几天应该看到了年货节版本,不知道刚打开首页有没有被一阵锣鼓声、鞭炮声给吓倒。为了营造一种过年的气氛出来。PD们给年货节上了一个舞龙的揭幕动画,而这个任务就落在了小生的头上,为了将.gif动效在称动端上实现,着实费劲。那么今天就来介绍这个动画效果是如何实现的?

动画效果

Web动画在PC上已不是难事,而且客户端自己带的动画特效也是非常的流畅,那么要将下面这种.gif动画效果在移动端上实现,我还是第二次经历(前一次是圣诞节的揭幕动画)。

揭幕动画

一开始看到这个效果,有点心虚也有点醉了。其实最开始打算直接上.gif动效图,但使用.gif动效图存在两个问题:

  • 文件过大(帧数越多,文件越大),可有可能造成应用卡死
  • 动效与音乐的匹配

那要怎么做呢?带着尝试的心情,开始了这个动效之旅。

动效分析

整个动画分为两个场景。那么先简单剖析这两个场景:

动画首屏

揭幕动画一进来是一个静态的蒙层:

动画首屏

在这个屏有以下几个动作:

  • 默认静音按钮不选择(这个是可配置时间段),用户点击之后可以处于选中静音状态
  • 点击整个云彩开始转入动画第二场,在这个过程中第一场渐渐隐去,到达第二场
  • 点击关闭按钮,不进入动画第二场,并且整个动画蒙层关闭

动画第二场

动画第二场

动画进入到第二场时整个动画会有以下几个动作:

  • 龙会有十个舞动动作,而且它会不断重复
  • 鞭炮扭动并且逐渐消失
  • 云彩飘扬
  • 如果静音按钮没选中,在第二场中会有音乐播放,反之不会有音乐播放

动画实现原理

整个动画使用CSS Animation中的animation属性完成。在这里主要使用了animation中的steps()animation-timing-function。其实就是一个多步动画,而多步动画中最主要使用到的是雪碧图,因为雪碧图和animation中的steps()配合能让我们轻松实现下面这样的动画效果:

<iframe id="JdMYdz" src="http://codepen.io/airen/embed/JdMYdz?height=500&theme-id=0&slug-hash=JdMYdz&default-tab=result&user=airen" scrolling="no" frameborder="0" height="500" allowtransparency="true" allowfullscreen="true" class="cp_embed_iframe undefined" style="width: 100%; overflow: hidden;"></iframe>

我样可以看到整个动画人特一直在运动,而且动作与动作之间的变动是非常的协调。

动画制作

了解了整个动画场景以及其实现原理,接下来我们看看具体制作过程又是怎么样的,并且在制作过程中碰到什么样的坑。

动画DEMO

别的先不说,先把整个动画的效果向大家展示一下,用你的手机猛扫下面的二维码:

动画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="//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="//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>

样式

整个舞台是充满整屏的,首先将htmlbody和舞台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时还有一个细节,就是每个区域(帧)大小一致,不然在播放时候,龙会乱帧。

第二个效果就是云彩飘动,其实这个效果非常简单,就是通过transformtranslate3d()更换他们的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加速器。而且在一些性能较差的设备会有显得更明显。希望我们在以后的技术沉淀中能把这方面做得更好。

@andge
Copy link

andge commented Jan 25, 2016

sprite 纵向排列 跟横向排列,为什么效果不一样?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants