pixi.js的使用经验
背景:为了实现2345天气王年终运营活动-疫情活动的动画效果,(欢迎扫码预览)
功能要点:
开发之前主要调研是否能够满足这些主要功能
1)页面动画元素可以根据用户滚动页面的单位时间内滚动的距离,控制动画效果(位移、速度即持续时间)
2)页面元素(精灵元素)支持点击事件
3)页面资源图片的加载进度(loading动画)
开发过程中踩坑记录:
1)将图片通过pixi.js转化为canvas之后,出现元素模糊的情况,绘画出的元素有锯齿;
解决方案:先绘制canvas的宽高为当前视口的2倍,然后通过transform进行缩小,平移到正确的位置;代码如下:
let app = new PIXI.Application({
view:document.getElementById('canvas'),
width:innerWidth * 2, // 放大两倍
height:innerHeight * 2, // 放大两倍
antialias: true, // 是否平滑抗锯齿,取消字体平滑,抗混叠。
forceCanvas: false,
resolution: 1,
clearBeforeRender: true
});
canvas {
transform: scale(0.5, 0.5) translate(-50%, -50%);
}
2)动态元素如:文案数据,怎么能跟着用户滚动的时候一起滚动;
解决方案:获取当前页面的滚动距离,对dom元素进行平移;
3)背景音乐的自动播放(目前手机在无用户行为的情况下几乎都不支持背景音乐的自动播放)
解决方案: (微信环境下)监听WeixinJSBridgeReady事件;
// 微信环境下的自动播放音乐
if(isWXBrowser()) {
document.addEventListener('DOMContentLoaded', function () {
function audioAutoPlay() {
let audio = document.getElementById('bg-music')
audio.play()
document.addEventListener("WeixinJSBridgeReady", function () {
audio.play()
}, false)
}
audioAutoPlay()
});
}
别的app,webview环境下依旧没有办法支持自动播放音乐,可以考虑下用户体验。建议把这部分功能去除掉; 或者改为当用户触摸屏幕的时候,自动播放;代码如下:
let audio = document.getElementById('bg-music')
function musicInBrowserHandler() {
audio.play()
document.body.removeEventListener('touchstart', musicInBrowserHandler)
}
document.body.addEventListener('touchstart', musicInBrowserHandler)
4)如何与UI确定动画元素的运动参数,尽可能还原实现动画效果;
解决方案: 页面中的所有具有运动效果的元素,首先:都遵循一个原则;具有初始位置,以及动画结束位置(x,y);
动画开始的时间设置为delay:滚动页面的距离 / 页面的总高度;
动画持续的时间设置为duration: 动画结束时候元素的位置 - 动画开始时候元素的位置 / totalHeight; 以上位置信息需要和UI动画设计师确认;
技术选型:结合调研方案,确定以PixiJS+TweenMax+TimelineMax+AlloyTouch就能实现一镜到底的套路,每个库发挥它自己应有的作用,相互配合;
参考资源如下:
参考博文:墨霁青玉:网易四字魔咒
github链接: pixi中文翻译
官网链接: TweenMax
流程简介
(1) 创建pixi应用,预加载图片资源(loader.add)
// 创建应用
let app = new PIXI.Application({
width:1334,
height:750
});
//
// 创建资源加载器loader ,进行资源预加载
const loader = new PIXI.loaders.Loader();
// 链式调用添加图片资源
loader.add('bg1', './imgs/bg1.png')
.add('bg_desk', './imgs/bg_desk.png')
.add('bg_person','./imgs/bg1_person.png')
// 监听加载进度,显示加载进度
loader.on("progress", function(target, resource) { //加载进度
document.getElementById('percent').innerText = parseInt(target.progress)+"%";
});
// 监听加载完毕
loader.once('complete', function(target, resource) { //加载完成
document.getElementById('loading').style.display = 'none'; // 隐藏进度条
document.body.appendChild(app.view); // 将pixi应用插入真实DOM中
initScenes(); // 初始化场景
initSprites(); // 初始化精灵
initAnimation(); // 初始化动画
initTouch(true, 'y');
});
// 开始加载资源
loader.load();
(2) 初始化场景:通过new PIXI.Container()函数创建每个场景,将他们加入PIXI舞台中, 每个场景包含宽高,位置等信息;
根据设计图,得出每个场景的长宽与位置。新建场景容器,等建完场景,才可以进行下一步的操作,将精灵放进场景里。
建场景的操作比较简单,定义每个场景的数据,新建Container对象并加入舞台中。这里的实现主要有三个东西:
①场景数据 scenesOptions:定义每个场景的数据
②对象集合 scenes:PIXI.Container对象,在初始化精灵的时候需要用到,所以需要将每个场景对象进行存储
③循环函数 initScenes:初始化场景并加入舞台里
const scenesOptions = [ // 场景数据:定义每个场景的宽高,x/y距离
{
name:"scene1",
x:0,y:0,
width:2933,height:750
},
{
name:"scene2",
x:2933,y:0,
width:1617,height:750
},
....
];
const scenes = {}; // 场景集合 - pixi对象
function initScenes(){ // 循环场景数组初始化场景
for (let i = scenesOptions.length-1; i >= 0 ; i--) {
scenes[scenesOptions[i].name] = new PIXI.Container({
width:scenesOptions[i].width,
height:scenesOptions[i].height
});
scenes[scenesOptions[i].name].x = scenesOptions[i].x;
app.stage.addChild(scenes[scenesOptions[i].name]);
}
}
(3) 初始化精灵:定义每个精灵的位置等属性数据,将其加到对应的场景中
这一步的操作与上一步操作一致:数据集,对象集,循环函数。
只是在这里我把精灵的初始化拆分成两个函数,一个是循环精灵数组,一个是循环每一个精灵的属性。再加了一个特殊属性的函数。根据需要,可以对某些精灵进行特殊的操作,都统一放在initSpecialProp函数里。
const spritesOptions = [ // 精灵数据:定义每个精灵的坐标
{ // 第一个场景的精灵
bg1:{
position:{x:0,y:0}
},
bg_person:{
position:{x:0,y:19},
anchor:{x:0.5,y:0.5}
},
....
},
{ // 第二个场景的精灵
bg_desk:{
position:{x:2213,y:38}
},
....
}
];
const sprites = {}; // 精灵集合 - pixi对象
function initSprites(){ // new出所有精灵对象,并交给函数initSprite分别赋值
for (let i = 0; i < spritesOptions.length; i++) {
let obj = spritesOptions[i];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
sprites[key] = PIXI.Sprite.fromImage(key);
initSprite(sprites[key],obj[key],i+1);
}
}
}
initSpecialProp();
}
function initSprite(sprite,prop,i){ // 初始化单个精灵的属性并加入对应的场景中
for (let key in prop) {
if (prop.hasOwnProperty(key)) {
sprite[key] = prop[key];
}
}
scenes['scene'+i].addChild(sprite);
}
function initSpecialProp(){ // 若有特殊精灵要处理特殊属性,可在此函数内处理
// sprites.mother_left.pivot.set(0,51);
// sprites.mother_right.pivot.set(95,50)
}
(4) 进行到第三步,所有的精灵都绘制出来了,但是此时屏幕还不可以拖动,这里需要用到一个滑动的库,我用的是AlloyTouch。通过AlloyTouch可以检测用户滑动的距离,在对应的滑动的change回调函数里可以改变舞台的位置app.stage.position.x来实现舞台的拖动效果。这样用户就可以通过滑动查看了。
new AlloyTouch({ ... }):
①touch定义触摸的DOM对象,在这里我们就直接是body了;
②vertical定义触摸的方向(横向滑动,还是竖向滑动。这里涉及设备是横屏还是竖屏,所以通过值vertical传进来。横屏则为false,竖屏则为true);
③可滚动的最大距离max跟最小距离min。因为我们都是往左滑,往上滑,所以为负距离,所以最大值max为0。最小值为舞台的整体宽度再减去一整屏的宽度,然后再取负值;
④关键点在于change函数,他可以返回实时滚动的距离。通过计算可以得到当前滚动的距离占全部距离的百分比,这个百分比就是我们当前的进度。拿到这个百分比。(默认总进度为1)就可以通过timeline.seek函数就可以随时改变播放的进度。这就是为什么往回滑动动画会往回撤的原因。想一下我们平时看视频,我们滚动进度条的行为就是用户滑动页面的行为,所以这就是实现的关键点。
let alloyTouch;
function initTouch(vertical, val) {
let scrollDis = app.stage.width-max;
alloyTouch = new AlloyTouch({
touch:"body", //反馈触摸的dom
vertical: vertical, //不必需,默认是true代表监听竖直方向touch
min: -app.stage.width + max, //不必需,运动属性的最小值
maxSpeed: 1,
max: 0, //不必需,滚动属性的最大值
bindSelf: false,
initialValue: 0,
change:function(value){
app.stage.position[val] = value;
let progress = -value/scrollDis;
progress = progress < 0 ? 0 : progress;
progress = progress > 1 ? 1 : progress;
timeline.seek(progress);
}
})
}
(5) 进行到第四步,一切的效果都是静态的,我们的元素还没有动起来。要实现随着用户的滑动播放对应的动画效果,这里需要用到一个库TimelineMax。这是管理动画播放进度条的库。直接new 一个主时间轴timeline。
const timeline = new TimelineMax({ // 整个舞台的时间轴
paused: true
});
第四步骤和第五步骤合并起来的代码
const w = document.body.clientWidth,
h = document.body.clientHeight;
const min = (w<h)?w:h;
const max = (w>h)?w:h;
const timeline = new TimelineMax({ // 整个舞台的时间轴
paused: true
});
let alloyTouch;
function initTouch(vertical, val) {
let scrollDis = app.stage.width - max;
alloyTouch = new AlloyTouch({
touch:"body", //反馈触摸的dom
vertical: vertical, //不必需,默认是true代表监听竖直方向touch
min: -app.stage.width + max, //不必需,运动属性的最小值
maxSpeed: 1,
max: 0, //不必需,滚动属性的最大值
bindSelf: false,
initialValue: 0,
change:function(value){
app.stage.position[val] = value;
let progress = -value/scrollDis;
progress = progress < 0 ? 0 : progress;
progress = progress > 1 ? 1 : progress;
timeline.seek(progress);
}
})
}
(6) 给精灵加上动画,这里用到一个库TweenMax,可以用来创建补间动画。我们只需定义精灵的起始状态,最终状态,它能轻松帮助我们进行状态的过渡。用TweenMax创建完动画,将动画加到时间轴timeline对应的位置。delay:0.1表示在动画长度的百分十处开始播放。
TweenMax常用的三个函数如下:
①TweenMax.to(target,duration,statusObj) ——目标target从当前状态到statusObj状态过渡
②TweenMax.from(target,duration,statusObj) ——目标target从statusObj状态到当前状态过渡
③TweenMax.fromTo(target,duration,statusObjFrom,statusObjTo)——目标target从statusObjFrom状态到statusObjTo状态过渡
duration表示过渡时长。如果duration=0.1则表示过渡时长占滚动总长的10%,即占时间轴的10%。
delay跟duration的计算规则:
- delay = 开始播放动画时的滚动距离 / 可滚动总长度
delay是动画开始的时间
- duration = (结束播放动画时的滚动距离 - 开始播放动画时的滚动距离) / 可滚动总长度
duration是动画持续的时间
const animationsOptions = { // 精灵动画集合
windows:[{
prop:'scale', // 这里有个prop,有些人没有注意到这是什么
delay:0.05,
duration:0.3,
to:{x:3,y:3,ease:Power0.easeNone} // 在这里注意一下 ease:Power0.easeNone 是缓动函数
},{
delay:0.1,
duration:0.1,
to:{alpha:0}
}],
talk_1:[{
delay:0.15,
duration:0.1,
from:{width:0,height:0,ease:Power0.easeNone}
}]
}
function initAnimation(){
// delay=0.1 表示滚动到10%开始播放动画
// duration=0.1 表示运动时间占滚动的百分比
for (let key in animationsOptions) {
if (animationsOptions.hasOwnProperty(key)) {
let obj = animationsOptions[key];
for (let i = 0; i < obj.length; i++) {
let act;
let target;
if (obj[i].prop) {
target = sprites[key][obj[i].prop];
} else {
target = sprites[key];
}
if (obj[i].from & obj[i].to) {
act = TweenMax.fromTo(target,obj[i].duration,obj[i].from,obj[i].to);
} else if (obj[i].from) {
act = TweenMax.from(target,obj[i].duration,obj[i].from);
} else if (obj[i].to) {
act = TweenMax.to(target,obj[i].duration,obj[i].to);
}
let tm = new TimelineMax({delay:obj[i].delay});
tm.add(act,0);
tm.play();
timeline.add(tm,0);
}
}
}
// 特殊动画特殊处理
let act = TweenMax.to(scenes.scene1,0.3,{x:2400});
let tm = new TimelineMax({delay:0.25});
tm.add(act,0);
timeline.add(tm,0);
}
tip!!!!!(猜猜上面的prop是干什么用的)
注意一下,TweenMax.from(target,duration,statusObj)等方法只识别target的属性。举个例子,精灵sprite的width从0变到100
TweenMax.to(sprite, 0.1, { width: 100}) // 即 sprite.width变为100
那么问题来了,要改变属性的属性呢?例如sprite.scale.x变为2,则target应该变为sprite.scale
TweenMax.to(sprite.scale, 0.1, { x: 2}) // 即 sprite.scale.x变为100
(7) 在第三步的时候,用户滑动的回调函数加上 timeline.seek(progress)就可以实现滑动到某个位置播放对应的动画
这里, 汇总一下每个库的作用
-
PixiJS: 绘图,其中包括舞台、场景、普通精灵、动画精灵、平铺精灵、定时器等。
-
TweenMax:制作过渡动画
-
TimelineMax:管理整个舞台的动画播放进度
-
AlloyTouch:实现滑动效果,监听用户滑动
PixiJS+TweenMax+TimelineMax+AlloyTouch就能实现一镜到底的套路,每个库发挥它自己应有的作用,相互配合