<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      制作一個(gè)炫酷的多小球碰碰的 JS 網(wǎng)頁特效,入門彈性碰撞模擬和類的應(yīng)用

      前言

      在前端開發(fā)里,canvas 是 HTML5 里最炫酷的工具。我們今天就來搞一個(gè)這樣的夢幻的效果,學(xué)習(xí)一下 ES6 的類在開發(fā)一個(gè)完整項(xiàng)目的思路(即 ES5 的構(gòu)造函數(shù)),還有物理碰撞的程序的實(shí)現(xiàn),當(dāng)然,效果也很酷炫!

      完整代碼在此處

      先畫一個(gè)圓

      使用“類”這種被廣泛應(yīng)用的面向?qū)ο蟮母拍睿覀兛梢愿玫恼砦覀兊拇a,做出更大的項(xiàng)目。

      所以我們先創(chuàng)建一個(gè) <canvas> 畫板的類 class Canvas { } ,以便抽象我們之后對 <canvas> 的操作。

      然后再向類里添加第一個(gè)方法 drawCircle() ,作為我們的測試吧,就是先畫一個(gè)最簡單的元素 --- 圓!

      完整代碼如下 (可以在 這個(gè)編輯器 進(jìn)行簡單調(diào)試):

      <body></body>
      <script>
          class Canvas {
              constructor(parent = document.body, width = 400, height = 400){
                  this.canvas = document.createElement('canvas');
                  this.canvas.width = width;  // canvas 的高
                  this.canvas.height = height;  // canvas 的高
                  parent.appendChild(this.canvas);  // 向 DOM 中添加 canvas
                  this.ctx = this.canvas.getContext('2d');  // 畫筆
              }
      
              drawCircle(actor){  // 畫一個(gè)圓
                  this.ctx.strokeStyle = 'black';
                  this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);  // 畫出邊框
                  this.ctx.beginPath();
                  this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2);
                  this.ctx.closePath();
                  this.ctx.fillStyle = actor.color;
                  this.ctx.fill();
              }
          }
          
          // ------------ 測試
          
          const draw = new Canvas();  // 聲明一個(gè)畫布類
          
          const ball = {  // 定義一個(gè) 圓
              position : {
                  x : 100,
                  y : 100,
              },
              radius : 25,
              color : 'blue',
          };
      
          draw.drawCircle(ball);  // 繪制
      </script>
      

      在代碼里,我們定義了一個(gè)圓的屬性,即 位置 x y 和半徑 、 顏色。通過這種井井有條又優(yōu)雅的方式,我們的目的就達(dá)到了!

      image

      這就是一切的基礎(chǔ),一切從這里開始。

      完善我們的類

      我們直接使用 ball 顯然是不夠的,小球它們要有自己的思想,我們的 Canvas 類要只負(fù)責(zé)繪制,所以我們需要重新開辟一個(gè)類,叫 Ball 類,來處理它們自己的“思想”。

      canvas 類也需要更多的可擴(kuò)展性,今天我們是畫圓,明天我們想畫圈、方塊,我們也要考慮到,所以現(xiàn)在,我們要完善一下。

      完整代碼如下,這樣就完美了 ~

      <body></body>
      <script>
           class Canvas{
              constructor(parent = document.body, width = 400, height = 400){
                  this.canvas = document.createElement('canvas');
                  this.canvas.width = width;  // canvas 的高
                  this.canvas.height = height;  // canvas 的高
                  parent.appendChild(this.canvas);  // 向 DOM 中添加 canvas
                  this.ctx = this.canvas.getContext('2d');  // 畫筆
              }
      
              drawCircle(actor){  // 畫一個(gè)圓
                  this.ctx.strokeStyle = 'black';
                  this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);  // 畫出邊框
                  this.ctx.beginPath();
                  this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2);
                  this.ctx.closePath();
                  this.ctx.fillStyle = actor.color;
                  this.ctx.fill();
              }
      
              drawActor(actors){  // 畫角色,可選擇畫圓等等
                  for (const actor of actors) {
                      if(actor.type === 'circle'){
                          this.drawCircle(actor);
                      }
                  }
              }
          }
      
          class Ball{
              constructor(config){
                  Object.assign(this,{  // 類 自身的屬性,在這里定義
                      type : 'circle',
                      position : {x : 100, y : 100},
                      color : 'blue',
                      radius : 25,
      
                  },config);
              }
          }
      
      
          // ---------- 測試
      
          const draw = new Canvas();
          const ball = new Ball();
          draw.drawCircle(ball);
      </script>
      

      圖像能畫出來,那么下一步就是運(yùn)動了。這個(gè)要復(fù)雜了,一下子想不到要怎么弄,所以要一步一步來。

      小球動起來

      我們想一下,小球動起來,必定需要把畫板清空,然后更改位置、繪制,再清空,再更改位置、繪制... 一幀一幀來。

      所以,

      1. 畫板需要有一個(gè)方法,清空畫板 方法
      2. 計(jì)算小球下一幀的位置
      3. 再封裝一個(gè) 【一鍵更新數(shù)據(jù)】,用于操作更新數(shù)據(jù)的邏輯,以及記錄和返回計(jì)算的結(jié)果(表示當(dāng)前一幀整個(gè)游戲的宏觀狀態(tài))

      (第三點(diǎn)的這種思想,可以看這個(gè)文章

      先實(shí)現(xiàn)第一條,這個(gè)很好搞,canvas 只需要使用白色畫筆,畫一個(gè)覆蓋全畫板的矩形即可:

      (不過,我們可以不使用純白,使用 0.4 的透明度,可以一點(diǎn)一點(diǎn)將上一幀給緩緩刷白,效果很好!)

      clearDisplay(){  // 清空畫布
      	this.ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';  // 這個(gè)透明度 0.4 是精華,繪制軌跡效果的關(guān)鍵
      	this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
      }
      

      然后是第二條。

      小球如果要運(yùn)動,必然需要知道要往哪里運(yùn)動。現(xiàn)在我們引入物理的概念 --- 速度(velocity),這是一個(gè)向量值。

      而下一幀要去的地方,就是當(dāng)前的位置,加上當(dāng)前的速度向量。比如速度是向右 5m/s,那下一秒的位置就是當(dāng)前位置加上向右 5 米。

      這是屬于球的個(gè)人的“思想”,所以我們寫到 Ball 類里面,同時(shí) 球 也要加上 速度 這個(gè)屬性,位置和速度都是向量,都是 x y。

      (當(dāng)然,向量又是一個(gè)復(fù)雜的個(gè)體,所以我們需要再單獨(dú)開辟一個(gè)向量類 Vector

      // 球類
      class Ball {
      	constructor(config){
      		Object.assign(this,{
      			type : 'circle',
      			position : new Vector(100, 100),  // 位置也是向量
      			velocity : new Vector(5, 3),  // 當(dāng)前的速度
      			color : 'blue',
      			radius : 25,
      
      		},config);
      	}
      
      	nextFrameUpdate(){  // 計(jì)算下一幀,小球的位置
      		return new Ball({
      			...this,  // 其他屬性保持不變
      			position: this.position.add(this.velocity),  // 所謂的計(jì)算,其實(shí)就是根據(jù)向量 +1
      		});
      	} 
      }
      

      canvas 里,x 和 y 的兩個(gè)正方向如圖所示,所以當(dāng)前小球的速度是向右下:

      image

      下面就是我們當(dāng)前的向量類 Vector

      // 向量(可作為位置 和 速度)
      class Vector {
      	constructor(x, y) {
      		this.x = x;
      		this.y = y;
      	}
      
      	add(vector) {  // 兩個(gè)向量相加,就是這樣
      		return new Vector(this.x + vector.x, this.y + vector.y);
      	}
      }
      

      然后,就是使用 js 里用爛了的 requestAnimationFrame 讓這個(gè)畫面一幀一幀動起來,它是根據(jù)瀏覽器的性能實(shí)時(shí)智能控制幀率的,一般是 100幀/s 左右。不熟悉的同學(xué)可以看這個(gè) MDN 的介紹

      image

      完整的代碼如下:

      <body></body>
      <script>
          // 畫圖 類
          class Canvas {
              constructor(parent = document.body, width = 400, height = 400){
                  this.canvas = document.createElement('canvas');
                  this.canvas.width = width;
                  this.canvas.height = height;
                  parent.appendChild(this.canvas);
                  this.ctx = this.canvas.getContext('2d');
              }
      
              sync(state){  // 執(zhí)行下一幀的繪圖(或稱 在畫板上同步已經(jīng)計(jì)算好的下一幀的數(shù)據(jù))
                  this.clearDisplay();
                  this.drawActor(state.actors);
              }
      
              clearDisplay(){  // 清空畫布
                  this.ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
                  this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
              }
      
              drawActor(actors){  // 畫一個(gè)角色,比如畫一個(gè)圓
                  for (const actor of actors) {
                      if(actor.type === 'circle'){
                          this.drawCircle(actor);
                      }
                  }
              }
      
              drawCircle(actor){  // 畫一個(gè)圓
                  this.ctx.strokeStyle = 'black';
                  this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);  // 畫出邊框
                  this.ctx.beginPath();
                  this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2);
                  this.ctx.closePath();
                  this.ctx.fillStyle = actor.color;
                  this.ctx.fill();
              }
          }
      
          // 球類
          class Ball {
              constructor(config){
                  Object.assign(this,{
                      type : 'circle',
                      position : new Vector(100, 100),
                      velocity : new Vector(5, 3),
                      color : 'blue',
                      radius : 25,
      
                  },config);
              }
      		
              nextFrameUpdate(){  // 計(jì)算下一幀,小球的位置
                  return new Ball({
                      ...this,  // 其他屬性保持不變,ES6 的寫法
                      position: this.position.add(this.velocity),  // 所謂的計(jì)算,其實(shí)就是根據(jù)向量 +1
                  });
              } 
          }
      
          // 向量(可作為位置 和 速度)
          class Vector {
              constructor(x, y) {
                  this.x = x;
                  this.y = y;
              }
              /* 向量的各種運(yùn)算 */
              add(vector) {  // 加
                  return new Vector(this.x + vector.x, this.y + vector.y);
              }
          }
      
          // 宏觀狀態(tài),可理解為【一鍵更新數(shù)據(jù)】
          class DisplayState {
              constructor(displayEle, actors) {
                  this.displayEle = displayEle;
                  this.actors = actors;
              }
      
              update() {
                  const new_actors = this.actors.map(actor => {  // 獲取下一幀的位置數(shù)據(jù)
                      return actor.nextFrameUpdate();
                  });
      
                  return new DisplayState(this.displayEle, new_actors);  // 把 DisplayState 類的屬性更新后,把最新數(shù)據(jù)再返回
              }
          }
      
          // ---------- 測試
      
          const displayEle = new Canvas();
          const ball = new Ball();
          const actors = [ball];  // 我們可能會繪制很多球
      
          let displayState = new DisplayState(displayEle, actors);
      
          function myAnimation(){
              displayState = displayState.update();  // 一鍵更新數(shù)據(jù)
              displayEle.sync(displayState);     // 根據(jù)更新的數(shù)據(jù)來繪畫
              requestAnimationFrame(myAnimation)
          }
      
          myAnimation();
      </script>
      

      最簡單的碰撞計(jì)算,接觸墻壁反彈

      這個(gè),還幾乎用不到物理碰撞算法之類。其實(shí)實(shí)現(xiàn)這個(gè)功能特別簡單,只需要檢測到小球到達(dá)墻壁邊界,然后相應(yīng)的速度正負(fù)轉(zhuǎn)化一下即可!

      代碼很簡單,很易懂,將 Ball 類里的 nextFrameUpdate 計(jì)算下一幀位置 的這個(gè)方法添加兩個(gè)判斷即可:

      nextFrameUpdate(displayState){  // 計(jì)算下一幀,小球的位置
      
      	// 如果小球左右到達(dá)邊界,X 速度取反
      	if (this.position.x >= displayState.displayEle.canvas.width - this.radius || this.position.x <= this.radius) {
      		this.velocity = new Vector(-this.velocity.x, this.velocity.y);
      	}
      
      	// 如果小球上下到達(dá)邊界,Y 速度取反
      	if (this.position.y >= displayState.displayEle.canvas.height - this.radius || this.position.y <= this.radius) {
      		this.velocity = new Vector(this.velocity.x, -this.velocity.y);
      	}
      
      	return new Ball({
      		...this,  // 其他屬性保持不變
      		position: this.position.add(this.velocity),
      	});
      }
      

      注意,判斷依據(jù)一定是小球的邊界,和墻壁的邊界,而不是小球的中心。這里就不貼出完整代碼了,完善向量后再貼!我們接下來要根據(jù)物理公式計(jì)算兩個(gè)小球之間的碰撞,因此我們需要將向量類 Vector 完善一下。

      向量類的完善

      向量是我們中學(xué)的學(xué)習(xí)內(nèi)容,向量有哪些計(jì)算呢?

      加減乘除?

      加減好說,每個(gè)元素分別加減即可。有乘法,但沒有除法。還有取模和角度。

      乘法有兩種,一種是常數(shù)與之乘法,每個(gè)元素都乘以相同的常數(shù):

      multiply(scalar) {  // 逐元素乘法
      	return new Vector(this.x * scalar, this.y * scalar);
      }
      

      另一種,是向量之間的相乘,我們稱其為點(diǎn)積或數(shù)量積:

      dotProduct(vector) {  // 數(shù)量積
      	return this.x * vector.x + this.y * vector.y;
      }
      

      除了加減乘除,還有取模和取角度,模就是向量的長度(用于計(jì)算兩個(gè)小球之間的距離),角度就是向量的 arctan 值(反正切值)。

      怎么取模呢?

      根據(jù)勾股定理,根號下 x 的平方 加 y 的平方。

      get magnitude() {  // 求模
      	return Math.sqrt(this.x ** 2 + this.y ** 2);
      }
      

      角度就使用反正切將 x y 搞一下就好:

      get direction() {  // 求方向的角度 tan
      	return Math.atan2(this.x, this.y);
      }
      

      完整的代碼如下:

      <body></body>
      <script>
          class Canvas {
              constructor(parent = document.body, width = 400, height = 400){
                  this.canvas = document.createElement('canvas');
                  this.canvas.width = width;
                  this.canvas.height = height;
                  parent.appendChild(this.canvas);
                  this.ctx = this.canvas.getContext('2d');
              }
      
              sync(state){  // 執(zhí)行下一幀的繪圖(或稱 在畫板上同步已經(jīng)計(jì)算好的下一幀的數(shù)據(jù))
                  this.clearDisplay();
                  this.drawActor(state.actors);
              }
      
              clearDisplay(){
                  this.ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
                  this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
              }
      
              drawActor(actors){  // 畫一個(gè)角色,比如畫一個(gè)圓
                  for (const actor of actors) {
                      if(actor.type === 'circle'){
                          this.drawCircle(actor);
                      }
                  }
              }
      
              drawCircle(actor){  // 畫一個(gè)圓
                  this.ctx.strokeStyle = 'black';
                  this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);  // 畫出邊框
      
                  this.ctx.beginPath();
                  this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2);
                  this.ctx.closePath();
                  this.ctx.fillStyle = actor.color;
                  this.ctx.fill();
              }
          }
      
          // 球類
          class Ball {
              constructor(config){
                  Object.assign(this,{
                      type : 'circle',
                      position : new Vector(100, 100),
                      velocity : new Vector(5, 3),
                      color : 'blue',
                      radius : 25,
      
                  },config);
              }
      
              nextFrameUpdate(displayState){  // 計(jì)算下一幀,小球的位置
      
                  // 如果小球左右到達(dá)邊界,X 速度取反
                  if (this.position.x >= displayState.displayEle.canvas.width - this.radius || this.position.x <= this.radius) {
                      this.velocity = new Vector(-this.velocity.x, this.velocity.y);
                  }
      
                  // 如果小球上下到達(dá)邊界,Y 速度取反
                  if (this.position.y >= displayState.displayEle.canvas.height - this.radius || this.position.y <= this.radius) {
                      this.velocity = new Vector(this.velocity.x, -this.velocity.y);
                  }
      
                  return new Ball({
                      ...this,  // 其他屬性保持不變
                      position: this.position.add(this.velocity),
                  });
              } 
          }
      
          // 向量(可作為位置 和 速度)
          class Vector {
              constructor(x, y) {
                  this.x = x;
                  this.y = y;
              }
      
              /* 向量的各種運(yùn)算 */
              add(vector) {  // 加
                  return new Vector(this.x + vector.x, this.y + vector.y);
              }
      
              subtract(vector) {  // 減
                  return new Vector(this.x - vector.x, this.y - vector.y);
              }
      
              multiply(scalar) {  // 逐元素乘法
                  return new Vector(this.x * scalar, this.y * scalar);
              }
      
              dotProduct(vector) {  // 數(shù)量積
                  return this.x * vector.x + this.y * vector.y;
              }
      
              get magnitude() {  // 求模
                  return Math.sqrt(this.x ** 2 + this.y ** 2);
              }
      
              get direction() {  // 求方向的角度 tan
                  return Math.atan2(this.x, this.y);
              }
          }
      
          // 宏觀狀態(tài)
          class DisplayState {
              constructor(displayEle, actors) {
                  this.displayEle = displayEle;
                  this.actors = actors;
              }
      
              update() {
                  const new_actors = this.actors.map(actor => {
                      return actor.nextFrameUpdate(this);
                  });
      
                  return new DisplayState(this.displayEle, new_actors);
              }
          }
      
      
          // ---------- 測試
      
          const displayEle = new Canvas();
          const ball = new Ball();
          const actors = [ball];
      
          let displayState = new DisplayState(displayEle, actors);
      
          function myAnimation(){
              
              displayState = displayState.update();  // 數(shù)據(jù)更新
              displayEle.sync(displayState);     // 根據(jù)更新的數(shù)據(jù)來繪畫
      
              requestAnimationFrame(myAnimation)
          }
      
          myAnimation();
      </script>
      

      檢測兩小球之間的碰撞

      我們要先定義兩個(gè)小球,大綠球、小藍(lán)球,我們的實(shí)驗(yàn)就是根據(jù)這倆球來進(jìn)行:

      const ball1 = new Ball({  // 小球一
      	position: new Vector(40, 100),
      	velocity: new Vector(1, 0),
      	radius: 20,
      	color: 'green',
      });
      const ball2 = new Ball({  // 小球二
      	position: new Vector(200, 100),
      	velocity: new Vector(-1, 0),
      	color: 'blue',
      });
      
      const actors = [ball1, ball2];
      

      然后,我們在計(jì)算下一幀的那個(gè) nextFrameUpdata() 方法里,添加這樣一個(gè)邏輯。每次都計(jì)算所有其他的小球與自己的距離,以判斷是否碰到。

      for (const actor of displayState.actors) {  // 把其他球都計(jì)算一次
      	if (this === actor) {  // 無需計(jì)算自己
      		continue;
      	}
      	const distance = this.position.subtract(actor.position).magnitude;  // 計(jì)算倆球的距離
      	if (distance <= this.radius + actor.radius) {  // 如果倆球距離小于兩球半徑,就都變灰
      		this.color = 'grey';
      		actor.color = 'grey';
      	}
      }
      

      這樣效果就出來了。

      image

      完善碰撞的效果

      我們現(xiàn)在需要完善這個(gè)碰撞的效果。變色,表示我們已經(jīng)能檢測到兩個(gè)球是否碰到了,但沒有視覺效果。

      碰撞的效果看起來很簡單,一瞬間的事,但實(shí)現(xiàn)起來并不簡單。

      能量既不會憑空產(chǎn)生,也不會憑空消失,它只能從一種形式轉(zhuǎn)化為另一種形式,或者從一個(gè)物體轉(zhuǎn)移到另一個(gè)物體,總量保持不變。 ----- 能量守恒定理

      首先,我們在物理里學(xué)過《能量守恒定理》,m 是質(zhì)量,v 是速度。

      $$m_{A} v_{A 1}+m_{B} v_{B 1}=m_{A} v_{A 2}+m_{B} v_{B 2}$$

      以及《動能守恒定理》

      $$\frac{1}{2} m_{A} v_{A 1}^{2}+\frac{1}{2} m_{B} v_{B 1}^{2}=\frac{1}{2} m_{A} v_{A 2}^{2}+\frac{1}{2} m_{B} v_{B 2}^{2}$$

      那么它們的碰撞后的速度變化呢?

      image

      維基百科:彈性碰撞 里給出了上面這個(gè)可視化的圖,幫助我們理解速度交互和向量的關(guān)系。

      在根據(jù)上面兩個(gè)公式的基礎(chǔ)上,加入了我們的速度向量,進(jìn)行了很多行的復(fù)雜繁瑣的推導(dǎo),我們得出了碰撞后兩個(gè)小球的最終速度(僅在二維空間有效):

      $$\begin{array}{l} \mathrm{v}_{1}^{\prime}=\mathrm{v}_{1}-\frac{2 m_{2}}{m_{1}+m_{2}} \frac{\left\langle\mathrm{v}_{1}-\mathrm{v}_{2}, \mathrm{x}_{1}-\mathrm{x}_{2}\right\rangle}{\left\|\mathrm{x}_{1}-\mathrm{x}_{2}\right\|^{2}}\left(\mathrm{x}_{1}-\mathrm{x}_{2}\right)\\ \end{array}$$

      $$\begin{array}{l} \mathrm{v}_{2}^{\prime}=\mathrm{v}_{2}-\frac{2 m_{1}}{m_{1}+m_{2}} \frac{\left\langle\mathrm{v}_{2}-\mathrm{v}_{1}, \mathrm{x}_{2}-\mathrm{x}_{1}\right\rangle}{\left\|\mathrm{x}_{2}-\mathrm{x}_{1}\right\|^{2}}\left(\mathrm{x}_{2}-\mathrm{x}_{1}\right) \end{array}$$

      在上面的公式中,雙豎線代表向量的模(長度);尖括號表示向量間的點(diǎn)積; X 是位置向量 \(\vec{v}\) ,里面包含了 x y 軸。

      現(xiàn)在我們的小球還沒有質(zhì)量 M 這個(gè)概念。假設(shè)球的密度穩(wěn)定,我們可以抽象成小球的面積,注意是表面積。表面積的計(jì)算公式為 \(S = 4\pi r^2\) ,在我們 Ball 里搞出這樣一個(gè)方法,來表示球的表面積屬性 :

      get sphereArea(){ return 4 * Math.PI * this.radius ** 2; }  // 計(jì)算球表面積(利用球面積,來表示小球的質(zhì)量)
      

      注意,這里使用了 get 這個(gè)關(guān)鍵字。get 會將返回值變?yōu)橐粋€(gè)屬性,而不加 get 則會以方法的形式來表現(xiàn)。什么意思呢?看一下對比圖:

      // 調(diào)用區(qū)別
      ball.sphereArea  // 使用 get 關(guān)鍵字
      ball.sphereArea()  // 不使用 get 關(guān)鍵字
      

      很顯然,使用 get 關(guān)鍵字更切合我們的使用邏輯。

      然后我們要將其轉(zhuǎn)化為我們的程序。這個(gè)很頭疼,要根據(jù)我們實(shí)現(xiàn)的向量類 Vector 里的向量運(yùn)算方法,一點(diǎn)點(diǎn)復(fù)刻那一大串公式,這是我們復(fù)刻完的函數(shù):

      // 碰撞后速度的計(jì)算函數(shù),參數(shù)為“自己”和“對方”,返回值為計(jì)算好的碰撞后“自己”的速度向量
      const collisionVector = (particle1, particle2) => {
      	return particle1.velocity.subtract(particle1.position
                    .subtract(particle2.position).multiply(particle1.velocity.subtract(particle2.velocity)
                    .dotProduct(particle1.position.subtract(particle2.position))
                    / particle1.position.subtract(particle2.position).magnitude ** 2)
                    .multiply((2 * particle2.sphereArea) / (particle1.sphereArea + particle2.sphereArea))
      	);
      };
      

      這一大坨很難看,完全沒有可讀性,但它很準(zhǔn)確。沒辦法,數(shù)學(xué)公式就是這樣。

      重復(fù)計(jì)算的問題

      很顯然,我們在里面的 for(){} 循環(huán)判斷碰撞時(shí),同一個(gè)碰撞事件會被計(jì)算兩次,所以我們需要為每個(gè)球再創(chuàng)建一個(gè) ID、一個(gè)碰撞數(shù)組,把有碰撞的球都放進(jìn)去,更新計(jì)算時(shí)跳過它。

      1. Ball 類里面為球球們添加兩個(gè)屬性,idcollisions
      Object.assign(this,{
          id: Math.floor(Math.random() * 1000000),  // 根據(jù)隨機(jī)數(shù)生成的 ID
          type : 'circle',
          position : new Vector(100, 100),
          velocity : new Vector(5, 3),
          color : 'blue',
          radius : 25,
          collisions: [],  // 與之碰撞的小球們組成的數(shù)組
      },config);
      
      1. 在 循環(huán)判斷碰撞 語句里,寫上下面的判斷語句:
      // 如果對方小球的 `collisions` 里包含自己的 id,那就跳過 ~
      if (this === actor || this.collisions.includes(actor.id + updateId)) { continue; }
      
      1. 記得在 DisplayState 類里將上面這個(gè)概念傳入。這里不再演示。

      撞擊墻壁定格問題

      另外,如果球同時(shí)撞擊墻壁和另一個(gè)小球,會產(chǎn)生 卡 在墻上不再動的效果(因?yàn)橄乱粠挠?jì)算值超過了邊界),所以我們也要改良一下我們的墻壁碰撞函數(shù):

      /* 碰到墻壁后,反彈 */
      const upperLimit = new Vector(displayState.displayEle.canvas.width - this.radius, displayState.displayEle.canvas.height - this.radius);  // canvas 的右下邊界
      const lowerLimit = new Vector(0 + this.radius, 0 + this.radius);  // canvas 的左上邊界
      if (this.position.x >= upperLimit.x || this.position.x <= lowerLimit.x) {
      	this.velocity = new Vector(-this.velocity.x, this.velocity.y);
      }
      if (this.position.y >= upperLimit.y || this.position.y <= lowerLimit.y) {
      	this.velocity = new Vector(this.velocity.x, -this.velocity.y);
      }
      
      // 墻擠壓發(fā)生在球同時(shí)撞擊墻壁和另一個(gè)球時(shí),球可能會卡在墻上
      // 下面這兩行,通過判斷,能確保球不會卡到墻壁外
      // 確保下一幀,始終在墻內(nèi)(min 計(jì)算右邊界,max 計(jì)算左邊界)
      const newX = Math.max(Math.min(this.position.x + this.velocity.x, upperLimit.x), lowerLimit.x);
      const newY = Math.max(Math.min(this.position.y + this.velocity.y, upperLimit.y), lowerLimit.y);
      return new Ball({ ...this, position: new Vector(newX, newY), });  // 最終生成下一幀數(shù)據(jù)
      

      內(nèi)存問題

      我們每次碰撞,都會跟蹤更新碰撞的數(shù)組,這會導(dǎo)致內(nèi)存增大,如果小球足夠多,則會很快將內(nèi)存耗盡,因此,我們要在適當(dāng)?shù)臅r(shí)候減少 collisions 數(shù)組的元素?cái)?shù)量。

      nextFrameUpdate 的最開始,我們加上這樣一行代碼:

      if (this.collisions.length > 10) { 
          this.collisions = this.collisions.slice(this.collisions.length - 3);  // 刪除無用的 collisions,只保留最后三個(gè)
      }
      

      每當(dāng) collisions 元素的數(shù)量達(dá)到 10 個(gè)以上,就只保留最后三個(gè)元素。

      這樣,我們就基本完成碰撞的檢測和碰撞的效果了 ~ 我們來實(shí)驗(yàn)一下效果吧!

      完整代碼:

      <body></body>
      <script>
          (function() {
              const thisExampleParent = document.body;
              class Canvas {  // 類:畫圖
                  constructor(parent = thisExampleParent, width = 400, height = 400){
                      this.canvas = document.createElement('canvas');
                      this.canvas.width = width;
                      this.canvas.height = height;
                      parent.appendChild(this.canvas);
                      this.ctx = this.canvas.getContext('2d');
                  }
                  sync(state){  // 執(zhí)行下一幀的繪圖(或稱 在畫板上同步已經(jīng)計(jì)算好的下一幀的數(shù)據(jù))
                      this.clearDisplay();
                      this.drawActor(state.actors);
                  }
                  clearDisplay(){  // 清除畫板(以方便繪制下一幀)
                      this.ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
                      this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
                  }
                  drawActor(actors){  // 畫一個(gè)角色,比如畫一個(gè)圓
                      for (const actor of actors) { if(actor.type === 'circle'){ this.drawCircle(actor); } }
                  }
                  drawCircle(actor){  // 畫一個(gè)圓
                      this.ctx.strokeStyle = 'black';
                      this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);  // 畫出邊框
                      this.ctx.beginPath();
                      this.ctx.arc(actor.position.x, actor.position.y, actor.radius, 0, Math.PI * 2);
                      this.ctx.closePath();
                      this.ctx.fillStyle = actor.color;
                      this.ctx.fill();
                  }
              }
              class Ball {  // 類:球類
                  constructor(config){
                      Object.assign(this,{
                          id: Math.floor(Math.random() * 1000000),  // 根據(jù)隨機(jī)數(shù)生成的 ID
                          type : 'circle',
                          position : new Vector(100, 100),
                          velocity : new Vector(5, 3),
                          color : 'blue',
                          radius : 25,
                          collisions: [],  // 與之碰撞的小球們組成的數(shù)組
                      },config);
                  }
                  nextFrameUpdate(displayState, time, updateId){  // 計(jì)算下一幀,小球的位置
                      for (const actor of displayState.actors) {
                          if (this === actor || this.collisions.includes(actor.id + updateId)) { continue; }
                          const distanceNext = this.position.add(this.velocity).subtract(actor.position.add(actor.velocity)).magnitude;
                          if (distanceNext <= this.radius + actor.radius) {
                              const v1 = collisionVector(this, actor);
                              const v2 = collisionVector(actor, this);
                              this.velocity = v1; actor.velocity = v2;
                              this.collisions.push(actor.id + updateId);
                              actor.collisions.push(this.id + updateId);
                          }
                      }
                      /* 碰到墻壁后,反彈 */
                      const upperLimit = new Vector(displayState.displayEle.canvas.width - this.radius, displayState.displayEle.canvas.height - this.radius);
                      const lowerLimit = new Vector(0 + this.radius, 0 + this.radius);
                      if (this.position.x >= upperLimit.x || this.position.x <= lowerLimit.x) {
                          this.velocity = new Vector(-this.velocity.x, this.velocity.y);
                      }
                      if (this.position.y >= upperLimit.y || this.position.y <= lowerLimit.y) {
                          this.velocity = new Vector(this.velocity.x, -this.velocity.y);
                      }
      				
      				// 墻擠壓發(fā)生在球同時(shí)撞擊墻壁和另一個(gè)球時(shí),球可能會卡在墻上
      				// 下面這兩行,通過判斷,能確保球不會卡到墻壁外
                      const newX = Math.max(Math.min(this.position.x + this.velocity.x, upperLimit.x), lowerLimit.x);
                      const newY = Math.max(Math.min(this.position.y + this.velocity.y, upperLimit.y), lowerLimit.y);
                      return new Ball({ ...this, position: new Vector(newX, newY), });  // 最終生成下一幀數(shù)據(jù)
                  }
                  get sphereArea(){ return 4 * Math.PI * this.radius ** 2; }  // 計(jì)算球表面積(利用球面積,來表示小球的質(zhì)量)
              }
              class Vector {  // 類:向量(可作為位置 和 速度)
                  constructor(x, y) { this.x = x; this.y = y; }
                  /* 向量的各種運(yùn)算 */
                  add(vector) {  return new Vector(this.x + vector.x, this.y + vector.y); }  // 加
                  subtract(vector) { return new Vector(this.x - vector.x, this.y - vector.y); }  // 減
                  multiply(scalar) { return new Vector(this.x * scalar, this.y * scalar); }  // 逐元素乘法
                  dotProduct(vector) { return this.x * vector.x + this.y * vector.y; }  // 數(shù)量積
                  get magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2); }  // 求模
                  get direction() { return Math.atan2(this.x, this.y); }  // 求方向的角度 tan
              }
              class DisplayState {  // 類:宏觀狀態(tài)
                  constructor(displayEle, actors) { this.displayEle = displayEle; this.actors = actors; }
                  update(time) {
                      const updateId = Math.floor(Math.random() * 1000000);  // 小球的身份證號(而且還能改,盡量不重復(fù))
                      const new_actors = this.actors.map(actor => { return actor.nextFrameUpdate(this, time, updateId); });
                      return new DisplayState(this.displayEle, new_actors);
                  }
              }
              const collisionVector = (particle1, particle2) => {
                  return particle1.velocity.subtract(particle1.position
                          .subtract(particle2.position).multiply(particle1.velocity.subtract(particle2.velocity)
                          .dotProduct(particle1.position.subtract(particle2.position))
                          / particle1.position.subtract(particle2.position).magnitude ** 2)
                          .multiply((2 * particle2.sphereArea) / (particle1.sphereArea + particle2.sphereArea))
                      );
              };
              // ---------- demo 測試
              const displayEle = new Canvas();
              
              const ball1 = new Ball({
                position: new Vector(40, 100),
                velocity: new Vector(2, 3),
                radius: 20,
                color: 'blue',
              });
              
              const ball2 = new Ball({
                position: new Vector(200, 100),
                velocity: new Vector(-1, 3),
                color: 'red',
              });
      
              const actors = [ball1, ball2];
              
              let displayState = new DisplayState(displayEle, actors);
              function myAnimation(time){  // 注意,這里的 time 是requestAnimationFrame回調(diào),可直接使用,是 秒
                  displayState = displayState.update();  // 數(shù)據(jù)更新
                  displayEle.sync(displayState);     // 根據(jù)更新的數(shù)據(jù)來繪畫
                  requestAnimationFrame(myAnimation);
              }
              myAnimation();
          })();
      </script>
      

      隨機(jī)數(shù)生成多個(gè)小球

      現(xiàn)在,我們就可以寫一個(gè)循環(huán)和隨機(jī)數(shù)結(jié)合的腳本,生成一大堆個(gè)小球,像開頭的那個(gè)動畫一樣的效果了。

      const displayEle = new Canvas();
      // 生成某個(gè)范圍內(nèi)的隨機(jī)數(shù)
      const random = (max = 9, min = 0) => { return Math.floor(Math.random() * (max - min + 1) + min); };
      const colors = ['red', 'green', 'blue', 'purple', 'orange'];  // 可供隨機(jī)挑選的顏色
      const balls = [];
      const count = 30;  // 球的數(shù)量
      for (let i = 0; i < count; i++) {
      	balls.push(new Ball({
      		radius: random(8, 3) + Math.random(),
      		color: colors[random(colors.length - 1)],
      		position: new Vector(random(400 - 10, 10), random(400 - 10, 10)),
      		velocity: new Vector(random(3, -3), random(3, -3)),
      	}));
      }
      let displayState = new DisplayState(displayEle, balls);
      function myAnimation(time){ 
      	displayState = displayState.update();  // 數(shù)據(jù)更新
      	displayEle.sync(displayState);     // 根據(jù)更新的數(shù)據(jù)來繪畫
      	requestAnimationFrame(myAnimation);
      }
      myAnimation();
      

      最后的效果如下面這個(gè)頁內(nèi)框架所示:

      參考資料

      1. https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial
      2. https://gist.github.com/joshuabradley012/bd2bc96bbe1909ca8555a792d6a36e04
      3. https://en.wikipedia.org/wiki/Elastic_collision#Two-dimensional
      4. https://eloquentjavascript.net/16_game.html
      主站蜘蛛池模板: 亚洲国产美国产综合一区| 欧美偷窥清纯综合图区| 亚洲欧洲日产国码AV天堂偷窥| 国产另类ts人妖一区二区| 国产国产午夜福利视频| 亚洲aⅴ男人的天堂在线观看| 免费国产午夜理论片不卡| 国内外成人综合免费视频| 亚洲伊人情人综合网站| 国产乱色熟女一二三四区| 成熟了的熟妇毛茸茸| 亚洲自在精品网久久一区| 南投县| 999福利激情视频| 秋霞av鲁丝片一区二区| 99久久国产一区二区三区| 亚洲成人高清av在线| 国产av综合一区二区三区| 又粗又硬又黄a级毛片| 亚洲高清成人av在线| 久99久热精品免费视频| 亚洲av无码专区在线亚| 五月天天天综合精品无码| 亚洲国产精品自产在线播放| 视频一区二区三区在线视频| 韩国免费a级毛片久久| 玩弄漂亮少妇高潮白浆| 久久亚洲日本激情战少妇| 亚洲区1区3区4区中文字幕码| 国产成人亚洲无码淙合青草| 中文字幕在线无码一区二区三区| 99人中文字幕亚洲区三| 亚洲精品日韩在线观看| 亚洲熟女国产熟女二区三区| 四虎国产精品永久在线国在线| 激情综合网一区二区三区| 国产AV福利第一精品| 久久96国产精品久久久| 亚洲春色在线视频| 免费ā片在线观看| 亚洲成亚洲成网中文字幕|