Day17 - 物理模拟篇 - 弹力、引力与磁力II - 成为Canvas Ninja ~ 理解2D渲染的精髓

在上一篇文中我们提到了一维弹力模拟的案例

这次我们则是要实作二维弹力模拟~并且是存在重力场的状态!

原则上原理是不会有太大的差异,我们这边再复习一下模拟程序1帧内的逻辑流程:

  • 在每次RAF更新画面的时候,首先先清掉画布
  • 重新描绘弹簧的位置
  • 依据加速度更新速度数据
  • 根据的位置更新弹力的向量
  • 计算阻力,最後和弹力向量相加,取得受力(optional)
  • 根据受力来更新加速度数据
  • 进到下一圈RAF

二维弹性模拟

下面这张图就是我们这次要模拟的案例~

img

画面中球跟球之间是藉由弹力线做串连;操作者可以拉动画面中的任何一颗球,整条弹力链 就会被拖动。

比较需要注意的是这次我们加入了重力场的计算,这意味着在受力计算的阶段,我们还要另外加上重力

除此之外,由於这次球的运动方向是二维的,所以就会有水平垂直方向的受力运算:

水平方向:会因爲使用者拖曳球的角度,而导致斜向弹力的出现。

垂直方向:基本上垂直方向除了会跟水平方向一样受到弹力的影响外,还会有我们刚刚提到的重力

由於这次的案例也有点小复杂,所以我也一样会把案例分成两次来讲解,并且我也会使用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)
})

截个图看看~
img

到这边我们就结束了初期场景与实体(Entity,也就是)的绘制,在下一篇我们就会进入正式的动画阶段~


<<:  JS Library 学习笔记:嘿!有听过 GSAP 吗? (二)

>>:  卡夫卡的藏书阁【Book18】- Kafka - KafkaJS 生产者 - 6

Day 30:Ansible Role

在昨天的文章中我整理了一些重用 Ansible 内容的方法,但有时候,我们要完成一项任务可能不单单只...

Flutter体验 Day 9-Button组件

Button组件 按钮也是在基础组件中常见的项目,它提供了点击事件可以用来定义互动的功能。 Butt...

抓取资料库数据 - SQL基础语法(中)

上次我们已经学会要怎麽从资料库依照各个表取出我们想要的栏位,也可以透过条件筛选的方式过滤我们想要的资...

30天学会 Python: Day 14-自动化的第一步

os 内建的模组之一。定义很多用於操作作业系统的函式,常用於自动化的功能 os.getcwd() 回...

[DAY20]图片旋转木马

TemplateSendMessage - ImageCarouselTemplate image_...