仿生学应用:电子蝌蚪(一)
前面介绍了一些基础概念和示例,本篇开始进入正题,目标是实现一个电子化蝌蚪的小项目
首先再来回看一下我们想要做出的最终效果:
界面中的每一个小蝌蚪,彷佛有生命般的在”自由“运动,他们长得一样,身体的结构都是由一个头,一个颈部和一个尾巴组成,但是各自都有各自的运动方向,及运动速度,同时还添加了类似碰撞检测的机制。这模仿的效果,相当的惊艳。
以上的描述,实际上就是我们本次任务的需求,那么废话不多说,让我们钻进代码的世界中,用代码,“创造虚拟生命”吧。本项目是基于vue2框架开发。
执行逻辑:在mounted生命周期中,依次调用了三个函数
mounted() {
this.init()
this.drawLovePath()
this.drawTadpole()
},
首先,我们需要利用paperjs,我们需要初始化世界。
initWorld() { console.log("$-_------------初始化世界---------------$-_-"); const canvas = this.$refs.tadpole; // canvas的dom节点给到paper装载 this.paper = paper; this.paper.setup(canvas); // 加装各类事件 this.paper.view.onResize = this.onResize; this.paper.view.onFrame = this.onFrame; this.paper.view.onKeyDown = this.onKeyDown; this.paper.view.onMouseDown = this.onMouseDown; },实质做的事情很简单,挂载页面canvas作为世界,然后绑定各种我们可能需要的相关方法。
有了世界,执行绘制蝌蚪主函数,实例化众多的蝌蚪
drawLovePath() { this.heartPath = new paper.Path('M514.69629,624.70313c-7.10205,-27.02441 -17.2373,-52.39453 -30.40576,-76.10059c-13.17383,-23.70703 -38.65137,-60.52246 -76.44434,-110.45801c-27.71631,-36.64355 -44.78174,-59.89355 -51.19189,-69.74414c-10.5376,-16.02979 -18.15527,-30.74951 -22.84717,-44.14893c-4.69727,-13.39893 -7.04297,-26.97021 -7.04297,-40.71289c0,-25.42432 8.47119,-46.72559 25.42383,-63.90381c16.94775,-17.17871 37.90527,-25.76758 62.87354,-25.76758c25.19287,0 47.06885,8.93262 65.62158,26.79834c13.96826,13.28662 25.30615,33.10059 34.01318,59.4375c7.55859,-25.88037 18.20898,-45.57666 31.95215,-59.09424c19.00879,-18.32178 40.99707,-27.48535 65.96484,-27.48535c24.7373,0 45.69531,8.53564 62.87305,25.5957c17.17871,17.06592 25.76855,37.39551 25.76855,60.98389c0,20.61377 -5.04102,42.08691 -15.11719,64.41895c-10.08203,22.33203 -29.54687,51.59521 -58.40723,87.78271c-37.56738,47.41211 -64.93457,86.35352 -82.11328,116.8125c-13.51758,24.0498 -23.82422,49.24902 -30.9209,75.58594z') }, // // 创建蝌蚪军团 drawTadpole() { for (let i = 0; i < 50; i++) { const location = this.random() this.boids.push(new Boid(location, 10, 0.05)) } },其中,变量heartPath是通过直接引入svg数据,画出一个心形的path路径,如下图所示。后面我们会让所有的蝌蚪沿着该图形的边缘游动。
在后面的for循环中,每次我们在画布中随机生成一个坐标点,并随同另外两个固定值,用于蝌蚪Boid这个类的输入。这是外围的代码。实质就是传入三个参数,一个确定其位置,一个确定其最大速度,还有一个,确定其最大力量,后面详解。
蝌蚪类的细节
- constructor
// 初始化tadpole类
// position: 随机的point类坐标
// maxSpeed: 10
// maxForce: 0.05
constructor(position, maxSpeed, maxForce) {
this.position = position
// 尾巴点数[10, 14]
this.tailAmount = Math.random() * 5 + 10
// 蝌蚪移动速度,此值极其重要,关系到蝌蚪实例的生命力
this.acceleration = new paper.Point()
// vector这个变量,决定了下一帧,或者说是下一时刻,小蝌蚪的运动目的地
// eslint-disable-next-line new-cap
this.vector = new paper.Point.random()
// 取值范围:[10, 10.5),干嘛的未知
this.maxSpeed = maxSpeed + strength
// 取值范围:[0.05, 0.55),干嘛的未知
this.maxForce = maxForce + strength
this.count = 0
this.createItems()
}
结合上述代码, 简要介绍一下我们是如何像当年女娲捏人一般,用代码捏出我们的蝌蚪军团的。
实例化蝌蚪的三个输入,分别是坐标、速度、强度。.constructor构造函数,初始化实例时,自动执行。该函数的作用主要是为了初始化蝌蚪示例的众多变量的数值,在最后调用createItems函数,其功能顾名思义。
- createItems
在createItems中,我们首先根据当前蝌蚪示例的坐标信息,绘制其头部。对了补充一下,官方示例的这个蝌蚪类,其身体由三部分组成,包括头部、颈部和尾巴,分别用不同颜色标识如下:// 定义蝌蚪的身体组成 createItems() { // 椭圆,代表蝌蚪的头部 this.head = new paper.Shape.Ellipse({ // center: [0, 0], center: [this.position.x, this.position.y], size: [15, 10], fillColor: 'orange' }) // 尾巴 this.path = new paper.Path({ strokeColor: 'green', strokeWidth: 2, strokeCap: 'round' }) // 目测是尾巴长度 for (let i = 0; i < this.amount; i++) { this.path.add(new paper.Point()) } // 颈部 this.shortPath = new paper.Path({ strokeColor: 'white', strokeWidth: 4, strokeCap: 'round' }) for (let i = 0; i < 3; i++) { this.shortPath.add(new paper.Point()) } }
其中,头部head用一个椭圆标识,通过new paper.Shape.Ellipse,给他一个椭圆的center及长短半径的值(佩服自己居然还记得椭圆的长短半径),当然还有颜色。而尾巴path以及颈部shortPath则都是path,两者均由众多的点组成。注意:截止此时,除了头部已经画出,颈部及尾巴均没有实际画出,实质上是通过加point的方式,埋了几个坑位.
到此阶段,准备工作大概做了约莫一半了,剩下的就是一些细节问题了。此时跑代码,你应该能看到如下的景象:
蝌蚪们的都有了,颈部和尾巴还没有。当然了,如果觉得有必要一开始连通脖子和颈部也一块初始化,也可以。看自己的需求。
- 接下来我们要做的,是让他们动起来

onFrame() {
this.boids.forEach((boid) => {
boid.run(this.boids, false)
})
},
代码逻辑很清晰,帧级别触发函数,遍历我们的蝌蚪军团实例,挨个调用run方法。正因为是帧级别的事件触发,因此从肉眼的角度观察,蝌蚪们就连贯的动起来了。那么,run函数里头,具体是怎么实现的呢?
咱们下期继续讲解。本文毕
