在上一篇文中我们提到了一维弹力模拟
的案例
这次我们则是要实作二维弹力模拟
~并且是存在重力场
的状态!
原则上原理是不会有太大的差异,我们这边再复习一下模拟程序1帧
内的逻辑流程:
RAF
更新画面的时候,首先先清掉画布球
和弹簧
的位置加速度
更新球
的速度
数据球
的位置更新弹力
的向量阻力
,最後和弹力
向量相加,取得受力
(optional)受力
来更新球
的加速度
数据RAF
下面这张图就是我们这次要模拟的案例~
画面中球跟球之间是藉由弹力线
做串连;操作者可以拉动画面中的任何一颗球
,整条弹力链
就会被拖动。
比较需要注意的是这次我们加入了重力场
的计算,这意味着在受力
计算的阶段,我们还要另外加上重力
。
除此之外,由於这次球的运动方向是二维
的,所以就会有水平
、垂直
方向的受力
运算:
水平方向:会因爲使用者拖曳球的角度
,而导致斜向弹力
的出现。
垂直方向:基本上垂直方向
除了会跟水平方向一样受到弹力
的影响外,还会有我们刚刚提到的重力
。
由於这次的案例也有点小复杂,所以我也一样会把案例分成两次来讲解,并且我也会使用webpack 搭配 esModule 来进行案例的实作~
这次我们还是先从场景的搭建开始~
const CANVAS = {
width: 800,
height: 600,
background: 'gray'
}
const BALL = {
radius: 5,
color: 'white'
}
const CORDS = [
{
length: 100,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
{
length: 30,
elasticConst: 100,
},
]
const GRAVITY = 9.8;
const BALL_MASS_CONST = 0.01;
class Ball {
constructor(x, y, radius, color, fixed) {
this.radius = radius;
this.mass = BALL_MASS_CONST * radius;
this.color = color;
this.fixed = fixed; //球是否固定在当前空间中
this.x = x;
this.y = y;
this.velocity = new Vector2D(0, 0);
this.force = new Vector2D(0, 0)
this.acc = new Vector2D(0, 0);
this.gravity = new Vector2D(0, GRAVITY);
}
// 这次我们给球的class 新增这一个方法。用途是用来计算与另外一颗球的距离向量(不含两颗球的半径)
distBetween(ball) {
const dx = ball.x - this.x;
const dy = ball.y - this.y;
const vectorBetween = new Vector2D(dx, dy);
const lengthAlpha = vectorBetween.length();
const length = vectorBetween.length() - this.radius - ball.radius;
const lengthVector = vectorBetween.multiply(length / lengthAlpha);
return lengthVector;
}
draw(ctx) {
ctx.save()
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
// 这次的我们没有要用之前写的Spring ,而是新增了Cord(弦)这个类,弦在初始化的时候必须要传入两个Ball的实例,还有弦的原始长度、弦的弹性系数
class Cord {
constructor(ballFormer, ballLatter, cordLength, elasticConst, cordWidth = 1, color = '#555') {
this.ballFormer = ballFormer; //上面端点的球
this.ballLatter = ballLatter;//下面端点的球
this.cordLength = cordLength; // 原始长度
this.elasticConst = elasticConst; //弹性系数
this.cordWidth = cordWidth;
this.color = color;
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this.ballFormer.x, this.ballFormer.y);
ctx.lineTo(this.ballLatter.x, this.ballLatter.y);
ctx.strokeStyle = this.color;
ctx.lineWidth = this.cordWidth;
ctx.stroke();
ctx.closePath();
}
}
class Elastic2DCordAnimation {
constructor(ctx) {
this.ctx = ctx;
this.cvs = ctx.canvas;
this.balls = [];
this.cords = [];
this.frameIsPaused = false;
// this.ballGrabbed;
this.init();
}
// 入口方法
init() {
this.time = 0;
this.setCanvasSize();
this.initEvents();
this.initEntities();
this.animate();
}
// 把所有的实体(entity) 也就是弦和球都先做实例的初始化
initEntities() {
// init balls;
for (let i = 0; i <= CORDS.length; i++) {
const x = this.cvs.width / 2;
let y = 0;
const cordsBefore = CORDS.filter((cord, index) => {
return index < i
})
// 依据每条弦的长短,总合出球的具体位置
// 这边大於0的判断是用来排除掉第一条弦用的
if (cordsBefore.length > 0) {
y = cordsBefore.map(cord => cord.length).reduce((prev, next, index) => {
const gap = index >= 1 ? BALL.radius * 2 : 0;
return prev + next + gap;
}, BALL.radius)
}
// 最顶端,也就是连结天花板的部分也会被视为一颗球,但是这颗球半径为0,而且会有『固定(fixed)』属性
this.balls.push(new Ball(x, y, i === 0 ? 0 : BALL.radius, BALL.color, i === 0))
}
// init cords
for (let i = 0; i < CORDS.length; i++) {
const cord = new Cord(this.balls[i], this.balls[i + 1], CORDS[i].length, CORDS[i].elasticConst)
this.cords.push(cord);
}
}
initEvents() {
this.initVisibilityChangeEvent();
// this.initMouseEvent();
}
initVisibilityChangeEvent() {
window.addEventListener('visibilitychange', () => {
if (document.visibilityState !== "visible") {
this.frameIsPaused = true;
}
else {
this.frameIsPaused = false;
this.time = performance.now();
}
});
}
setCanvasSize() {
this.cvs.width = CANVAS.width;
this.cvs.height = CANVAS.height;
this.cvs.style.backgroundColor = CANVAS.background;
}
animate() {
if (this.frameIsPaused) {
this.animate();
}
const $this = this;
const dt = (performance.now() - this.time) / 1000;
this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
this.drawAll(dt);
this.time = performance.now();
requestAnimationFrame(this.animate.bind($this));
}
drawAll(dt) {
// 把球和弦都个别画出来
this.cords.forEach((o, i) => {
o.draw(this.ctx);
})
this.balls.forEach((o, i) => {
o.draw(this.ctx);
})
}
}
document.addEventListener('DOMContentLoaded', () => {
let ctx = document.querySelector('canvas').getContext('2d');
let instance = new Elastic2DCordAnimation(ctx)
})
截个图看看~
到这边我们就结束了初期场景与实体(Entity,也就是弦
和球
)的绘制,在下一篇我们就会进入正式的动画阶段~
<<: JS Library 学习笔记:嘿!有听过 GSAP 吗? (二)
>>: 卡夫卡的藏书阁【Book18】- Kafka - KafkaJS 生产者 - 6
在昨天的文章中我整理了一些重用 Ansible 内容的方法,但有时候,我们要完成一项任务可能不单单只...
Button组件 按钮也是在基础组件中常见的项目,它提供了点击事件可以用来定义互动的功能。 Butt...
上次我们已经学会要怎麽从资料库依照各个表取出我们想要的栏位,也可以透过条件筛选的方式过滤我们想要的资...
os 内建的模组之一。定义很多用於操作作业系统的函式,常用於自动化的功能 os.getcwd() 回...
TemplateSendMessage - ImageCarouselTemplate image_...