Appearance
使用 pixi.js 开发一镜到底 H5 (vue版)
总共的流程分为以下几步:
- 创建舞台
- 创建主容器(长图容器)
- 根据场景创建容器下的子容器(分屏容器)
- 整理图片素材,创建图片对象,加载图片素材,包括后续使用的序列帧图片
- 使用
phy-touch
监听滑动,并且根据滑动计算当前的位置,及滑动百分比 - 使用
gsap
中的timeline
,创建总体时间轴、和长图时间轴 - 使用
timeline.seek
方法,将此时的滑动位置作为参数,实现滚动距离和动画播放时间的映射 - 在长图时间轴中,根据滑动距离添加不同的素材动画,包括:
- 向下滑动长图的向右移动
- 星星的出现
- 场景 2 的出现,场景 1 透明度及大小的变化
- 音符的移动
- 黑夜缩小为小窗,黑夜缩小期间 工作中的男孩出现
- 制作序列帧动画
- 根据滑动百分比,在位置之前保持第 1 张图,超过滑动距离则是最后一张图,当前位置图片序号=((当前滑动的距离 - 开始的距离/持续距离)*序列帧图片数量)
- 制作孩子走路序列帧
- 结尾漩涡序列帧
- 加载声音素材,包括背景音乐、星星声音、鼓掌声音
- 与序列帧制作逻辑相似,在 当前滑动的距离 到达预定的滑动位置时,先停止当前播放的声音,再播放预定的声音,为了避免一个声音的多次触发播放
- 制作星星出现声音
- 制作鼓掌声音
- 设置加载进度百分比,加载完成后才可以播放 H5,一旦开始播放,背景声音开始播放,优化播放图标,点击可以静音
1. 创建舞台
javascript
const appPIXI = new PIXI.Application({
width: 750,
height: 1448
})
const rootApp = document.querySelector('body #app')
1.1 创建长图容器
javascript
// 添加背景精灵容器
const spriteGroupBg = new PIXI.Container()
appPIXI.stage.addChild(spriteGroupBg)
spriteGroupBg.name = 'spriteGroupBg'
// 添加场景精灵容器(长图精灵组)
const spriteGroupScenes = new PIXI.Container()
appPIXI.stage.addChild(spriteGroupScenes)
1.2 创建分屏容器
javascript
// 向 添加场景精灵容器 添加 分屏容器
const scene1 = new PIXI.Container()
scene1.position.set(1784, 621)
scene1.pivot.set(1784, 621)
scene1.name = 'scene1'
const scene2 = new PIXI.Container()
scene2.position.set(1773, 0)
scene2.alpha = 0
scene2.name = 'scene2'
const scene3 = new PIXI.Container()
scene3.position.set(4960, 0)
scene3.name = 'scene3'
const scene4 = new PIXI.Container()
scene4.position.set(7902, 0)
scene4.name = 'scene4'
spriteGroupScenes.addChild(scene1)
spriteGroupScenes.addChild(scene2)
spriteGroupScenes.addChild(scene3)
spriteGroupScenes.addChild(scene4)
const spriteGroupLast = new PIXI.Container()
spriteGroupLast.position.set(-203, 0)
appPIXI.stage.addChild(spriteGroupLast)
2. 整理图片素材及导入到 PIXI 中
2.1 整理图片素材为对象
使用对象的形式导入图片素材,包含后续导入到具体的容器、位置、透明度等其他设置。 其中也包括序列帧图片的导入,首先导入序列帧的第一张。后续修改序列帧对象的纹理图就可以达到序列帧的效果, 并为每个对象增加别名,便于在后期操作时根据别名找到对应的精灵,进行动画的操作
javascript
// 引入各个容器
import {
spriteGroupBg,
scene1,
scene2,
scene3,
scene4,
spriteGroupLast
} from '@/js/container'
// 通用图片
const indexImgArr = [
{
name: 'bg',
src: '../src/assets/images/bg.jpg',
position: {
x: 0,
y: 0
},
container: spriteGroupBg
}
]
const scene1ImgArr = [
{
name: 'p1_bg',
src: './src/assets/images/p1-bg.png',
position: {
x: 0,
y: 0
},
container: scene1
},
{
name: 'p1_cloud1',
src: './src/assets/images/p1-cloud1.png',
position: {
x: -20,
y: 177
},
container: scene1
}
]
// ... 其他导入
const scene4ImgArr1 = [
{
name: 'p4_1',
src: './src/assets/images/p4-1.png',
position: {
x: 691,
y: 529
},
container: scene4
},
{
name: 'p4_bg',
src: './src/assets/images/p4-bg.png',
position: {
x: 558,
y: 0
},
container: scene4
},
{
name: 'p4_house3',
src: './src/assets/images/p4-house3.png',
position: {
x: 0,
y: 0
},
container: scene4
},
{
name: 'p4_start',
src: './src/assets/images/p4-start.png',
position: {
x: 1398,
y: 0
},
container: scene4
},
{
name: 'xuan',
src: './src/assets/images/x1.png',
position: {
x: 0,
y: 0
},
alpha: 0,
container: spriteGroupLast
}
]
// 孩子走路序列帧
export const childMove = []
for (let i = 1 + 1; i <= 34; i++) {
const imgSrc = `./src/assets/images/w${i}.png`
childMove.push(PIXI.Texture.from(imgSrc))
}
2.2 导入到 PXIXI
为了将图片快速导入到对应容器,新增了一个方法
javascript
import * as PIXI from 'pixi.js'
const addSpriteToContainer = (item) => {
const sprite = PIXI.Sprite.from(item.src)
sprite.position.set(item.position.x, item.position.y)
if (item.alpha !== undefined) {
sprite.alpha = item.alpha
}
sprite.name = item.name
if (item.container !== undefined) {
item.container.addChild(sprite)
}
}
export default addSpriteToContainer
javascript
import { allImgArr } from '@/js/imgData'
// 添加精灵到场景容器
allImgArr.map((item) => {
addSpriteToContainer(item)
})
3. 监听滑动
使用 phy-touch
活动滚动位置
javascript
import AlloyTouch from 'phy-touch'
// 最大的滑动距离,设计图宽度 - 一个屏幕的距离
const MAXLONE = -(10800 - 750)
new AlloyTouch({
touch: '#app',
maxSpeed: 0.8,
max: 0,
min: MAXLONE,
value: 0,
change: (v) => {
const progress = v / MAXLONE
if (bgm.isPlaying) {
bgmIsPlaying.value = true
}
// 滚动百分比
const progressPercent = Math.round(progress * 100) + '%'
}
})
4. 创建时间轴
使用 gasp
创建总时间轴,以及长图时间轴,总时间轴设置暂停,因为后续需要根据滚动的距离/百分比来执行对应的动画进度
长图时间轴设置 delay
延迟属性为 0,表示滑动触发的距离为 0
javascript
import gsap from 'gsap'
// 导入总长
import { MAXLONE } from './constant'
// 导入长图容器
import { spriteGroupScenes } from '@/js/container'
// 总时间轴
const allTimeLine = gsap.timeline({ paused: true })
// 长图时间轴
const sceneTimeLine = gsap.timeline({ delay: 0 })
export { allTimeLine, sceneTimeLine }
5. timeline.seek 方法
在new AlloyTouch()
方法中的change
属性中,新增allTimeLine.seek(progress)
javascript
new AlloyTouch({
touch: '#app',
maxSpeed: 0.8,
max: 0,
min: MAXLONE,
value: 0,
change: (v) => {
const progress = v / MAXLONE
if (bgm.isPlaying) {
bgmIsPlaying.value = true
}
allTimeLine.seek(progress)
}
})
6. 添加动画
6.1 长图滑动动画
添加长图移动,需要使用 gasp
中的 to
方法,设置对象,以及执行的效果
- 后续测试,可以只使用一个长图时间轴实现对应的效果
javascript
// 导入时间轴
import { allTimeLine, sceneTimeLine } from '@/js/timeLine'
// 导入长图容器
import { spriteGroupScenes } from '@/js/container'
// 设置长图移动
sceneTimeLine.to(spriteGroupScenes, {
// x轴移动到的位置
x: MAXLONE,
// 持续的时间 ,一镜到底H5.通常设置为1,
duration: 1
})
// 添加到总时间轴中
allTimeLine.add(sceneTimeLine)
6.2 星星的出现
getChildByName
就是根据图片的别名来查找对应的二级容器,以及二级容器下的精灵,
javascript
// 长图容器 spriteGroupScenes
// 定义动画 星星出现 spriteGroupScenes / scene1 / p1_star
const starAnimation = gsap.to(
spriteGroupScenes.getChildByName('scene1').getChildByName('p1_star'),
{
alpha: 1,
delay: -15 / MAXLONE,
duration: -25 / MAXLONE
}
)
// 在长图时间轴中添加动画
sceneTimeLine.add(starAnimation, 0)
6.3 场景 2 的出现,场景 1 透明度及大小的变化
javascript
// 可以选择场景进行 scale 旋转等操作
const scene1Sprite = spriteGroupScenes.getChildByName('scene1')
const scene1SpriteScaleAnimation = gsap.to(scene1Sprite.scale, {
x: 3,
y: 3,
delay: -600 / MAXLONE,
duration: -190 / MAXLONE
})
// 设置场景的透明度
const scene1SpriteAplapAnimation = gsap.to(scene1Sprite, {
alpha: 0,
delay: -600 / MAXLONE,
duration: -190 / MAXLONE
})
// 设置场景2 放大 透明度变为1
const scene2Sprite = spriteGroupScenes.getChildByName('scene2')
const scene2SpriteScaleAnimation = gsap.to(scene2Sprite, {
alpha: 1,
delay: -700 / MAXLONE,
duration: -100 / MAXLONE
})
sceneTimeLine.add(scene1SpriteScaleAnimation, 0)
sceneTimeLine.add(scene1SpriteAplapAnimation, 0)
sceneTimeLine.add(scene2SpriteScaleAnimation, 0)
6.4 音符的移动
javascript
// 获取音符精灵
const yinfuSprite = spriteGroupScenes
.getChildByName('scene2')
.getChildByName('p2_yinfu')
// 音符动画
const yinfuSpriteAnimation = gsap.to(yinfuSprite, {
x: 3400,
y: 300,
alpha: 0,
delay: -2450 / MAXLONE,
duration: -500 / MAXLONE
})
// 添加音符动画到时间轴
sceneTimeLine.add(yinfuSpriteAnimation, 0)
6.5 黑夜缩小为小窗,黑夜缩小期间 工作中的男孩出现
javascript
const chuanghu = spriteGroupScenes
.getChildByName('scene3')
.getChildByName('p3_2')
// 小窗大小改变
const chuanghuScaleAnimation = gsap.from(chuanghu.scale, {
x: 5,
y: 5,
delay: -2800 / MAXLONE,
duration: -800 / MAXLONE
})
// 位置的改变
const chuanghuPositionAnimation = gsap.from(chuanghu.position, {
x: 0,
y: -20,
delay: -2800 / MAXLONE,
duration: -800 / MAXLONE
})
sceneTimeLine.add(chuanghuScaleAnimation, 0)
sceneTimeLine.add(chuanghuPositionAnimation, 0)
javascript
const workingBoy = spriteGroupScenes
.getChildByName('scene3')
.getChildByName('p3_1')
// 黑夜缩小期间 工作中的小男孩出现
const workingBoyAnimation = gsap.to(workingBoy, {
alpha: 1,
delay: -2980 / MAXLONE,
duration: -400 / MAXLONE
})
sceneTimeLine.add(workingBoyAnimation, 0)
7. 序列帧动画
定义开始位置、持续长度,并根据图片长度、滑动位置计算出索引。并在设定的范围内改变图片索引,修改孩子走路的精灵纹理
javascript
// ~~ 向上取整 与 Math.floor() 相似
export const animation = (progress) => {
// 孩子走路序列帧
// 开始位置
const childStartTime = -1000 / MAXLONE
// 持续长度
const childDurationTime = (-1300 - childStartTime) / MAXLONE
// 当前图片索引
const childIndex = ~~(
((progress - childStartTime) / childDurationTime) *
childMove.length
)
// 判断只有在范围内才可以改变图片索引
if (childIndex > 0 && childIndex < childMove.length - 1) {
// 修改对应精灵的纹理
scene2.getChildByName('childMove').texture = childMove[childIndex]
}
// 结尾漩涡动画
const xuanStartTime = -6613 / MAXLONE
const xuanDurationTime = -1000 / MAXLONE // 持续时间通常需要根据具体情况来处理
const xuanIndex = ~~(
((progress - xuanStartTime) / xuanDurationTime) *
xuan.length
)
if (xuanIndex <= 0) {
spriteGroupLast.getChildByName('xuan').alpha = 0
}
if (xuanIndex > 0 && xuanIndex < xuan.length - 1) {
spriteGroupLast.getChildByName('xuan').alpha = 1
spriteGroupLast.getChildByName('xuan').texture = xuan[xuanIndex]
}
}
完成animation
方法后需要在 new AlloyTouch()
方法中增加调用
javascript
new AlloyTouch({
change: (v) => {
const progress = v / MAXLONE
// ...
animation(progress)
}
})
8. 加载声音素材
设置背景音乐循环 bgm.loop = true
javascript
import { sound } from '@pixi/sound'
export const bgm = sound.add('bgm', './src/assets/audio/bg.mp3')
bgm.loop = true
export const ding = sound.add('ding', './src/assets/audio/ding.mp3')
export const huanhu = sound.add('huanhu', './src/assets/audio/huanhu.mp3')
8.1 制作星星和鼓掌的声音
当前滑动的距离 到达预定的滑动位置时,先停止当前播放的声音,再播放预定的声音,为了避免一个声音的多次触发播放
javascript
export const playSound = (progress) => {
// 音频出现时间
const timeDur = 20
// 星星和孩子啼哭播放的时间
const auStar_StartTime = -40 / MAXLONE
const auStar_EndTime = -(40 + timeDur) / MAXLONE
if (progress >= auStar_StartTime && progress < auStar_EndTime) {
// 先停止
ding.stop()
// 再播放
ding.play()
}
// 如果位置小于开始位置,则停止
if (progress < auStar_StartTime) {
ding.stop()
}
// 欢呼播放的时间
const auHuanHu_StartTime = -2270 / MAXLONE
const auHuanHu_EndTime = -(2270 + timeDur) / MAXLONE
if (progress >= auHuanHu_StartTime && progress < auHuanHu_EndTime) {
huanhu.stop()
huanhu.play()
}
if (progress < auHuanHu_StartTime) {
huanhu.stop()
}
}
完成playSound
方法后需要在 new AlloyTouch()
方法中增加调用
javascript
new AlloyTouch({
change: (v) => {
const progress = v / MAXLONE
// ...
playSound(progress)
}
})
9. 其他
9.1 增加资源加载进度
使用PIXI.Assets.load()
方法加载所有图片,并获取进度,在页面进入时显示
javascript
const allImgArrLoad = PIXI.Assets.load(allImgArr, (progress) => {
percent.value = Math.round(progress * 100) + '%'
})
9.2 完成资源加载
完成后播放音乐,显示音乐图标,显示页面
javascript
allImgArrLoad.then((res) => {
bgm.play()
musicIcon.value = true
percentShow.value = false
rootApp.appendChild(appPIXI.view)
new AlloyTouch({
// ...
})
// 定义的动画
// 增加动画到对应的时间轴
// 其他
})
9.3 增加音乐播放暂停的切换
javascript
const toggleMusic = () => {
bgm.isPlaying ? bgm.stop() : bgm.play()
bgmIsPlaying.value = !bgmIsPlaying.value
}