iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Modern Web

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

Day10 - 物理模擬篇 - 彈跳球世界I - 成為Canvas Ninja ~ 理解2D渲染的精髓

作為物理模擬開場的第一進程,當然就要來講一下最經典的物理模擬案例:『彈跳球』~
其實很多國外的Canvas特效教程都會把這一篇當成第一個介紹案例,比方說

這邊推薦一下 Apress Physics for JavaScript Games Animation and Simulations 這本書,因為在學習物理模擬的路上這本書給了我不少幫助XD~

在這個案例中我們除了會介紹彈跳球的案例,還會介紹一些關於這個案例的基礎物理常識,最後還會帶到一些更進階的物理模擬實作。

在一開始我們還不會馬上的帶到程式源碼,而是要先來討論高中數理的向量反射斜向拋射,由於我們在這個案例中會持續用到的三個基礎概念,所以我打算在一開始就講清楚物理模擬在這三部分的相關概念。

我們在這篇文章中會先討論到Canvas向量類的建立,就讓我們接著開始吧~

向量是什麼樣的概念?

我們其實在前面的文章有提過向量,向量指的是一種從座標A移動到座標B的附帶方向的移動量,從數學的角度上來看,假設今天有一個質點即將從(1,2)移動到(2,4),則我們可說這個質點被附加了一個(1,2)的移動向量。

向量如果要轉變成純量,那麼就必須要取該向量X,Y值的平方和,然後再開根號(畢氏定理),以我們剛剛提到的(1,2),他的純量就是√5(也就是該質點一共移動了√5的距離長度)。

向量再轉變成純量的過程中會丟失他的方向屬性,而變成單純的量值,所以如果今天換成另外一個案例,假設我們只知道移動的距離是√5而不知道這個移動的起始點和結束點; 想要把√5這個距離轉變成向量(也就是要知道水平和垂直移動的距離),那我們就必須要先獲知該純量的方向(也就是下圖中的角度θ),然後用三角函數來把√5轉變成1(水平移動量)和2(垂直移動量)。

img

(cosθ * √5, sinθ * √5) = (1,2)

除了向量變純量, 純量變向量的運算以外,向量之間有其他類型的運算,像是:

  • 相加/相減

以下面這張圖為例,我們可以可以把紫色向量看作是向量a(紅色向量)和向量b(藍色向量)的和。
所以反過來也可以推導紫色向量 - 向量a = 向量b

  • 內積

內積是一個有趣的概念,求取兩個向量內積的方法如下:

假設向量a為(ax,ay),向量b則是(bx,by)
則向量a與向量b的內積是ax*bx+ay*by

內積的結果會是一個純量,他的幾何意義在於我們可以透過內積取得兩個向量的夾角。
透過內積取得夾角的公式如下:

img

一般來說,內積的值大於0,代表兩向量夾角低於90度
內積的值等於0,代表兩個向量互相垂直
內積的值小於0,代表兩個向量夾角介於90度到180度之間。

對公式推導有興趣的人可以看這邊

用javascript建立向量類(Vector Class)

在前端開發的環境下,我們其實可以利用ES6的class(當然也可以用ES5的構築式)去給向量建立一個獨立的類。

class Vector2D {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  /**
   * 求純量值
   *
   * @returns
   * @memberof Vector2D
   */
  length() {
    return Math.sqrt(this.lengthSquared());
  }
  /**
   * 複製該向量
   *
   * @returns
   * @memberof Vector2D
   */
  clone() {
    return new Vector2D(this.x, this.y);
  }
  /**
   *倒轉該向量
   *
   * @memberof Vector2D
   */
  negate() {
    this.x = - this.x;
    this.y = - this.y;
  }

  /**
   * 把該向量轉變成單位向量
   *
   * @returns
   * @memberof Vector2D
   */
  normalize() {
    let length = this.length(); if (length > 0) {
      this.x /= length;
      this.y /= length;
    }
    return this.length();
  }

  /**
   * 回傳與某向量的向量和
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  add(vec) {
    return new Vector2D(this.x + vec.x, this.y + vec.y);
  }

  /**
   * 加上某向量
   *
   * @param {*} vec
   * @memberof Vector2D
   */
  incrementBy(vec) {
    this.x += vec.x;
    this.y += vec.y;
  }

  /**
   * 
   * 回傳與某向量的向量差
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  subtract(vec) {
    return new Vector2D(this.x - vec.x, this.y - vec.y);
  }

  /**
   * 扣除某向量
   *
   * @param {*} vec
   * @memberof Vector2D
   */
  decrementBy(vec) {
    this.x -= vec.x;
    this.y -= vec.y;
  }


  /**
     * 回傳擴增k倍後的向量
     *
     * @param {*} k
     * @memberof Vector2D
     */
  multiply(k) {
    return new Vector2D(k * this.x, k * this.y);
  }

  /**
   * 擴增該向量
   *
   * @param {*} k
   * @memberof Vector2D
   */
  scaleBy(k) {
    this.x *= k; this.y *= k;
  }


  /**
   * 求取該向量與其他向量的內積
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  dotProduct(vec) {
    return this.x * vec.x + this.y * vec.y;
  }

  /**
   * 求取此向量映射在某向量上的長度
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  projection(vec) {
    const length = this.length();
    const lengthVec = vec.length();
    let proj;
    if ((length == 0) || (lengthVec == 0)) {
      proj = 0;
    } else {
      proj = (this.x * vec.x + this.y * vec.y) / lengthVec;
    }
    return proj;
  }

  /**
   * 回傳一個新向量,新向量的方向會跟作為參數向量相同,但是量值上是作為此向量投射在參數向量上的長度
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  project(vec) {
    return vec.para(this.projection(vec));
  }
  
  
  /**
   * 回傳垂直與此向量的u倍單位向量
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  perp(u,anticlockwise = true){
		if (typeof(anticlockwise)==='undefined') anticlockwise = true;
		var length = this.length();
		var vec = new Vector2D(this.y, -this.x);
		if (length > 0) {
			if (anticlockwise){ 
				vec.scaleBy(u/length);
			}else{
				vec.scaleBy(-u/length);				
			}
		}else{
			vec = new Vector2D(0,0);
		}	
		return vec;
	}


/**
   * 根據傳入的u值來回傳一個u倍(或-u倍)的單位向量
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  para(u, positive = true) {

    const length = this.length();
    const vec = new Vector2D(this.x, this.y);
    if (positive) {
      vec.scaleBy(u / length);
    } else {
      vec.scaleBy(-u / length);
    }
    return vec;
  }

  /**
   * 求取該向量與其他向量的夾角
   *
   * @param {*} vec
   * @returns
   * @memberof Vector2D
   */
  static angleBetween(vec1, vec2) {
    return Math.acos(vec1.dotProduct(vec2) / (vec1.length() * vec2.length()));
  }
  
  
  

}

這邊我其實是參照Apress Physics for JavaScript Games Animation and Simulations, With HTML5 Canvas 上的寫法,改寫成ES6 Class,並刪除部分不常用到的方法。

我們在接下來的文章中會持續的用到由這邊建立好的向量類,所以各位同學可以看一下這個類裡面都有些什麼方法~

下一篇文我們將會講到如何在Canvas中實作反射(Reflection)行為,敬請期待~


上一篇
Day9 - 2D渲染環境基礎篇 V[Canvas動畫概論] - 成為Canvas Ninja ~ 理解2D渲染的精髓
下一篇
Day11 - 物理模擬篇 - 彈跳球世界II - 成為Canvas Ninja ~ 理解2D渲染的精髓
系列文
成為Canvas Ninja ! ~ 理解2D渲染的精髓31

尚未有邦友留言

立即登入留言