iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Modern Web

成為Canvas Ninja ! ~ 理解2D渲染的精髓系列 第 17

Day17 - 物理模擬篇 - 彈力、引力與磁力II - 成為Canvas Ninja ~ 理解2D渲染的精髓

  • 分享至 

  • xImage
  •  

在上一篇文中我們提到了一維彈力模擬的案例

這次我們則是要實作二維彈力模擬~並且是存在重力場的狀態!

原則上原理是不會有太大的差異,我們這邊再複習一下模擬程序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,也就是)的繪製,在下一篇我們就會進入正式的動畫階段~


上一篇
Day16 - 物理模擬篇 - 彈力、引力與磁力I - 成為Canvas Ninja ~ 理解2D渲染的精髓
下一篇
Day18 - 物理模擬篇 - 彈力、引力與磁力III - 成為Canvas Ninja ~ 理解2D渲染的精髓
系列文
成為Canvas Ninja ! ~ 理解2D渲染的精髓31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言