一文带你入门前端动效开发,PIXI+GSAP仿写vanmoof刹车动效 | 猿创营

2,750 阅读6分钟

背景

本篇文章来源于大帅老猿摸金校尉群(一个教你接私活赚外快的群,感兴趣的可以联系大帅老猿)内的一次全员实战项目,是一个真实的外包项目,在交付给客户之后经过大帅的脱敏之后,给群内小伙伴练手学习的,对于和我一样从未学习过前端动效开发的小白来说,是一个不错的练手项目。

客户需求

简单来说,就是要完成类似 vanmoof 电商页面的刹车视觉动效(链接:www.vanmoof.com/en-NL/s3?co… ),效果如下图所示:

vanmoof.gif 这种效果要让没有写过动效的小伙伴来写,可能完全不知道从何下手,但是跟着下面的步骤做,你就会发现其实还好,也是蛮有意思的。下面上干货!

需求分析

组成元素

首先,上图中大体上由四个部分组成:

  1. 按钮
  2. 车架及车把
  3. 刹车
  4. 动线

动效

动效主要有三个:

  1. 按钮点击的波纹效果
  2. 刹车的按压效果
  3. 动线的效果及按下刹车时动线的回弹效果

交互

交互总共就一个:

  1. 点击按钮时的动效的变化

分析完需求,下面正式进入代码的开发。

开发环境配置

本次项目开发环境如下:

  • IDE:VS Code
  • VS Code 插件:微软出品的 Live Preview,方便开发过程中效果的预览
  • JS 库:PIXI、GSAP
  • 项目基础代码:github.com/ezshine/YCY…

上面的开发环境配置好后,从基础代码中fork一份出来,然后把代码取到本地,并用 VS Code 打开项目。 项目结构如下图所示:

image.png

结构很简单,主要包括要用到的图片资源,动效核心代码 brakebanner.js,以及 index.html (引入了 PIXI 及 GSAP),其他的文件不用关心,详细的代码可以去基础代码仓库中查看。

用 VS Code 打开项目之后,我们直接打开 brakebanner.js 文件,并在 index.html 文件上右键 Live Preview: Show Preview,使用 Live Preview 插件预览项目,此时,你看到的页面应该是空白的,像下面这样:

image.png

不要慌,此时项目正常运行的结果就是这样!

第一步,先创建一块画布

在我们正式画内容之前,我们要先创建一块画布,然后再在画布上画我们要的效果,下面是代码:

this.app = new PIXI.Application({
    width: window.innerWidth,
    height: window.innerHeight,
    backgroundColor: 0xffffff
});

document.querySelector(selector).appendChild(this.app.view);

代码运行结果如下:

image.png

蛤?怎么这样?怎么啥都没有?你TM在逗我?浪费时间啊同志!

不要急,因为此时画布的背景色是白色的,看不出代码的效果,我们先把背景色改成红色的,效果就出来了。

image.png

恭喜你!你已经成功通过 PIXI 的 API 在画面上创建了一块红色的画布!入门了,入门了!

下面,我们一个接一个地画前面需求分析中提到的几个元素,首先是按钮:

第二步,绘制按钮及第一个动效

// 通过 Loader 加载图片资源
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.load();

// 在 Loader 的成功回调里绘制按钮
this.loader.onComplete.add(() => {
        // 创建一个容器,有助于容器内资源整体的控制
        let actionButton = new PIXI.Container(); 
        let btnImage = new PIXI.Sprite(this.loader.resources['btn.png'].texture);
        let btnCircle = new PIXI.Sprite(this.loader.resources['btn_circle.png'].texture);

        // 容器里加入按钮图片及圆环
        actionButton.addChild(btnImage);
        actionButton.addChild(btnCircle);

        // 将容器添加到画布上,此时就能在画布上看到按钮了
        this.app.stage.addChild(actionButton);
});

效果如下:

image.png

此时,按钮已经出来了,下面我们调整按钮与圆环的位置,并为按钮加上动态效果:

// 调整按钮位置
actionButton.x = actionButton.y = 300;
btnImage.pivot.x = btnImage.pivot.y = btnImage.width / 2;// 通过 pivot 对象调整元素的圆心位置
btnCircle.pivot.x = btnCircle.pivot.y = btnCircle.width / 2;

// 添加按钮圆环动效
btnCircle.scale.x = btnCircle.scale.y = 0.8;
gsap.to(btnCircle.scale, { duration: 1, x: 1.3, y: 1.3, repeat: -1 });

效果如图:

圆环动效.gif

再对其进行微调:

let btnCircle2 = new PIXI.Sprite(this.loader.resources['btn_circle.png'].texture);
actionButton.addChild(btnCircle2);// 再加一个圆环
gsap.to(btnCircle, { duration: 1, alpha: 0, repeat: -1 });// 加点透明度的变化

最终效果如下:

圆环动效2.gif

怎么样,是不是感觉已经像模像样了呢?

此时,通过 GSAP,我们成功地给按钮的圆环添加了波纹的动效,第一个动效大功告成!

至此,本文我们要用到的 API 基本展示完毕,总结一下,就是以下几个 API:

// PIXI
PIXI.Application
pixiApplication.view
pixiApplication.stage // 根显示容器,也可以理解为画布
// 加载资源
PIXI.Loader
Loader.add
Loader.load
Loader.onComplete.add
Loader.resources
// 创建容器,组合元素
PIXI.Container
PIXI.Container.addChild
// 精灵,可以渲染图像
PIXI.Sprite
// Sprite、Container 都是 DisplayObject
DisplayObject.x
DisplayObject.y
DisplayObject.scale
DisplayObject.pivot

// GSAP
gsap.to

下面我们就围绕这上面用到的这些 API完成剩下的绘制工作!

第三步,绘制车架、车把及刹车

// 加载车架、车把及刹车图片资源
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');

// 创建自行车容器
let bikeContainer = new PIXI.Container();
this.app.stage.addChild(bikeContainer);
// 自行车图片太大,缩小一下图片
bikeContainer.scale.x = bikeContainer.scale.y = 0.3;

// 车架
let bikeImage = new PIXI.Sprite(this.loader.resources['brake_bike.png'].texture);
bikeContainer.addChild(bikeImage);

// 刹车
let bikeLeverImage = new PIXI.Sprite(this.loader.resources['brake_lever.png'].texture);
bikeContainer.addChild(bikeLeverImage);
bikeLeverImage.pivot.x = bikeLeverImage.pivot.y = 455;
bikeLeverImage.x = 722;
bikeLeverImage.y = 900;

// 车把:车把要盖在刹车上,所以车把手要在刹车后面添加,PIXI 是以元素添加的顺序定元素的显示层级的
let bikeHandlerbarImage = new PIXI.Sprite(this.loader.resources['brake_handlerbar.png'].texture);
bikeContainer.addChild(bikeHandlerbarImage);

// 将自行车始终保持在画布的右下角位置
let resize = () => {
        bikeContainer.x = window.innerWidth - bikeContainer.width;
        bikeContainer.y = window.innerHeight - bikeContainer.height;
};
window.addEventListener('resize', resize)
resize();

效果如图:

image.png

第四步,添加按钮交互事件

按钮与自行车都画好了,这个时候就该给按钮添加交互效果了:

// 启用按钮的交互事件
actionButton.interactive = true;
// 光标移动到按钮上时显示“小手”
actionButton.buttonMode = true;
actionButton.on('mousedown', () => {
    gsap.to(bikeLeverImage, { duration: .6, rotation: -30 * Math.PI / 180 });
})
actionButton.on('mouseup', () => {
    gsap.to(bikeLeverImage, { duration: .6, rotation: 0 });
})

效果如图:

按钮交互.gif

如图,通过 PIXI 元素的 mousedown、mouseup 事件以及 gsap 的 to 方法,我们实现了长按左键按下刹车,放开左键松开刹车的效果。

第五步,绘制动线效果

下面进行本次动效中最难的一步,完成动线的绘制:

let particleContainer = new PIXI.Container();
this.app.stage.addChild(particleContainer);
// 通过旋转容器实现动线向左下角移动的效果
particleContainer.rotation = 35 * Math.PI / 180;

// 旋转后,对位置进行适当的调整
particleContainer.pivot.x = window.innerWidth / 2;
particleContainer.pivot.y = window.innerHeight / 2;
particleContainer.x = window.innerWidth / 2;
particleContainer.y = window.innerHeight / 2;

// 随机生成 10 个圆点
let particles = [];
let colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x81881, 0x000000];
for (let i = 0; i < 10; i++) {
        let gr = new PIXI.Graphics();

        gr.beginFill(colors[Math.floor(Math.random() * colors.length)]);
        gr.drawCircle(0, 0, 6);
        gr.endFill();

        let pItem = {
                sx: Math.random() * window.innerWidth,
                sy: Math.random() * window.innerHeight,
                gr
        };

        gr.x = pItem.sx;
        gr.y = pItem.sy;

        particleContainer.addChild(gr);

        particles.push(pItem);

}

// 速度由 0 增加到 20
let speed = 0;
function loop() {
    speed += .5;
    speed = Math.min(speed, 20);

    for (let i = 0; i < particles.length; i++) {
        let pItem = particles[i];

        pItem.gr.y += speed;

        // 速度增加到 20 了才变成线,实现一个由静到动的过渡效果
        if (speed >= 20) {
                pItem.gr.scale.y = 40;
                pItem.gr.scale.x = 0.03;
        }

        // 圆点运动到底部了,再从回到顶部,实现一个循环的效果
        if (pItem.gr.y > window.innerHeight) {
                pItem.gr.y = 0;
        }
    }
}


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;

        // 通过 ease 实现回弹的效果
        gsap.to(pItem.gr, { duration: .6, x: pItem.sx, y: pItem.sy, ease: 'elastic.out' });
    }
}

start();

// 按住按钮,刹车的同时,动线停止运动,并有一个回弹的效果
actionButton.on('mousedown', () => {
    pause();
})
// 松开按钮,动线继续运动
actionButton.on('mouseup', () => {
    start();
})

效果如下图所示:

动线效果.gif

代码注释已给出,请参照注释及效果图理解动效的实现过程,此处用到了一个新的 api,gsap.ticker

至此,全部动效都已实现完毕。

总结

本文主要通过 PIXI 实现了图片的绘制、定位及旋转,还有圆点的绘制、变形,然后配合 GSAP 的 to 方法及 ticker 方法实现我们想要的动效,用到的 API 其实不多。

是不是感觉前端动效“也不过如此”?那么,还等什么,赶快也自己动手去实现一个吧!

更深入的学习,请参考 PIXIGSAP 的官网。

最后

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