因为本人最近在作一些 growth hacking 的工做,业务上之后可能也会涉及去作一些可以在朋友圈火爆分享的 H5 页面,忽然想到去年看到一个网易娱乐年度新闻盘点的 H5 页面很是的新颖,采用画中画的形式依次串联十多个手绘娱乐图片,加上洗脑的“好运来”音乐,让人有很大的分享的欲望。css
手机扫码体验网易年度娱乐盘点:前端
接下来咱们来一步步实现这样的一个 H5 页面,首先,咱们须要搞懂这个页面用到了那些前端的知识点。css3
首屏有不少动画,其中大多数是用雪碧图+animation 的 step 动画函数实现的,包括底部的鼓,右上角的镲,中间人物飘动的头发。脚下来回滚动的浪花就是普通的 animation 动画。除了首屏的这些动画,后面切换到某些场景的时候也会有动画,这些动画也是用的雪碧图动画。git
这个就是使用 audio 元素便可,设置 audio 为循环播放,当点击右上角镲的动图的时候,调用 audio.pause()便可。github
在首屏,当长按鼓的时候,页面的 animation 动画会中止,静态画面一点点的缩小,直至出现第一个完整的画中画。此时过渡动画中止,页面 animation 动画(白百何一指禅)开始出现。canvas
咱们先来分析这一小段,咱们代码上要作哪些工做。浏览器
首先,咱们须要两个图层,一个 canvas 图层用来展现场景过渡动画,z-index 较低;一个展现场景动画的图层,咱们叫作 gif 图层,z-index 较高;微信
在 canvas 图层里,咱们使用 drawImage()这个方法来绘制每一帧的过渡图片,咱们先来看看这个方法的使用方式:app
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);dom
参数值
参数 | 描述 |
---|---|
img | 规定要使用的图像、画布或视频。 |
sx | 可选。开始剪切的 x 坐标位置。 |
sy | 可选。开始剪切的 y 坐标位置。 |
swidth | 可选。被剪切图像的宽度。 |
sHeight | 可选。被剪切图像的高度。 |
x | 在画布上放置图像的 x 坐标位置。 |
y | 在画布上放置图像的 y 坐标位置。 |
width | 可选。要使用的图像的宽度。(伸展或缩小图像) |
height | 可选。要使用的图像的高度。(伸展或缩小图像)。 |
过渡动画的每一帧,咱们都要在 canvas 上面使用 drawImage 绘制两张图片,一张是大图,一张是画中画里的小图,以第一个过渡动画为例,大图是 P2,小图是 P1,
如图:(原谅我不知道使用什么工具画图,只好动手了)
咱们假设大图 P2 是长方形 ABCD,小图 P1 是长方形 IJKL,动画过程当中某一时刻的手机屏幕是长方形 EFGH,咱们有个前提条件就是这三个长方形都是宽高比为 750:1206 的长方形,并且,全部的图片宽高像素大小是相等的(网易的场景图片大小统一为:1875*3015)这也意味着 iPhone X 等全面屏手机的适配会有问题,在 iPhone678 手机上表现良好。(看看今年网易会不会解决这个问题,毕竟全面屏手机愈来愈多)。
那么,在这样的一个时刻,咱们须要在 canvas 上面画两张图片,
drawImage(P2,ME,NE,EF,EH,0,0,750,1206)
drwaImage(P1,0,0,AB,AD,OI,PI,IJ,IL)
复制代码
那咱们知道了某一时刻的状况,可是如何将画面动起来,有一个收缩画面的效果呢?
如今开始写咱们的 render 函数:
const render = () => {
this.radio = this.radio * this.scale;
this.timer = requestAnimationFrame(render);
this.draw();// 绘制两个图片
};
draw() {
if (this.index + 1 != this.imgList.length) {
if (
this.radio <
this.imgList[this.index + 1].areaW / this.imgList[this.index + 1].imgW
) {
if (this.willPause) {
this.radio =
this.imgList[this.index + 1].areaW / this.imgList[this.index + 1].imgW;
cancelAnimationFrame(this.timer);
}
this.index++;
this.radio = 1;
if (!this.imgList[this.index + 1]) {
this.showEnd();
}
}
this.imgNext = this.imgList[this.index + 1];
this.imgCur = this.imgList[this.index];
this.containerImage = this.domList[this.index + 1];
this.innerImage = this.domList[this.index];
this.drawImgOversize(
this.containerImage,
this.imgNext.imgW,
this.imgNext.imgH,
this.imgNext.areaW,
this.imgNext.areaH,
this.imgNext.areaL,
this.imgNext.areaT,
this.radio,
),
this.drawImgMinisize(
this.innerImage,
this.imgCur.imgW,
this.imgCur.imgH,
this.imgNext.imgW,
this.imgNext.imgH,
this.imgNext.areaW,
this.imgNext.areaH,
this.imgNext.areaL,
this.imgNext.areaT,
this.radio,
);
}
}
复制代码
render 函数里面有两个变量 radio 和 scale,radio = IJ/EF,因此在一个场景切换动画中,咱们只须要改变 radio 的值,使其从 1 逐渐变小到等于 IJ/AB 便可。scale 就是这样一个用来表示 radio 变化速率的常量。这里咱们能够定义为 0.99,由于 requestAnimationFrame 的回调在浏览器里面大约一秒会执行 60 次, 而 o.99^240 = 0.08 因此大约 4s 左右,咱们就能够完成一个场景切换,这个速度仍是比较适中的。
从而,在动画中的任一时刻,EF 的大小能够表示为 IJ/this.radio,另外,由于全部的图片都是咱们的画师制做的,因此,每张图的像素大小(imgW、imgH)、小图在大图中的偏移位置 SI(areaL)、TI(areaT)、小图的宽高 IJ(areaW)、IL(areaH),都是已知的,根据这些已知的数据,咱们能够轻松的(对于数学好的同窗)将 drawImage 中未知变量用 this.radio 表示。
这样,咱们一个切换动画算是搞定了,可是咱们如何将多个切换动画串联起来呢,很简单,看看 draw()的代码,咱们只须要在 this.radio 达到临界值时候,将 index++,从新给 imgNext 和 imgCur 赋值。
最后将 render 函数写到 touchHandler 里面便可。
touchHandler(e) {
e.stopPropagation();
// e.preventDefault();
const render = () => {
this.radio = this.radio * this.scale;
this.timer = requestAnimationFrame(render);
this.draw();
};
cancelAnimationFrame(this.timer);
this.willPause = false;
// clearInterval(this.gif_timer);
this.timer = requestAnimationFrame(render);
}
复制代码
说是 gif 动画,可是实现上仍是用雪碧图+step 实现的。若是某一场景中有动画展现的环节,那么在过渡动画结束时,我就能够将 gif 图层展现出来,gif 图层有两部分构成,一个是背景图片,一个是动画区域。背景图片将动画区域留白,动画区域采用雪碧图+step 的方式,实现动画。这样作是为了减小图片资源大小,加快加载速度。
这个 H5 页面须要加载大量的图片,而这些图片必定要保证在用户交互以前加载完成,因此咱们要给页面初始化时候一个加载态,当全部图片加载完成后,咱们才展现可交互的页面。因此,咱们须要知道何时图片已经加载好了,上代码:
loadGifImg() {
const loadPromises = this.gifImgs.map(
item =>
new Promise((resolve, reject) => {
const img = new Image();
img.src = item;
img.onload = () => resolve(img);
img.onerror = () => reject();
}),
);
return Promise.all(loadPromises);
}
loadPageImg() {
const loadPromises = this.imgList.map(
(item, index) =>
new Promise((resolve, reject) => {
const img = new Image();
img.src = item.link;
img.i = index;
img.name = index;
img.className = 'item';
item.image = img;
img.onload = () => {
$('.collection').append(item.image);
resolve();
};
img.onerror = () => reject();
}),
);
return Promise.all(loadPromises);
}
复制代码
因此,咱们只须要等这两个 Promise resolve 了就加载完成了。
完整的代码 github 欢迎 star
微信扫码体验