【PIXI】超唯美!如何制作中秋场景级动画!!

4,084 阅读4分钟

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前言

月沉碧海望重楼,谁放明灯惹梦游。

——《七律·孔明灯》

介绍

本期我们开始使用pixi.js轻量级h5游戏引擎,去开发一个中秋夜空的场景级完整动画。这是临时创作的实战案例,希望大家喜欢,里面有灿烂的星空,有迷人的圆月,有桂花的飘香,还有那一盏盏寄托我们美好愿望的孔明灯。。

VID_20210907_134552.gif

正文

1.分析需求

  • 初始化引擎与图片的加载与纹理生成
  • 星空与月亮的静态渲染
  • 清风拂过桂花枝效果
  • 花瓣的生成与散落
  • 孔明灯近大远小,升起与交互

我们主要完成的就是这么多,我们先准备好四张图片吧,然后再把基础结构先打起来。

微信截图_20210907141902.png

2.基础结构

<body>
    <div id="app"></div>
    <script type="module" src="./app.js"></script>
</body>
* {
    padding: 0;
    margin: 0;
}
html,
body {
    width: 100%;
    height: 100vh;
    position: relative;
    overflow: hidden;
    background-color: #999;
}
#app {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

因为我们这次配合vite做的项目搭建所以要用module模式,后面方便引入模块。

import * as PIXI from "pixi.js";

const imgs = {
  "sweet": "assets/sweet.png",
  "lamp": "assets/lamp.png",
  "moon": "assets/moon.png",
  "petal": "assets/petal.png"
}

class Application {
  constructor() {
    this.app = null;
    this.stage = null;
    this.textures = null;
    this.w = 800;
    this.h = 600;
    this.dt = 0;
    this.init();
  }
  init() {
    // 初始化
    let el = document.getElementById("app")
    this.app = new PIXI.Application({
      resizeTo: el
    });
    this.w = this.app.view.width;
    this.h = this.app.view.height;
    this.stage = this.app.stage;
    this.stage.sortableChildren = true;
    el.appendChild(this.app.view);
    this.loadTexture(imgs).then(data => {
      this.textures = data;
      window.addEventListener("resize", this.reset.bind(this))
      this.render();
    })
    this.app.ticker.add(this.step.bind(this));
  }
  reset() {
      // 重置
      this.w = this.app.view.width;
      this.h = this.app.view.height;
      this.stage.children.length = 0;
      this.render()
  }
  loadTexture(imgs) {// 加载图片纹理}
  render() {
    this._renderSky();
    this._renderMoon();
    this._renderLamp();
    this._renderSweet();
    this._renderPetal();
  }
  _renderLamp() {// 渲染孔明灯}
  _renderMoon() {// 渲染圆月}
  _renderPetal() {// 渲染花瓣}
  _renderSweet() {// 渲染桂花枝}
  _renderSky() {// 渲染星空}
  step() {
    // 帧
    this.dt++;
  }
}

window.onload = new Application();

这里我们做的是一个全屏动画,所以PIXI直接调整的父容器的全部区域得到宽高给后面的元素使用。并且,我们期望的是加载纹理之后存储纹理集然后才开始渲染元素。接下来就着手写一个图片加载生成纹理的loadTexture函数吧。

3.资源加载

loadTexture(imgs) {
    const textures = {};
    return new Promise((reslove, reject) => {
        let loader = this.app.loader;
        for (const key in imgs) {
            loader.add(key, imgs[key])
        }
        loader.load((info, resources) => {
            for (const key in resources) {
                let texture = PIXI.Texture.from(resources[key].data);
                textures[key] = texture;
            }
            reslove(textures)
        });
    })
}

我们这里用到了pixi.js自带的一个加载器,将这些图片加载出来,加载完毕后我们再给他逐个转成纹理存储到对象里面后面方便其生成精灵。

4.圆月星空

// 圆月
_renderMoon() {
    const moon = PIXI.Sprite.from(this.textures["moon"]);
    moon.scale.set(this.h / moon.height * 0.7, this.h / moon.height * 0.7);
    moon.position.set(this.w - moon.width * 0.7, -moon.height * 0.3)
    this.stage.addChild(moon);
}
// 星空
_renderSky() {
    const sky = new PIXI.Container();
    let color = this.createGradTexture();
    for (let i = 0; i < this.w / 20; i++) {
        const star = new PIXI.Sprite(color);
        let scale = Math.random();
        star.scale.set(scale, scale);
        star.position.set(this.w * Math.random(), this.h * 0.7 * Math.random());
        sky.addChild(star)
    }
    this.stage.addChild(sky)
}
createGradTexture() {
    let canvas = document.createElement('canvas');
    canvas.width = 8;
    canvas.height = 8;
    let context = canvas.getContext('2d');
    let gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
    gradient.addColorStop(0, 'rgba(255,255,255,1)');
    gradient.addColorStop(0.3, 'rgba(255,255,255,1)');
    gradient.addColorStop(0.45, 'rgba(0,0,128,1)');
    gradient.addColorStop(1, 'rgba(0,0,0,1)');
    context.fillStyle = gradient;
    context.fillRect(0, 0, canvas.width, canvas.height);
    return PIXI.Texture.from(canvas);
}

月亮的绘制不做赘述,就是简单的生成精灵摆放位置。

星空我们先要做个容器Container来收集星星,然后把这个容器添加到场景中。星星的数量跟屏幕大小正相关,for循环逐个生成精灵,但我们的精灵纹理从何而来呢,我们期望星星是白色的心,周围发点蓝光的,此时,我们可以再引入一张图片生成纹理,或者,跟我一样,在createGradTexture函数用canvas api的圆形渐变绘制一个8*8的矩形然后生成纹理。

至于,星星的位置我们随机在屏幕中生成,这里我想只随机到高度的70%全满的话稍有些违和。

微信截图_20210907144746.png

5.桂枝拂动

_renderSweet(num = 10) {
    const sweet = this.textures["sweet"];
    const points = []
    const ropeLength = sweet.width / num;
    for (let i = 0; i < num; i++) {
        points.push(new PIXI.Point(i * ropeLength, 0));
    }
    const strip = new PIXI.SimpleRope(sweet, points);
    const stripContainer = new PIXI.Container();
    stripContainer.addChild(strip);
    stripContainer.position.set(10, -20);
    stripContainer.scale.set(.5, .5);
    stripContainer.rotation = Math.PI / 180 * 40;
    stripContainer.zIndex = 10;
    this.stage.addChild(stripContainer);
    this.app.ticker.add(() => {
        for (let i = 0; i < points.length; i++) {
            points[i].y = Math.sin((i * 0.5) + this.dt * 0.1) * 2;
            points[i].x = i * ropeLength + Math.cos((i * 0.3) + this.dt * 0.1) * 1;
        }
    })
  }

我们这里不做帧动画或者骨骼动画了,就用一张静态图来完成,主要用到了pixi.js的SimpleRope,将桂花枝变成一条绳子,其实就做了点的切片联动,再在渲染更新时改变每个点的位置,就可实现清风浮动的效果。

VID_20210907_150040.gif

6.花瓣飘落

_renderPetal(num = 20) {
    let petals = [...Array(num).values()];
    petals = [].map.call(petals, petal => {
        petal = PIXI.Sprite.from(this.textures["petal"]);
        let scale = .1 + .2 * Math.random();
        petal.scale.set(scale, scale);
        petal.rotation = Math.PI / 180 * 360 * Math.random()
        petal.position.set(this.w * Math.random(), this.h * Math.random());
        petal.vy = Math.random() * 0.2 + 0.5;
        petal.vx = Math.random() * 0.5 + 0.5;
        petal.zIndex = 15 * Math.random();
        this.stage.addChild(petal);
        return petal;
    })
    this.app.ticker.add(() => {
        petals.forEach(petal => {
            petal.rotation += (0.025 * Math.random())
            petal.alpha -= .0006;
            petal.y += petal.vy;
            petal.x += petal.vx;
            if (petal.x > this.w + petal.width || petal.y > this.h + petal.height) {
                petal.x = -this.w / 4 * Math.random()
                petal.y = this.h * Math.random() - this.h / 2
                petal.zIndex = 15 * Math.random();
                petal.alpha = 1;
            }
        })
    })
}

这里我们先生成20个花瓣精灵控制好位移方向大小等参数,在渲染更新时会改变这些参数来实现运动的。但是,我们更新时要注意,做好边界判断,一旦超过边界后,让他再重新赋值,不断的重复利用。

7.天灯飞扬

_renderLamp(num = 12) {
    let lamps = [...Array(num).values()];
    lamps = [].map.call(lamps, lamp => {
        lamp = PIXI.Sprite.from(this.textures["lamp"]);
        let scale = .15 + .35 * Math.random();
        lamp.scale.set(scale, scale);
        lamp.position.set(this.w * Math.random(), this.h * Math.random());
        lamp.vy = Math.random() * 0.36 + 0.2;
        lamp.vx = 0.12 * Math.random();
        lamp.zIndex = scale;
        lamp.interactive = true;
        lamp.on('pointerdown', this._pointerDown.bind(this, lamp));
        lamp.on('pointerover', this._pointerOver.bind(this, lamp));
        lamp.on('pointerout', this._pointerOut.bind(this, lamp));
        const blurFilter = new PIXI.filters.BlurFilter();
        lamp.filters = [blurFilter];
        blurFilter.blur = (0.5 - scale) * 10;
        this.stage.addChild(lamp);
        return lamp;
    });

    this.app.ticker.add(() => {
        lamps.forEach(lamp => {
            lamp.y -= lamp.vy;
            lamp.x += Math.sin(this.dt * lamp.vx) * 0.2;
            if (lamp.y < -lamp.height) {
                lamp.y = this.h;
                lamp.w = this.w * Math.random();
                lamp.vy = Math.random() * 0.36 + 0.2;
                lamp.vx = 0.12 * Math.random();
            }
        })
    })
}
_pointerDown(lamp) {
    lamp.vy += .8;
}
_pointerOver(lamp) {
    lamp.filters[0].blur = 0;
}
_pointerOut(lamp) {
    lamp.filters[0].blur = (0.5 - lamp.scale.x) * 10;
}

这里与花瓣飘落的实现形式类似,但是方向一直是朝上的,因为孔明灯放飞后左右并不能保证一直不动所以用三角函数sin去模拟他的横向周期变化。值得说的还有近大远小的,我们把它的大小随机生成,再用zIndex根据大小设置优先级,肯定是大的在前,小的在后,然后在利用pixi.js内置的过滤器做模糊处理,这样远处的孔明灯就会变模糊,更加真实。

接下来,介绍pixi.js三个事件:

  • pointerdown:当在显示对象上按下指针设备按钮时触发事件。

  • pointerover:当指针设备移动到显示对象上时触发事件。

  • pointerout:当指针设备移出显示对象时触发事件。

要接收事件我们要把孔明灯的interactive属性开启。

点击后会孔明灯加速,我们就改变一下vy值,移入移出模糊消散也很简单。


虽然有一些工作量,但难度其实不是那么高,讲到这里其实已经结束了,在线演示

拓展

我们本期只是用到了pixi.js的皮毛就可以构建出这样的画面,不知道你看完有何种联想。是做一个场景动画编辑器?还是想做绳索游戏?或是粒子特效?我们学习的本质目的就是要有探索,批判,创新思想,所以在这个世界尽情的探索吧!


创作不易,你们的支持就是我创作的动力,请各位看官老爷们多多点赞评论收藏,一键三连哟~