本系列文章可以在这里查看:verletjs动画框架

你可以从这里查看该项目的所有源文件:https://github.com/subprotocol/verlet-js

涉及文件:lib/verlet.js, lib/constraint.js

设计思路

动画由基本粒子和粒子间的约束组成,粒子有两种形态:固定和不固定;动画产生的外力来自重力,或者是鼠标的拖动;重力产生的运动比较好理解,每个粒子的速度沿垂直向下方向递增,然后考虑是否超过边界,然后做边界碰撞(牛顿第三定律–作用力与反作用力)。至于鼠标拖动导致的运动,力的方向不固定,因此需要当前的速度和鼠标外力做向量的算术运算来决定粒子下一时刻的位置。最后在固定的时间帧内刷新画布。

动画过程

1、 根据外力计算当前时刻的速度(位置)

2、 重新计算粒子约束(连线)位置、大小、方向等

3、 如果是鼠标外力所致(比如拖动了某个节点),高亮被拖拽的节点

4、 调用draw方法重绘画布

基本粒子构造函数

1
2
3
4
function Particle(pos) {
    this.pos = (new Vec2()).mutableSet(pos);
    this.lastPos = (new Vec2()).mutableSet(pos);
}

可以看出,一个基本的粒子元素只有两个元素;当前位置和上一时刻位置。每个位置都是一个二维的坐标对象;如前一节介绍。

1
2
3
4
5
6
Particle.prototype.draw = function(ctx) {
    ctx.beginPath();
    ctx.arc(this.pos.x, this.pos.y, 2, 0, 2*Math.PI);
    ctx.fillStyle = "#2dad8f";
    ctx.fill();
}

粒子对象有自己draw方法,这里就是画一个半径为2的圆。当前实际应用中不会这么简单,比如某些时候的draw方法我们需要引入图片或者画一些复杂的图形(蜘蛛、雪花、树等)

粒子间的约束对象构造函数

1
2
3
4
5
6
7
8
9
10
11
/*
 * @param {Particle} a 粒子a
 * @param {Particle} b 粒子b
 * @param {Number} stiffness 刚度,决定线段的强度(承重能力)或者拉伸能力;取值0-1,可以大于1(存在BUG)
 */
function DistanceConstraint(a, b, stiffness, distance /*optional*/) {
    this.a = a;
    this.b = b;
    this.distance = typeof distance != "undefined" ? distance : a.pos.sub(b.pos).length();
    this.stiffness = stiffness;
}

所谓约束就是两个粒子间连线(连线具有“柔性”属性,可以决定外力作用在其上面发生的形变—胡克定律)

线段约束的draw方法

1
2
3
4
5
6
7
DistanceConstraint.prototype.draw = function(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.a.pos.x, this.a.pos.y);
    ctx.lineTo(this.b.pos.x, this.b.pos.y);
    ctx.strokeStyle = "#d8dde2";
    ctx.stroke();
}

线段约束在拖动过程中产生的“振荡”效果(relax方法)

1
2
3
4
5
6
7
8
9
10
11
/*
 * 约束线段产生小范围振荡的效果; 模拟惯性
 * @param {Number} stepCoef 振荡系数 0-1
 */
DistanceConstraint.prototype.relax = function(stepCoef) {
    var normal = this.a.pos.sub(this.b.pos);
    var m = normal.length2();
    normal.mutableScale(((this.distance*this.distance - m)/m)*this.stiffness*stepCoef);
    this.a.pos.mutableAdd(normal);
    this.b.pos.mutableSub(normal);
}

固定的粒子对象

1
2
3
4
5
6
7
8
/*
 * @param {Particle} a 需要固定的粒子对象
 * @param {Vec2 || Object} 固定的位置
 */
function PinConstraint(a, pos) {
    this.a = a;
    this.pos = (new Vec2()).mutableSet(pos);
}

当然,你可以把“固定”看成和“约束”一类的实体。都有自己的draw方法。

固定粒子的draw方法

1
2
3
4
5
6
PinConstraint.prototype.draw = function(ctx) {
    ctx.beginPath();
    ctx.arc(this.pos.x, this.pos.y, 6, 0, 2*Math.PI);
    ctx.fillStyle = "rgba(0,153,255,0.1)";
    ctx.fill();
}

固定粒子在运动过程中的“惯性”效果

1
2
3
PinConstraint.prototype.relax = function(stepCoef) {
    this.a.pos.mutableSet(this.pos);
}

其效果就是保持该固定粒子节点的位置不变

组合体

什么是组合体呢?粒子和约束都是分散的,为了便于管理(执行动画);引入组合这个概率,从编程的角度来看就是一个对象集合。后续的动画执行只需要关心该集合即可

1
2
3
4
5
6
7
function Composite() {
    this.particles = [];
    this.constraints = [];
    
    this.drawParticles = null;
    this.drawConstraints = null;
}

从其属性上就能看出,这个对象是粒子和约束的组合体。当然还可以定义两个方法:drawParticles用于自定义画粒子,如上面粒子的draw方法,可用于替代该方法; drawConstraints用于自定义画约束

组合对象有一个供设定固定点的方法

1
2
3
4
5
6
Composite.prototype.pin = function(index, pos) {
    pos = pos || this.particles[index].pos;
    var pc = new PinConstraint(this.particles[index], pos);
    this.constraints.push(pc);
    return pc;
}

其作用就是,新建一个固定点对象,然后也放到约束数组里面。约束数组里面的元素都具有自己的draw方法:如线段约束的draw方法就是连线(moveTo,lineTo),而固定点的约束就是在重力的影响下保持位置不变(除非外力,如鼠标拖动)

三角形对象

除了以上介绍的几个对象之外,作者还单独针对三角形图形实现了三角形对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
 * @param {Particle} a 顶点
 * @param {Particle} b 顶点
 * @param {Particle} c 顶点
 * @param {Number} stiffness 刚度
 */
function AngleConstraint(a, b, c, stiffness) {
    this.a = a;
    this.b = b;
    this.c = c;
    // 角abc的度数(弧度)
    this.angle = this.b.pos.angle2(this.a.pos, this.c.pos);
    this.stiffness = stiffness;
}

/*
 *  三角形对象在运动过程中的“惯性”效果
 * @param {Number} stepCoef 惯性系数0-1
 */
AngleConstraint.prototype.relax = function(stepCoef) {
    var angle = this.b.pos.angle2(this.a.pos, this.c.pos);
    var diff = angle - this.angle;
    
    if (diff <= -Math.PI)
        diff += 2*Math.PI;
    else if (diff >= Math.PI)
        diff -= 2*Math.PI;

    diff *= stepCoef*this.stiffness;
    
    // 旋转一个小角度
    this.a.pos = this.a.pos.rotate(this.b.pos, diff);
    this.c.pos = this.c.pos.rotate(this.b.pos, -diff);
    this.b.pos = this.b.pos.rotate(this.a.pos, diff);
    this.b.pos = this.b.pos.rotate(this.c.pos, -diff);
}

// 画一个三角形
AngleConstraint.prototype.draw = function(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.a.pos.x, this.a.pos.y);
    ctx.lineTo(this.b.pos.x, this.b.pos.y);
    ctx.lineTo(this.c.pos.x, this.c.pos.y);
    var tmp = ctx.lineWidth;
    ctx.lineWidth = 5;
    ctx.strokeStyle = "rgba(255,255,0,0.2)";
    ctx.stroke();
    ctx.lineWidth = tmp;
}

本教程介绍了verletjs的核心对象和方法,接下来会接受verletjs是如何把以上对象组合在一起实现动画的。