Skip to content

使用 pixi.js 开发一镜到底 H5 (vue版)

总共的流程分为以下几步:

  1. 创建舞台
    1. 创建主容器(长图容器)
    2. 根据场景创建容器下的子容器(分屏容器)
  2. 整理图片素材,创建图片对象,加载图片素材,包括后续使用的序列帧图片
  3. 使用phy-touch监听滑动,并且根据滑动计算当前的位置,及滑动百分比
  4. 使用gsap中的timeline,创建总体时间轴、和长图时间轴
  5. 使用timeline.seek方法,将此时的滑动位置作为参数,实现滚动距离和动画播放时间的映射
  6. 在长图时间轴中,根据滑动距离添加不同的素材动画,包括:
    1. 向下滑动长图的向右移动
    2. 星星的出现
    3. 场景 2 的出现,场景 1 透明度及大小的变化
    4. 音符的移动
    5. 黑夜缩小为小窗,黑夜缩小期间 工作中的男孩出现
  7. 制作序列帧动画
    1. 根据滑动百分比,在位置之前保持第 1 张图,超过滑动距离则是最后一张图,当前位置图片序号=((当前滑动的距离 - 开始的距离/持续距离)*序列帧图片数量)
    2. 制作孩子走路序列帧
    3. 结尾漩涡序列帧
  8. 加载声音素材,包括背景音乐、星星声音、鼓掌声音
    1. 与序列帧制作逻辑相似,在 当前滑动的距离 到达预定的滑动位置时,先停止当前播放的声音,再播放预定的声音,为了避免一个声音的多次触发播放
    2. 制作星星出现声音
    3. 制作鼓掌声音
  9. 设置加载进度百分比,加载完成后才可以播放 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
}