使用PIXI+GSAP来模仿vanmoof电商页面的刹车视觉动效 | 猿创营

682 阅读5分钟

动画2.gif

写作初衷

  1. 参与猿创营的活动
  2. 踏入前端行业之前,一直被前端的酷炫效果,动画所吸引,奈何工作中无接触类似的工作,不知道如何学,加上懒惰,想法便只停留在脑海中
  3. 尝试开始输出内容,证明存在过的痕迹

资料

PIXI:是一个非常快的2D sprite渲染引擎。这是什么意思?这意味着它可以帮助你显示、动画和管理交互式图形,这样你就可以轻松地使用JavaScript和其他HTML5技术制作游戏和应用程序。它有一个合理的,整洁的API,并包括许多有用的功能,如支持纹理地图集和提供一个精简的系统,为动画精灵(交互式图像)。它还为您提供了一个完整的场景图,这样您就可以创建嵌套精灵(精灵中的精灵)的层次结构,并允许您将鼠标和触摸事件直接附加到精灵上。而且,最重要的是,Pixi可以让您自由的使用,使其适应您的个人编码风格,并与其它框架无缝集成。

GSAP:(GreenSock Animation Platform)是一个从flash时代一直发展到今天的web专业动画库。

实现步骤

1、html结构及brakeBanner类实现

// 存放的容器
<div id="brakebanner"></div>
<script>
    window.onload = init;
    function init() {
        let banner = new BrakeBanner("#brakebanner");
    }
    class BrakeBanner{}
</script>

2、创建PIXI应用PIXI.Application【画布】& 导入资源 PIXI.Loader

想要在画布显示的东西,都必须被加进app.stage中,app.stage: 舞台,是PIXI的容器对象,用来包裹所有精灵的主要容器

class BrakeBanner{
	constructor(selector){
		// 1、创建PIXI应用【画布】
		this.app = new PIXI.Application({
			width: window.innerWidth,
			height: window.innerHeight,
			backgroundColor: 0x3b3d3c, // 灰色
			resizeTo: window // 根据window窗口进行缩放
		})
		document.querySelector(selector).appendChild(this.app.view)
                // 想在画布显示东西,必须被加进stage中, stage也叫舞台,是PIXI的容器对象,也是所有可见对象的根容器
		this.stage = this.app.stage 
		// 2、加载资源
		this.loader = new PIXI.Loader()
		// 自定义的key: btn.png, 资源路径:images/btn.png
		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()
		})
	} 

图中有公路,自行车,按钮,线条,由于从上往下显示顺序,所以最先加载的图片,在最底下,故创建顺序为:公路 > 自行车 > 按钮

3、创建公路:创建容器和精灵,并添加进app.stage, 并对容器进行35°旋转

    // 公路
    createHighway () {
        // 创建公路容器
        const highwayContainer = new PIXI.Container()
        // 轴中心【原点】设置为屏幕的中心,默认为左上角0,0
        highwayContainer.pivot.x = window.innerWidth/2
        highwayContainer.pivot.y = window.innerHeight/2
        // 位置
        highwayContainer.x = window.innerWidth/2
        highwayContainer.y = window.innerHeight/2
        // 想在画布显示,必须添加进stage
        this.stage.addChild(highwayContainer) 
        // 35°旋转
        highwayContainer.rotation = 35 * Math.PI / 180 
        // 使用loader.resources,从已加载的资源中创建精灵
        const highway = new PIXI.Sprite(this.loader.resources['highway.png'].texture)
        // 图片添加进公路容器
        highwayContainer.addChild(highway)
     }

同理创建自行车,并给自行车着色

    // 自行车
    createBike () {
        const bikeContainer = new PIXI.Container()
        this.stage.addChild(bikeContainer)

        // 调整bikeLeverImg 和 bikeHandlerbarImg精灵创建的顺序, 从上往下执行
        // 从已有资源创建精灵,'brake_bike.png'是key值
        const bikeImg = new PIXI.Sprite(this.loader.resources['brake_bike.png'].texture)
        const bikeLeverImg = new PIXI.Sprite(this.loader.resources['bikeLeverImg.png'].texture)
        const bikeHandlerbarImg = new PIXI.Sprite(this.loader.resources['bikeHandlerbarImg.png'].texture)
        
        bikeContainer.addChild(bikeImg)
        bikeContainer.addChild(bikeLeverImg)
        bikeContainer.addChild(bikeHandlerbarImg)
        
        // 把手中心点
        bikeLeverImg.pivot.x = 455
        bikeLeverImg.pivot.y = 455
        // 把手位置
        bikeLeverImg.x = 722
        bikeLeverImg.y = 900

        // 把手按下的效果:逆时针30°
        // bikeLeverImg.rotation = Math.PI/180*-30

        //调色器, 自行车填颜色
        bikeLeverImg.tint = 0x9AC0CD
        bikeImg.tint = 0x9AC0CD
        bikeHandlerbarImg.tint = 0x9AC0CD

        return {
                bikeContainer,
                bikeLeverImg
        }
     }

同理创建按钮,使用gsap进行动画渲染

    // 按钮
    creatActionButton () {
            // 创建容器,用于按钮和圈的偏移
            let actionButton = new PIXI.Container()
            this.stage.addChild(actionButton)

            // initSprite是封装的创建精灵
            let btnImg = this.initSprite('btn.png', actionButton)
            let btnCircle = this.initSprite('btn_circle.png', actionButton)
            let btnCircle2 = this.initSprite('btn_circle.png', actionButton)

            //  3、改变圆心, 原点为中心,而不是左上角[正方形]
            btnImg.pivot.x = btnImg.pivot.y = btnImg.width/2
            btnCircle.pivot.x = btnCircle.pivot.y = btnCircle.width/2
            btnCircle2.pivot.x = btnCircle2.pivot.y = btnCircle2.width/2

            // actionButton.x = actionButton.y = 400 // 父容器移动到400的位置

            // 4、添加动画 gsap.to从当前位置变换为其他位置, gasp.from从其他位置变换为当前位置
            // 缩放:从小到大 duration:1为1s, repeat: -1循环播放 alpha: 透明度
            btnCircle.scale.x = btnCircle.scale.y = 0.8
            // 设置按钮圆圈缩放
            gsap.to(btnCircle.scale, {duration: 1, x: 1.3, y: 1.3, repeat: -1})
            // 设置透明度和重复播放
            gsap.to(btnCircle, {duration: 1, alpha: 0, repeat: -1})
            return actionButton
    }
    
    // 初始化精灵图
    initSprite(key, stage = this.stage) {
            let source = new PIXI.Sprite(this.loader.resources[key].texture)
            stage.addChild(source)
            return source
    }

创建线条,让线条,马路,自行车动起来

	show() {
		let { highwayContainer, highway } = this.createHighway() // 公路
		let { bikeContainer, bikeLeverImg } = this.createBike() // 自行车
		bikeContainer.scale.x = bikeContainer.scale.y = 0.3 // 自行车缩小,图片太大了

		let actionButton = this.creatActionButton()
		actionButton.scale.x = actionButton.scale.y = 0.45
		actionButton.x = 430 
		actionButton.y = 400
		actionButton.buttonMode = true // 移动上去箭头变为手
		actionButton.interactive = true // 可以交互

		// 公路位置
		highway.x = window.innerWidth - bikeContainer.width - 1130
		highway.y = window.innerHeight - bikeContainer.height - 1130
                // 按下效果
		actionButton.on('mousedown', () => { 
                    // 把手被按下的效果:逆时针30°
                    // bikeLeverImg.rotation = Math.PI/180*-30
                    gsap.to(bikeLeverImg, {duration: 0.6, rotation: Math.PI/180*-30})

                    // 粒子暂停
                    pause()
		})
		actionButton.on('mouseup', () => { // 松开效果 pointerdown
                    // bikeLeverImg.rotation = 0
                    gsap.to(bikeLeverImg, {duration: 0.6, rotation: 0})
                    // 粒子启动
                    start()
		})

		let resize = () => {
                    // 自行车位置
                    bikeContainer.x = window.innerWidth - bikeContainer.width
                    bikeContainer.y = window.innerHeight - bikeContainer.height

                    // 公路位置
                    highway.x = window.innerWidth - bikeContainer.width - 600
                    highway.y = window.innerHeight - bikeContainer.height - 600

                    // 按钮位置
                    actionButton.x = window.innerWidth - bikeContainer.x + 420
                    actionButton.y = window.innerHeight - bikeContainer.y - 160
		}
		window.addEventListener('resize', resize)
		resize()

		// 5、创建粒子
		let particleContainer = new PIXI.Container()
		this.stage.addChild(particleContainer)
		// 轴心为中心点
		particleContainer.pivot.x = window.innerWidth/2
		particleContainer.pivot.y = window.innerHeight/2

		particleContainer.x = window.innerWidth/2
		particleContainer.y = window.innerHeight/2
		// 粒子向35°旋转
		particleContainer.rotation = 35*Math.PI/180
		// 粒子有多个颜色
		let particles = []
		const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0xFF8C00]
		for(let i = 0; i<10; i++) { // 给10个粒子,随机给颜色和位置随机分布
                    let gr = new PIXI.Graphics()
                    gr.beginFill(colors[Math.floor(Math.random()*colors.length)])
                    // 画圆,半径为6
                    gr.drawCircle(0,0,4)
                    gr.endFill()

                    let pItem = {
                            sx:  Math.random() * window.innerWidth,
                            sy: Math.random() * window.innerHeight,
                            gr: gr
                    }
                    gr.x = pItem.sx
                    gr.y = pItem.sy
                    particleContainer.addChild(gr)
                    particles.push(pItem)
		}
		// 向某一个角度持续移动: 让容器一直往y轴向下移动, 容器旋转
		// 超出边界后回到顶部继续移动
		let speed = 0
		function loop() {
			speed += .3
			speed = Math.min(speed, 20) // 最大速度为20
                        // 计算粒子
			for(let i = 0; i<particles.length; i++) {
				let pItem = particles[i]
				pItem.gr.y += speed
				// pItem.gr.x = 30
				if (speed >= 20) { // 圆点变成线, 由慢到快
					pItem.gr.scale.y = 30
					pItem.gr.scale.x = 0.03

				}
				if (pItem.gr.y > window.innerHeight) pItem.gr.y = 0 // 超出时回到原点
			}
                        // 计算公路的x, y值
			highwayContainer.y +=  Math.cos(35* Math.PI/180) * speed
			highwayContainer.x -=  Math.sin(35* Math.PI/180) * speed
			// 公路超出重置
			if (highwayContainer.y > 400) {
				highwayContainer.y = -100
				highwayContainer.x = 800
			}
		}

		function start() {
			speed = 0
			gsap.ticker.add(loop)
		}
		
		function pause() {
			gsap.ticker.remove(loop)
			for(let i = 0; i<particles.length; i++) {
				let pItem = particles[i]
				pItem.gr.scale.y = 1
				pItem.gr.scale.x = 1
				// 暂停回弹效果
				gsap.to(pItem.gr, {duration: .6, x:pItem.sx, y:pItem.sy, ease: 'elastic.out'})
			}
		}

		start()
		// 按住鼠标停止
		// 停止的时候还有一点回弹的效果
		// 松开鼠标继续
	}

最后

我的github: github.com/dpgirl/YCY-… ,以后会尽量输出内容,写的一般请见谅!

公众号里搜 大帅老猿,在他这里可以学到很多东西。

马路的资源和想法,是参考:juejin.cn/post/711902… 自行车轮子动起来还没实现,但可以参考:juejin.cn/post/712527…