用PixiJS和GSAP仿写vanmoof动效的入门笔记|猿创营

老规矩,先放码上掘金源码展示:

觉得看着不习惯的,也可以直接在Github仓库查看,欢迎吐槽~

前言

在大帅老师的直播讲解中,第一次了解了PixiJS的实操。跟写完刹车动效,微微膨胀地写起来了变速器动效,然后就卡壳了。。

还是回过头来先趁着残留的一丝记忆,整理下刹车动效的入门笔记吧。

实现

创建应用和舞台

定义一个BrakeBanner类,在构造函数中创建一个适应屏幕宽高的PIXI应用。为了看起来方便,我们先给一个显眼的背景色。

class BrakeBanner {
  constructor(selector){
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      backgroundColor: 0x1d9c9c,
      antialias: true,
      resolution: 1,
      resizeTo: window
    })
​
    document.querySelector(selector).appendChild(this.app.view)
    this.stage = this.app.stage
​
  }
}

html文件中定义一个承接应用的容器brakebanner

<div id="brakebanner"></div>

html文件中引入定义类的js文件,并且在页面加载时,new一下类,挂载应用实例并执行。

window.onload = init;
function init() {
  let banner = new BrakeBanner("#brakebanner");
}

brake01.png

创建一个辅助文本函数

接下来,我们来创建一个辅助文本函数,并把它添加到舞台上。没什么鸟用,纯粹是因为光秃秃的背景看起来太单调啦。

class BrakeBanner {
  constructor(selector) {
    // ...
    this.showText = this.createText('PixiJS and GSAP 入门笔记')
    this.stage.addChild(this.showText)
  }
  createText(text='你好~') {
    const textContainer = new PIXI.Container()
    const showText = new PIXI.Text(text, {
      width: 80,
      fontSize: 48,
      fontFamily: 'serif',
      fill: 0xffffff
    })
​
    showText.x = 100
    showText.y = 200
​
    textContainer.addChild(showText)
​
    return textContainer
  }
}

Loader加载需要使用的静态资源

我们在构造函数中加载需要使用的静态资源,并且在加载完成后的onComplete函数中执行对应的处理事件。

class BrakeBanner {
  constructor(selector) {
    // ...
    this.loader = new PIXI.Loader();
    this.loader.add('btn.png', './images/btn.png')
    this.loader.add('btn_circle.png', './images/btn_circle.png')
    this.loader.add('brake_bike.png', './images/brake_bike.png')
    this.loader.add('brake_handlerbar.png', './images/brake_handlerbar.png')
    this.loader.add('brake_lever.png', './images/brake_lever.png')
    this.loader.load()
    this.loader.onComplete.add(() => {
      this.show()
    })
  }
  // ...
  // 资源加载完成后执行的处理函数
  show() {
    
  }
}

改造辅助文本函数

为了文本展示方便,就把文本函数createText改造一下,可以支持传入文本样式和坐标位置等基本参数。

createText(text='你好~', textStyle = {}, props = {}) {
  const textContainer = new PIXI.Container()
  const finalStyle = {
    width: 80,
    fontSize: 48,
    fontFamily: 'serif',
    fill: 0xffffff,
    x: 200,
    ...textStyle
  }
  const finalProps  = {
    x: 100,
    y: 200,
    ...props
  }
  const showText = new PIXI.Text(text, finalStyle)
​
  Object.keys(finalProps).map(key => {
    showText[key] = finalProps[key]
  })
​
  textContainer.addChild(showText)
  return textContainer
}
​
show() {
  const demoText = this.createText('loader加载完成', {fontSize: 36}, {y: 300})
  this.stage.addChild(demoText)
}

brake02.png

辅助函数:创建精灵

从上面通过loader加载的图片可以看出来,我们需要重复加载图片、创建精灵的过程,那就简单封装一下。

resource (name) {
  return new PIXI.Sprite(this.loader.resources[name].texture)
}

添加精灵到舞台

把舞台背景色恢复成白色,开始添加精灵元素。

创建刹车容器,把容器添加到舞台上。然后把车架、车闸、车把依次添加到容器中,并调整车闸的坐标点使它能够和车把贴合在一起,调整车闸的原点以便于模拟捏住/松开车闸的动效。

缩放刹车容器,使之能完整展示在舞台上。

show() {
  const brakeContainer = new PIXI.Container()
  this.stage.addChild(brakeContainer)
​
  const brakeBike = this.resource('brake_bike.png')
  brakeContainer.addChild(brakeBike)
​
  const brakeLever = this.resource('brake_lever.png')
  brakeLever.pivot.x = brakeLever.width
  brakeLever.pivot.y = brakeLever.height
  brakeLever.x = 780
  brakeLever.y = 950
  brakeContainer.addChild(brakeLever)
​
  const brakeHandlerbar = this.resource('brake_handlerbar.png')
  brakeContainer.addChild(brakeHandlerbar)
​
  brakeContainer.scale.x = brakeContainer.scale.y = 0.3
}
​

调整容器位置,适配屏幕

我们通过resize事件来监听屏幕宽高,调整刹车容器的位置一直处于屏幕的右下角。

show() {
  //...
  function resize () {
    brakeContainer.x = window.innerWidth - brakeContainer.width
    brakeContainer.y = window.innerHeight - brakeContainer.height
  }
  window.addEventListener('resize', resize);
  resize()
}

创建并添加按钮

在类中添加一个创建按钮元素的createButton函数,调整按钮的原点,并且通过GSAP来创建按钮的不透明度、缩放过渡动画。

class BrakeBanner {
  //...
  createButton () {
    const actionButton = new PIXI.Container();
    const btnImage = this.resource('btn.png')
    const btnCircleImage = this.resource('btn_circle.png')
    const btnCircleImage2 = this.resource('btn_circle.png')
​
    btnImage.pivot.x = btnImage.pivot.y = btnImage.width / 2
    btnCircleImage.pivot.x = btnCircleImage.pivot.y = btnCircleImage.width / 2
    btnCircleImage2.pivot.x = btnCircleImage2.pivot.y = btnCircleImage2.width / 2
    btnCircleImage.scale.x = btnCircleImage.scale.y = 0.8
    actionButton.addChild(btnImage)
    actionButton.addChild(btnCircleImage)
    actionButton.addChild(btnCircleImage2)
​
    gsap.to(btnCircleImage.scale, { duration: 1, x: 1.2, y: 1.2, repeat: -1 })
    gsap.to(btnCircleImage, { duration: 1, alpha: 0, repeat: -1 })
    actionButton.scale.x = actionButton.scale.y = 0.6
​
​
    this.stage.addChild(actionButton)
    return actionButton
  }
  //...
}

show方法中调用生成一个按钮元素,并且在resize函数中,通过刹车容器的坐标位置来定位按钮元素的位置。

show() {
  //...
  const actionButton = this.createButton()
  function resize () {
    //...
    actionButton.x = window.innerWidth - brakeContainer.width + 142
    actionButton.y = window.innerHeight - brakeContainer.height + 220
  }
  //...
}

brake03_900.gif

按钮的鼠标监听事件

对按钮元素的鼠标按下和松开事件进行监听,进行对应的函数处理。

当监听到按下事件时,使用GSAP过渡动画让车闸角度旋转,达到捏紧车闸的效果。

当监听到松开事件时,使车闸旋转角度归零,达到松开车闸的效果。

show() {
  //...
  actionButton.interactive = true
  actionButton.buttonMode = true
​
  actionButton.on('mousedown', function () {
    gsap.to(brakeLever, { duration: .6, rotation: -35 * Math.PI / 180 })
  })
​
  actionButton.on('mouseup', function () {
    gsap.to(brakeLever, { duration: .6, rotation: 0 })
  })
}

鼠标行为控制按钮状态

当鼠标按下/松开时,监听一个对应的状态暂停/开始事件,来控制按钮等元素的展示状态,顺便把车闸动画控制也放进去。

show() {
  //...
  function start () {
    gsap.to(brakeLever, { duration: .6, rotation: 0 })
    gsap.to(actionButton, {duration: 0.6, alpha: 1})
    gsap.to(actionButton.scale, {duration: 0.6, x: 0.6, y: 0.6})
  }
  function pause () {
    gsap.to(brakeLever, { duration: .6, rotation: -35 * Math.PI / 180 })
    gsap.to(actionButton, {duration: 0.6, alpha: 0})
    gsap.to(actionButton.scale, {duration: 0.6, x: 0.2, y: 0.2})
  }
  //...
}

brake04_900.gif

创建随机粒子

创建一个粒子容器,并把容器添加到舞台上。

定义一组色值,通过Graphics创建圆形形状并通过随机选取色值,循环创建10个粒子,并插入到粒子容器。

为了保证所有粒子是倾斜运动的,我们把粒子容器直接旋转一定的角度。

show() {
  //...
  const particleContainer = new PIXI.Container()
  const particles = []
  this.stage.addChild(particleContainer)
​
  const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000]
​
  for (let i = 0; i < 10; i++) {
    const gr = new PIXI.Graphics()
    gr.beginFill(colors[Math.floor(Math.random() * colors.length)])
    gr.drawCircle(0, 0, 6)
    gr.endFill()
​
    const pItem = {
      sx: Math.random() * window.innerWidth,
      sy: Math.random() * window.innerHeight,
      gr
    }
​
    gr.x = pItem.sx
    gr.y = pItem.sy
​
    particles.push(pItem)
    particleContainer.addChild(gr)
    particleContainer.rotation = Math.PI / 180 * 35
  }
  //...
}

brake05.png

让粒子动起来

粒子怎么动?我们要反着想一下,粒子运动时发生位移,坐标发生变化,也就是说我们直接改变粒子的坐标,在视觉上就是粒子动起来了。

而且为了让粒子有一个加速的过程,我们设置一个不断自增的speed来改变粒子的y坐标。当速度达到一定程度时改变粒子x轴和y轴的缩放比,使粒子呈现出速度线的效果,当然为了科学,我们要让speed的增大有一个上限。

let speed = 2
function loop () {
  for (let i = 0; i < particles.length; i++) {
    const partItem = particles[i]
    let gr = partItem.gr
    speed++
    speed = Math.min(speed, 20)
    gr.y += speed
    if (gr.y >= window.innerHeight) {
      gr.y = 0
    }
    if (speed === 20) {
      gr.scale.x = 0.03
      gr.scale.y = 40
    }
  }
}

但是,我们直接调用loop函数是肯定看不到想要的效果的。需要在鼠标按下时,让GSAP动画添加loop函数的侦听器;在鼠标松开时,同步移除loop函数的侦听。

show() {
  const conY = brakeContainer.y
​
  function start () {
    //...
    speed = 0
    gsap.ticker.add(loop)
  }
​
  function pause () {
    //...
    gsap.ticker.remove(loop)
    for (let i = 0; i < particles.length; i++) {
      const partItem = particles[i]
      let gr = partItem.gr
      gsap.to(gr, { duration: 0.3, x: partItem.sx, y: partItem.sy, ease: 'elastic.out' })
      gsap.to(gr.scale, { duration: 0.3, x: 1, y: 1, ease: 'elastic.out' })
    }
  }
​
  start()
}

这时候,我们可以看到效果已经基本完善了。

brake06_900.gif

细节处理

对比vanmoof的动效,我们还能看到在运动时,(因为速度)车架是半透明状态,在刹车时,(突然减速)车架变得清晰,自行车容器略微抬起。

我们定义一个变量来记录自行车容器的初始Y轴坐标位置,便于运行时复位和刹车时抬起的校准。

show() {
  //...
  const conY = brakeContainer.y
  function start() {
    //...
    gsap.to(brakeBike, { duration: 0.3, alpha: 0.6 })
    gsap.to(brakeContainer, { duration: 0.3, y: conY })
  }
  function pause() {
    //...
    gsap.to(brakeBike, { duration: 0.3, alpha: 1 })
    gsap.to(brakeContainer, { duration: 0.3, y: conY + 100 })
  }
  //...
}

brake07_900.gif

写在最后

这个时候来看,是不是稍微好一点点啦?我们参照动效进行仿写,写完记个笔记就更便于我们入门PixiJS和GSAP。当然,其实还有一些细节需要去完善,比如按钮通过形状Graphics和文本Text来实现。

吃水不忘挖井人,感谢大帅老师的直播讲解。在公众号里搜 大帅老猿,在他这里可以学到很多东西。

猜你喜欢

转载自juejin.im/post/7125272238651080735