H5场景小动画实现之PixiJs实战
实现目标效果
本次我们需要实现的动画效果是横向滚动,切换不同场景幕布,一些场景幕布内不同层需要层次分明的效果,一些场景幕布需要以某个点为中心点放大整个场景的效果;场景内,绘制图片,加装饰,部分装饰需要单独加动画,可能是缓动效果,自动播放;也可能是与用户的触摸操作有关。
在设计师给我们描绘了如此强大的动画效果后,我们考虑用PixiJs来实现整体的动画效果。
实际实现效果:
https://event.midea.com/act/mystery/carnival (二维码自动识别)
【注:请在H5微信打开看效果】
1. 绘制场景内容——PixiJs
1.1 为什么选择PixiJs
PixiJS是一个快速的轻量级的2D动画渲染引擎,主要使用了WebGL技术,能帮助展示、驱动和管理富有交互性的图形、制作游戏。使用JavaScript以及其他HTML5技术,结合PixiJS引擎,可以创建出丰富的交互式图形,跨平台的应用程序和游戏。
而对于我们来说,pixiJS每个元素都可以单独操作,单独设置属性来控制动效。在绘制图片时,我们可以通过requestAnimationFrame不断循环,来实现切换同一位置上,不同元素的擦除和绘制效果。
1.2 PixiJs实现思路
我们给每个元素单独设置一些属性来满足我们的动效。主要实现思路如下:
- 渲染单张图片时,可以使用addChild添加到舞台,然后用render来刷新。
- 对于图片的位置,可以给每个图片用position属性设置x、y坐标来精准还原设计稿。
- 实现层次分明的背景动画效果,我们考虑用相对位移实现,原理是给不同的图层设置不同的运动系数,在用户划动幕布时,产生不相等的位移,从而在视觉上产生层次分明的效果。主要计算公式是:
// p.data.position.x为元素初始位置 ,p.data.speed为运动系数(不一致),distance为相对于初始位置的运动位移
p.x = p.data.position.x + (p.data.speed.x) * distance;
- 在场景增多,图片元素增加的时候,为了更完整的动画效果和用户体验,需要图片预加载,所以考虑用PIXI.loader来实现图片的预加载,然后在图片预加载回调里执行动画
- 对于连续播放的动画,考虑使用PIXI.extras.AnimatedSprite来实现,并通过animationSpeed精准控制运动速度、play/stop控制播放/暂停、gotoAndStop精细到第几帧等来满足设计的动效需求。
- 对于与相对位移相关的动画,使用与位移产生相关联的参数来控制动效
- 字幕的实现,可以用PIXI.Text来加载和设置字体,然后设置position属性来控制位置,addChild添加到画布和舞台,render来刷新。
- 多个画布,选择用ScrollerJs来实现画布的横向滚动效果,多个画布的管理,我们选择将数据抽象成对象,尽量使用循环实现来减少代码量。
接下来,我们将按照这个思路来阐述我们的实现过程。
1.3 实现
本次场景都是一整屏,所以先声明宽高:
var windowHeight = window.innerHeight,
windowWidth = window.innerWidth;
1.3.1 创建舞台(stage)和渲染器 (renderer)
舞台(本例中objPixiContainer ):所有要渲染的对象都必须连接到舞台中才能被显示出来,舞台处于整个树形展示结构的最底层,可以理解为背景。
渲染器(本例中pixiRender ):选用canvas或webGL进行渲染的一个区域
//创建渲染器,宽高为视窗的宽高
var pixiRender = new PIXI.autoDetectRenderer(windowWidth, windowHeight);
//把渲染器添加到HTML结构里
$(".game_con_wrap")[0].appendChild(pixiRender.view);
//创建一个容器对象:舞台
var objPixiContainer = new PIXI.Container();
//告诉渲染器去渲染舞台
pixiRender.render(objPixiContainer);
其中,autoDetectRenderer方法根据浏览器的支持情况,选择用WebGL还是Canvas去绘制画面,默认是WebGL。但是因为在我司设计师的手机(ip 6sp)上多次打开后出现画面黑屏闪动的副作用,因此选用强制使用CanvasRenderer来进行渲染。
var pixiRender = new PIXI.CanvasRenderer(windowWidth, windowHeight);
运行,canvas已经渲染到了全屏幕上:
画布黑乎乎的,于是我们摆点元素上去:
1.3.2 创建精灵(sprite)
特殊的图片对象被叫做精灵图。你可以控制它们的位置,尺寸以及其他许多有用的可以制作交互式动画图形的属性。Pixi有一个 Sprite 类,它是创建游戏精灵的通用方式。
创建精灵:
Pixi强大的loader对象可以用来做图片的预加载。以下代码显示了如何加载图片,并在图片加载完成后执行setup回调。
PIXI.loader
.add("images/anyImage.png")
.load(setup);
function setup() {
//This code will run when the loader has finished loading the image
}
Pixi开发团队推荐,使用了loader,就应该通过引用loader 的 resources 对象来创建精灵。
以下代码就可以通过loader的resources来加载一个图像,调用loadingFinish函数,并且从加载过的图像中创建一个精灵,其中一连串的 add 方法来一次性加载许多图像:
//图片预加载
PIXI.loader
.add("https://g.mdcdn.cn/h5/img/act/201711/new-1-1.jpg")
.add("https://g.mdcdn.cn/h5/img/act/201711/new-1-2.png")
.on("progress", function(){
//do sth when loading
})
.load(loadingFinish);
//加载完成回调
function loadingFinish() {
//创建一个精灵
var sprite = new PIXI.Sprite(
PIXI.loader.resources["https://g.mdcdn.cn/h5/img/act/201711/new-1-1.jpg"].texture
);
//添加到舞台
objPixiContainer.addChild(sprite)
//渲染到渲染器
pixiRender.render(objPixiContainer);
}
到这里,你会发现,第一个场景的第一张图就渲染完了。
同理,我们再渲染第二张静态背景图:
var sprite2 = new PIXI.Sprite(
PIXI.loader.resources["https://g.mdcdn.cn/h5/img/act/201711/new-1-2.png"].texture
);
objPixiContainer.addChild(sprite2)
pixiRender.render(objPixiContainer);
聪明的你一定看出来了,每次添加精灵或者精灵有动作之后,都需要重新去渲染舞台。这里有两种解决方案:
- 每次动作完后,手动执行一遍 pixiRender.render(objPixiContainer);
- 利用RAF不断去刷新舞台,只需要调用一次pageUpdate()就可以了。
function pageUpdate() {
requestAnimationFrame(pageUpdate);
pixiRender.render(objPixiContainer);
}
完成后,发现图片定位是初始位置跟设计稿不符合,这里可以再去查PixiJs API,给当前精灵设x轴 Y轴的位置:
sprite2.position.set(290, 297)
照这样,再加一个精灵new-1-3.png就可以看见第一个场景完整的框架了:
1.3.3 创建连续播放的动画
在完成上述背景的构建后,设计要求在幕布上云朵要有缓动效果,并给出一系列序列帧。
实现效果如下:
例子可以看:
这时候,我们可以考虑使用PIXI.extras.AnimatedSprite来实现。
//声明AnimatedSprite的序列帧
var urlPadding = "https://g.mdcdn.cn/h5/img/act/201711/",
act_1_animate_bg_img_arr = [];
for (let $e = 0; $e < 11; $e++) {
act_1_animate_bg_img_arr.push(urlPadding + "new-1-sky-" + ($e + 1) + ".jpg");
}
//精灵
var sprite4 = new PIXI.extras.AnimatedSprite.fromImages(act_1_animate_bg_img_arr)
//精灵添加到舞台
objPixiContainer.addChild(sprite4)
//找到序列帧所在的图层,放到对应位置,并设置移动速度
sprite4.animationSpeed = 0.1; //控制速度
sprite4.play();//自动播放序列帧
如果需要控制跳到第几帧可以使用gotoAndStop/gotoAndPlay API; 这里第一帧,我们基本就完成了。
同理,后文中,奔跑的狼影子动画,也会这样实现。不同的只是图片。
单独查看例子:Jsfiddle: Running wolf
1.3.4 添加字幕
用new PIXI.Text实现:
新建舞台:
var scriptText_con;
objPixiContainer.removeChild(scriptText_con);
scriptText_con = new pixiContainer;
scriptText_con.position.set(0, 628);
新建文字渲染:
var textSample = new PIXI.Text("老板大人", {fontSize: '22px', fill: 'white', align: 'center'});
文字渲染到舞台:
scriptText_con.addChild(textSample);
渲染到整个舞台:
objPixiContainer.addChild(scriptText_con);
1.3.5 多个场景管理
我们发现有多个场景,可以在objPixiContainer这个舞台下创建多个舞台来管理多个场景:
//声明一个舞台
var stageContainer = new pixiContainer();
//多个场景(画布)
let scenes = [Act_1, Act_2, Act_3, Act_5, Act_6, Act_7, Act_8_1, Act_8, Act_10, Act_11];
//每个场景又是一个舞台,依次渲染初始位置和舞台精灵
for (let j = 0; j < scenes.length; j++) {
scenes[j] = new pixiContainer();
scenes[j].pivot.set(act_sprites_list[j][0].position.x, 0);
scenes[j].position.set(act_sprites_list[j][0].position.x, 0);
}
//每个actContainerArr依次赋值初始位移和渲染背景
actContainerArr = scenes;
//todo 精灵渲染
//场景舞台添加到背景
objPixiContainer.addChild(stageContainer);
//每个场景添加到场景舞台
stageContainer.addChild(Act_1, Act_2, Act_3, Act_5, Act_6, Act_7, Act_8_1, Act_8, Act_10, Act_11);
那么问题又来了,我要怎么横向切换第二个场景呢? 这里我们选择使用ScrollerJS。
2. 场景切换——ScrollerJS
2.1 为什么选择ScrollerJS
ScrollerJs是一个纯净的监听scroll和zoom的组件。
2.2 ScrollerJS实现思路
当用户手指滑动屏幕时,触发滚动,通过监听滚动事件,在滚动一定距离触发事件,事件参数包含位置信息等,根据位置信息决定渲染内容(画布显示隐藏,图片位置等)。
2.3 实现
首先,需要给scroller设置容器宽高限制:
scrollerObj.setDimensions(clientWidth, clientHeight, contentWidth, contentHeight);
所以,这里我们设置:
var scrollX=10205;//视具体视觉效果而定
objScroller.setDimensions(windowWidth, windowHeight, scrollX + windowWidth, windowHeight);
然后实时监听鼠标滚动的距离:
//控制位移
function scrollerCallback(left, top, zoom) {
console.log(`当前滚动条位置:${left},${top}`)
var cur_Y, cur_X;
if (GlobalRotation > 0) {
cur_X = top;
cur_Y = left;
} else {
cur_X = left;
cur_Y = top;
}
//todo 位移距离对应在哪个具体场景
if(cur_X>950){
//第一个场景内 执行动画
}else if(...){
...
}
//当前场景相对移动
stageContainer.position.x = -cur_X;
stageContainer.position.y = -cur_Y;
}
var objScroller = new Scroller(scrollerCallback, {
zooming: false,
animating: true,
bouncing: false,
animationDuration: 1000
});
objScroller.__enableScrollY = true;
这个滚动区域创建完成后,我们还需要把滚动区域与渲染器关联起来。
objPixiContainer.on("touchstart", onTouchStart)
.on("touchmove", onTouchMove)
.on("touchend", onTouchEnd);
function onTouchStart(e) {
// start_stage_con.visible = false;
var i = e.data.originalEvent;
isTouching = true;
objScroller.doTouchStart(i.touches, i.timeStamp);
}
function onTouchMove(e) {
if (isTouching) {
var i = e.data.originalEvent;
objScroller.doTouchMove(i.touches, i.timeStamp, i.scale);
}
}
function onTouchEnd(e) {
var i = e.data.originalEvent;
objScroller.doTouchEnd(i.timeStamp);
isTouching = false;
}
然后,我们只需要在元素发生滚动时,在scrollerCallback回调里去根据滚动条的位置判断当前是在哪个场景里,然后执行对应动画效果就好了。音效播放也是根据滚动条的位置来确认画布位置。
到这里,我们就可以控制画布的移动了。
当场景变多的时候,我们把所有的场景相关的参数都配成对象,然后通过操作对象来设置动效。
act_1_sprites_arr = [
{
name: "bg-1",
url: urlPadding + "new-1-1.jpg",
position: {x: 0, y: 0},//相对于画布左上角初始位置的坐标
speed: {x: .02, y: 0},//移动速度
gif: true,//是否是有序列帧
fromImages: act_1_animate_bg_img_arr //序列帧图
},
{name: "mid-1", url: urlPadding + "new-1-2.png", position: {x: 290, y: 297}, speed: {x: -.1, y: 0}, gif: false},
{
name: "front-1",
url: urlPadding + "new-1-3.png?t=1",
position: {x: -200, y: 0},
speed: {x: -.18, y: 0},
gif: false
}
];
如果需要平滑放大场景,且随着位移放大系数放大或缩小,那就需要与位移产生关系。
var distance = cur_X - switchX.to3;//在第三个场景内的相对位移
let o_point_3_4 = {x:200, y:355}, tmp_x_3_4 = 0, s_3_4 = 0, scale_v_3_4 //3放大
//800为当进入该场景,并发生800的相对位移 开始放大
tmp_x_3_4 = (distance - 800) / 400; //放大系数
s_3_4 = tmp_x_3_4 * tmp_x_3_4 / 3;
let tmp_x = back1.data.position.x + 800;
let tmp_y = back1.data.position.y;
// 放大
Act_3.pivot.set(tmp_x - o_point_3_4.x * s_3_4, tmp_y - o_point_3_4.y * s_3_4);
scale_v_3_4 = 1 + s_3_4
Act_3.scale.set(scale_v_3_4,scale_v_3_4);
实现效果为:
坑:场景内的滚动条位置,最好用相对位移来算,不然满屏全是绝对位移,改起来非常头疼。
到这里,基本整个画卷可以基本动起来了,就剩一下小元素了。比如突然移动到某个点,突然蹿出来的狼影。这里,我们用到了TweenJS。
3. 装饰动效——结合TweenJS
动画效果实现思路:
- 可以回溯的动效使用AnimatedSprite + ScrollerJS结合相对位移逐帧渲染。
- 不能中断的使用 TweenJs 来填充实现。
3.1 为什么考虑TweenJS
Tween.js,类似于jQuery的animate方法和CSS3动画。以平滑的方式修改元素的属性值。只需要告诉TweenJS你想修改什么值,以及动画结束时它的最终值是什么,动画花费多少时间等信息,tween引擎就可以计算从开始动画点到结束动画点之间值,就可以产生平滑的动画效果。
3.2 TweenJS实现思路
定义起始、结束状态和时间,中间的过渡状态由TweenJS自动填充缓动效果。
3.3 实现
//runningWolf是一个精灵sprite,准确来说是animatedsprite
//开始位置声明
var startPositionW = runningWolf.data.position;
//endX : 结束位置(X轴)
new TWEEN.Tween(startPositionW)
.to({x: endX}, 1 * 1000)
.onUpdate(function () {
runningWolf.x = this.x
})
.onComplete(function () {
runningWolf.visible = false;
scrollerAnimatingWolfRunning = false;
runningWolf.x = runningWolf.data.position.x
})
.start();
实现效果为:
4. 声音管理
对声音的预加载,我们使用了audio标签的preload来实现:
<audio src="https://g.mdcdn.cn/h5/music/act/sound2017/ms_canival_bgm.mp3" id="ms_canival_bgm" preload="auto" loop="loop" ></audio>
但是我们很快发现,在微信浏览器中,ios11等系统的限制使得音频没办法自动播放,因此,我们使用一下方法来处理:
function playBgmAfterLoading(e) {
var i = document.getElementById(e);
i.play();
var n = function () {
document.removeEventListener("WeixinJSBridgeReady", n);
i.play();
};
document.addEventListener("WeixinJSBridgeReady", n, false);
}
这个原理其实是利用了微信WeixinJSBridgeReady的接口,这个接口是对手机的音频自动播放限制进行处理过了的。 经过这样特殊处理后,在微信浏览器中就可以自动播放音频了。
到什么位置播放什么音频,就需要结合ScrollerJs来管理了。
5. 自动缩放
我们选择以750px宽度为缩放标准,然后算出一个比例: screenScaleRito = window.innerHeight/ 750;(横屏状态下),然后结合横竖屏缩放。
objPixiContainer.scale.set(screenScaleRito, screenScaleRito);
pixiRender.resize(windowWidth, windowHeight);
objPixiContainer.position.set(0, 0);
6. 横竖屏处理
原理:用浏览器窗口文档显示区域的宽度(不包括滚动条)和屏幕的宽高对比来判断横竖屏状态。 横竖屏处理完整代码:
function detectOrient() {
var storage = localStorage; // 不一定要使用localStorage,其他存储数据的手段都可以
var data = storage.getItem('J-recordOrientX');
var cw = document.documentElement.clientWidth;
var _Width = 0,
_Height = 0;
if (!data) {
var sw = window.screen.width;
var sh = window.screen.height;
// 2.在某些机型(如华为P9)下出现 screen.width/height 值交换,所以进行大小值比较判断
_Width = sw < sh ? sw : sh;
_Height = sw >= sh ? sw : sh;
storage.setItem('J-recordOrientX', _Width + ',' + _Height);
} else {
var str = data.split(',');
_Width = str[0];
_Height = str[1];
}
if (cw == _Width) {
// 竖屏
return 'portrait';
}
if (cw == _Height) {
// 横屏
return 'landscape';
}
}
detectOrient()
然后剩下的就是根据横竖屏来对渲染器和滚动区域进行缩放了。
相关参考:
http://www.createjs.cc/src/docs/tweenjs/modules/TweenJS.html
http://pixijs.download/release/docs/index.html
https://juejin.im/entry/58f864dfa0bb9f0065a28f9c
https://github.com/pbakaus/scroller