Canvas粒子模拟的N种玩法(一)

1,172 阅读4分钟

一、导读


相信很多小伙伴,刚开始接触web前端(这里缩小下范围,前端的概念很泛)的时候,从事web前端工程师的浪漫与激动都是对于网页上面令人感到惊喜和惊讶的特效动画。内心有一种执念的朋友或许已经产生了一种想法,有朝一日我也能写出如此牛逼的特效岂不是美滋滋。下面我就对我之前关于粒子的玩法分享给大家。

1.1准备工作

首先我想确认的一件事情是,各位同学对于canvas 2D 绘图的API是否大概的了解,以及自己动手实践过。关于前置的canvas的知识可以参考 Canvas API 详解

1.2基于对象模型的映射

首先我们设计一个粒子对象,里面包含了粒子的基本信息


    class Particle {
        constructor(x, y) {
            this.id = Date.now()
            this.size = 10
            // x坐标
            this.x = x
            // y坐标
            this.y = y
        }
    }

const singleParticle = new Particle(50,50)

一般的方法我们是基于一个对象一个粒子,然后在requestAnimationFrame函数里面触发更新逻辑。

为了管理方便我们要实现一个粒子管理的类,用于更新粒子的状态

 class ParticeManager {
        constructor() {
            this.particles = []
            this.particlesMap = {}
        }
        init(num) {
            let i = num
            while (i > 0) {
                const x = Math.floor(Math.random() * 500 + 1);
                const y = Math.floor(Math.random() * 500 + 1);
                this._add(new Particle(x, y))
                i--
            }
        }

        _add(particle) {
            this.particles.push(particle)
            this.particlesMap[particle.id] = this.particles.length - 1
        }

        _remove(id) {
            this.particles.splice(this.particlesMap[particle.id], 1)
            delete this.particlesMap[id]
        }

        update(callback) {
            this.particles.forEach(p => {
                callback(p)
            })
        }

        get() {
            return this.particles
        }
    }

接下来我们初始化一个canvas

    const canvas = document.getElementById('demo')
    const ctx = canvas.getContext('2d')

然后实例化管理类

    const particles = new ParticeManager()
    // 100个随机坐标的粒子
    particles.init(100)

后面我们定义tick函数用于更新画板

    function tick() {
        // 清理画布
        ctx.clearRect(0, 0, 500, 500)

        particles.get().forEach(point => {
            ctx.beginPath()
            ctx.arc(point.x, point.y, 3, 0, 2 * Math.PI)
            ctx.closePath()
            ctx.fillStyle = '#aaa'
            ctx.fill()
        })

        requestAnimationFrame(tick)
    }

    tick()

效果如图

image.png

1.2.1 粒子生成内存优化方案

以上代码还是未经过优化的代码,内存占用还是比较高。聪明的你们一定想到用TypeArray + 索引函数去实现粒子的内存空间的分配(后期会出一起介绍这个的分配方式)。在遍历粒子修改的时候,尽量使用while 去遍历,for的遍历性能会比while遍历低。

1.3 粒子更新的方法抽象到具体实现

现在我们的粒子还没有真正动起来。我们可以基于我们粒子一个坐标更新的逻辑。可以很简单,也可以很复杂。这里我这边提供几种常见的玩法。

1.3.1 update函数定义

首先我们在tick 函数中定义更新入口

    function tick() {
        // 清理画布
        ctx.clearRect(0, 0, 500, 500)

        particles.get().forEach(point => {
            ctx.beginPath()
            ctx.arc(point.x, point.y, 3, 0, 2 * Math.PI)
            ctx.closePath()
            ctx.fillStyle = '#aaa'
            ctx.fill()
        })
        // 绑定更新方法
        particles.update(update)

        requestAnimationFrame(tick)
    }

让后我们定义一个更新函数

    function update(particle) {}

1.3.1 简单平移

    function update(particle) {
        // particle 参数就是管理类传进来的粒子实例我们可以修改每一个粒子的坐标
        particle.x += 1
        particle.y += 1
    }

xy-20210726-223548.gif

1.3.2 边界反弹

简单的平移怕是太枯燥乏味了,而且边界的条件没有考虑,这种没有啥意思,于是便有了边界反弹的示例

    function update(particle) {
        // particle 参数就是管理类传进来的粒子实例我们可以修改每一个粒子的坐标

        if (particle.x > 500) {
            particle.factorX = -1
        }
        if (particle.y > 500) {
            particle.factorY = -1
        }
        if (particle.x < 0) {
            particle.factorX = 1
        }
        if (particle.y < 0) {
            particle.factorY = 1
        }

        particle.x += particle.factorX || 1 * 1
        particle.y += particle.factorY || 1 * 1
        // particle.x += 1
        // particle.y += 1
    }

xy-20210726-221559.gif

1.3.3 简单力场

我们假设有一个画板区域内有一个简单的水平向右的力场支配着粒子们,假设大家初始速度都为0,初速度方向水平向右,根据牛顿第二定律,x分量的变化率(即位移的导数)等于每次速度加上加速度的和,就是于是便有了这样的代码

    function update(particle) {
        const FORCE = 1
        const M = 10
        const a = FORCE / M
        if (typeof particle.v === 'undefined') {
            particle.v = 0
        }
        // particle 参数就是管理类传进来的粒子实例我们可以修改每一个粒子的坐标
        particle.v = particle.v + a
        particle.x += particle.v

    }

xy-20210726-224759.gif 可以看出,粒子们有个缓慢加速的过程

1.3.4 平抛运动(典型力场)

让我们把一维速度向量拓展到二维,还有加速度,模拟一个粒子的平抛运动

    function update(particle) {
        const FORCE = 1
        // 随机质量,每个粒子不同的加速度
        const M = Math.random() * 10
        const a = FORCE / M
        if (typeof particle.vx === 'undefined') {
            particle.vx = 5
        }
        if (typeof particle.vy === 'undefined') {
            particle.vy = 0
        }
        // particle 参数就是管理类传进来的粒子实例我们可以修改每一个粒子的坐标
        particle.vy = particle.vy + a
        particle.x += particle.vx
        particle.y += particle.vy

    }

xy-20210726-225355.gif

ToBe Contiune

本期的分享就到这里,下一期我们就粒子的生命周期的设计做一期单独的解说。另外还有怎么另外设计物理解算的架构,把代码从渲染逻辑中解耦出来,以便我们以后更好的实现物理模型的添加和维护。渲染的优化等等一系列内容。敬请期待~~~~