电影故障抖动视觉效果实现:对大话手游愚人节专题的探究
程沛权2020/4/20 23:47:00
Star on GitHub前几天愚人节的时候,大话手游官网上了一个专题(专题地址),里面很多图片位置都使用了一个类似电影那种画面抖动的效果(目前只剩下一个 slogan 了,其他都下线了),很好奇是怎么实现的,于是扒了一下页面的源码,了解了一些实现思路,整理一下以后可能会用到。
效果
起因
其实这个需求如果是直接给你做,你能不能马上有思路去做出来呢?相信大部分人都会有,那我为啥还要去看他们怎么实现的呢?
想做这件事的原因,主要是:
1、想知道对方是怎么实现的,那个抖动的特效,是有素材还是直接前端处理出来的,是不是在自己的预料之中。
2、自己虽然能马上想到一些实现方案,但还是想看看是否有更优秀的解决办法,可以偷师学习!
3、看看这个效果的素材是怎么处理的,以后遇到类似的需求,我是不是可以把一些锅甩给设计师?
思路
在看它是怎么实现之前,先凭经验猜测了一下,有多少种可能实现的方式:gif 动图、视频、flash、逐帧动画、图片切换、背景图切换、svg、canvas…(前端牛逼啊!!!!)
当然每种方法的实现成本不一样,对应的体验和性能也不一样,思路有了,那么来验证一下官网是怎么做的。
探路
我开始以为是用的视频,因为按网易游戏以往的尿性来说,营销页面上的动态类主视觉基本都是用视频来实现的…那么要怎么看实现方法呢?当然是看 DOM 啊!
所以,用的是 canvas,那就有趣了!还好不是视频,直接弄个视频引入的话也就没这篇文章什么事了…
canvas 的动画效果,都是一帧一帧的定时走出来的,这说明素材是来自设计师之手,不一定是前端直接处理的。
接下来就找素材了,这种效果的素材,基本上都是图片,找图片的过程就比较简单了,作为主视觉上这么大的 slogan,结合刚刚查看 DOM 的时候,你发现那个地方的 className 就叫 slogan,那么对应的素材命名,肯定也跟 slogan 有关,单刀直入切到 img,搜索 slogan 关键词,全都出来了。
可以看出他们是把整个动画过程的每一帧,都处理了一张图片素材,我们先把素材弄下来。
一共有 30 帧,30 张一样尺寸的素材,现在素材有了,接下来就可以开始尝试效果复原。
实现
实现方案我上面说了,那就一个方案一个方案来看怎么实现,完整的在线 demo 在文末有地址。
方案根据推荐度从低到高说起吧,实现难度基本上也是从低到高这样…
方案一:使用 gif 动图
先从最容易想到的方案说起吧,动图从制作成本来说是最省事的…只需要一个 img 标签就可以导进来了。
【推荐】★★★☆☆
【优点】简单,直接导个 gif 引入就完事,纯 html。
【弊端】一般来说动图都会比较大,像 demo 里面只有一个 slogan 动图都去到了 866KB,太多的 gif 对页面的渲染速度有影响,用户体验不是最佳。
// html
<section class="section section-01">
<div class="img">
<img src="img/slogan.gif">
</div>
</section>
方案二:切换图片地址
结合我们的素材,已经是处理好一帧一帧这样的过渡状态,那通过定时切换的效果,把他们按指定的时间和顺序切下去,也可以达到想要的效果。
【推荐】★★☆☆☆
【优点】简单,批量导出每一帧的 png 素材出来,定时替换图片的地址就完事(而且每一帧的素材都不会很大)。
【弊端】需要频繁的操作 DOM,性能方面开销太大。
// html
<section class="section section-02">
<div class="img">
<img src="img/0.png">
</div>
</section>
// js
<script type="text/javascript">
const slogan = {
index: 0,
indexMax: 29,
time: 0,
dom: document.querySelector('.section-02 .img img'),
auto(speed){
let change = setInterval( () => {
// 动态调整图片帧显示
this.index < this.indexMax ? this.index++ : this.index = 0;
this.dom.setAttribute('src', `img/${this.index}.png`);
// 每运行10次动画周期后销毁定时器,进行垃圾回收后再重新创建
this.time += speed;
if ( this.time > this.indexMax * speed * 10) {
clearInterval(change);
change = null;
this.time = 0;
this.auto(speed);
}
}, speed);
}
}
slogan.auto(100);
</script>
方案三:使用定时器切换背景图
【推荐】★★☆☆☆
和方法二比较类似,只不过方法二是切换图片的 src,这里是切换 div 的样式,来达到换背景图的效果,优缺点说起来差不多。
// html
<section class="section section-03">
<div class="img bg"></div>
</section>
// js
<script type="text/javascript">
const sloganBg = {
index: 0,
indexMax: 29,
time: 0,
dom: document.querySelector('.section-03 .bg'),
auto(speed){
let change = setInterval( () => {
// 先移除上一帧的样式
this.dom.classList.remove(`bg-${this.index}`);
// 动态调整图片帧显示
this.index < this.indexMax ? this.index++ : this.index = 0;
this.dom.classList.add(`bg-${this.index}`);
// 每运行10次动画周期后销毁定时器,进行垃圾回收后再重新创建
this.time += speed;
if ( this.time > this.indexMax * speed * 10) {
clearInterval(change);
change = null;
this.time = 0;
this.auto(speed);
}
}, speed);
}
}
sloganBg.auto(100);
</script>
方案四:使用 css3 逐帧动画
这个办法我是比较推荐的,实现成本并不高,写起来也很简单,体验又好。
【推荐】★★★★☆
【优点】简单,性能好,生成雪碧图,然后写个动画就完事。
【弊端】部分古董设备不兼容,然后还有个问题,就是像 demo 里的这个素材,做成雪碧图贼他妈大(2.24MB,经过 tinypin 压缩后还是有 640KB),所以素材太大的情况下,最好不要用这个办法来搞。
// css
<style>
.go {
background-image: url('../img/sprites.png');
background-repeat: no-repeat;
animation: go steps(29, end) 3s infinite;
}
@keyframes go {
100% {
background-position: -0 -8729px;
}
</style>
// html
<section class="section section-04">
<div class="img bg go"></div>
</section>
实现思路:
1、把所有的帧素材都合并为一张雪碧图,减少 http 请求,通过 animation 的背景图移动来实现视觉上的切换。
2、这里运用到了 css3 的 animation-timing-function 的 steps,减少动画过程的代码编写
3、结合第 2 点,因为 steps(number, position)的两个参数,第一个参数是设定有多少帧,第二个参数是设置动画的连续方式,所以根据 steps 的特性,我们生成的雪碧图需要无间隔并且连贯(我生成的就是从上到下排序下来的)
这里推荐一个在线工具:快速生成雪碧图
还有关于里面的 steps 的用法,可以参考张老师的文章:CSS3 animation 属性中的 steps 功能符深入介绍
方案五:使用 canvas 逐帧绘制
终于来到一开头提到的 canvas 实现方案了。大话官网专题,我看了一下源代码,虽然代码被混淆,但还是可以看出,应该是通过引入插件来实现的。
我们自己写其实也不难,因为知道了本身的实现套路(有逐帧素材,然后通过逐帧逐帧去绘制渲染出来),那就可以着手编写代码了。
【推荐】★★★★★
【优点】canvas 在性能上有天然的优势,对于高频率的更新渲染,用 canvas 重绘来实现效果更佳。
【弊端】古董机对 canvas 的兼容性不太友好,如果可以抛弃这些古董用户的话,还是推荐这个方案!
// html
<section class="section section-05">
<div class="img"></div>
</section>
// js
<script type="text/javascript">
const sloganCanvas = {
index: 0,
indexMax: 29,
canvasWidth: 801,
canvasHeight: 301,
init(){
// 创建画布
const canvas = document.createElement('canvas');
canvas['width'] = this.canvasWidth;
canvas['height'] = this.canvasHeight;
document.querySelector('.section-05 .img').appendChild(canvas);
// 绘制内容
this.draw(this.index, canvas);
},
draw(index, canvas){
const context = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 绘制前先清空画布
context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 然后再绘制当前帧
context.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight);
// 画完更换下一帧的索引
this.index < this.indexMax ? this.index++ : this.index = 0;
setTimeout( () => {
this.draw(this.index, canvas);
}, 100);
}
img.src = `img/${this.index}.png`;
}
}
sloganCanvas.init();
</script>
实现思路:
1、动态创建 canvas,不要写死 canvas 的宽度和高度,实际需求如果要覆盖移动端,请动态计算尺寸后生成(官网专题是不做移动端了,移动端纯 jpg 图,感觉有点可惜)
2、载入每帧的素材的时候,后续操作都要放在 img 的 onload 事件里执行
3、每次进行绘制之前,记得先清空画布,否则会一直叠加绘制,就没法看了…
4、通过 setTimeout 的延时回调控制无限循环动画的速度
最后
有几个方案我没有写,关于 svg 对这个需求的实现,暂时没有思路,我选择放弃,以后想到了再来补上!
而视频和 flash 这些多媒体的展示方案,这里就略过了,处理成本对我来说还是比较高的,要配合设计的主视觉去输出优质的素材,我不太擅长…(所以这种方案可以考虑把活交给设计师…)
最后放上 demo 地址:电影抖屏效果 demo