顯示具有 canvas 標籤的文章。 顯示所有文章
顯示具有 canvas 標籤的文章。 顯示所有文章

2019年6月3日 星期一

Javascript Canvas - 物理球碰撞 (含重力加速度)

Javascript Canvas - 物理球碰撞 (含重力加速度) 的分享
其中用到了二維非彈性碰撞物理公式
假設每個球的質量都一樣,
設置了牆壁和球的恢復係數 (coefficient of restitution) ,
在空白處按滑鼠可以產生出一個新的球

https://jsfiddle.net/hugogo7646/ep4x608d/



html:
<canvas></canvas>
==================
CSS:
canvas {
  display: block;
  width: 100%;
  height: 100%;
  border: 1px solid blue;
}
==================
Javascript:
//Class definition - Start
function Ball(x, y) {
  this.x = x;
  this.y = y;
  this.r = 10;
  this.vx = 0; //Math.random(10);
  this.vy = 0; //Math.random(10);
  this.ax = 0;
  this.ay = g;
  this.color_R = Math.round(Math.random() * 255);
  this.color_G = Math.round(Math.random() * 255);
  this.color_B = Math.round(Math.random() * 255);
}
Ball.prototype = {
  draw: function() {
    this.vx += this.ax;
    this.vy += this.ay;     
    this.x += this.vx;
    this.y += this.vy;

//if ball hit with border
    if (isTopWallCollision(this)) {
    this.y = this.r;
    this.vy *= -1 * restitutionCoefficient_of_border
    }
    if (isBottomWallCollision(this)) {
    this.y = canvas.height - this.r;
    this.vy *= -1 * restitutionCoefficient_of_border
    }
    if (isLeftWallCollision(this)) {
    this.x = this.r;
    this.vx *= -1 * restitutionCoefficient_of_border
    }
    if (isRightWallCollision(this)) {
    this.x = canvas.width - this.r;
    this.vx *= -1 * restitutionCoefficient_of_border
    }
 
    context.beginPath();
    context.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
    context.fillStyle = 'rgba(' + this.color_R + ', ' + this.color_G + ', ' + this.color_B + ', 1)';
    context.fill()
  }
}
//Class definition - End

//Config - Start //
var ballCount = 30;
var ballList = [];
var g = 0.1;
var restitutionCoefficient_of_border = 0.9;
var restitutionCoefficient_of_ball = 0.9;
//Config - End //

var canvas = document.querySelectorAll("canvas")[0];
var context = canvas.getContext("2d");

//Create balls
for (var i = 0; i < ballCount; i++) {
  var isNewBallCollision = false;
  var newBall;
  do {
    newBall = new Ball(Math.random() * canvas.width, Math.random() * canvas.height);
    isNewBallCollision = isWallCollision(newBall);
    if (isNewBallCollision){
    continue;
    }
    for (var j = 0; j < ballList.length; j++) {
      isNewBallCollision = isBallCollision(ballList[j], newBall);
      if (isNewBallCollision) {
        break;
      }
    }
  } while (isNewBallCollision);

  ballList.push(newBall);
}

draw();

canvas.addEventListener('click', function(e) {
  var x = event.clientX;
  var y = event.clientY;
  var rect = this.getBoundingClientRect();
  //Recalculate mouse offsets to relative offsets
  x -= rect.left;
  y -= rect.top;
  //Also recalculate offsets of canvas is stretched
  //changes coordinates by ratio
  x *= this.width / this.clientWidth;
  y *= this.height / this.clientHeight;
  ballList.push(new Ball(x, y));
})

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = 0; i < ballList.length; i++) {
    ballList[i].draw();
    for (var j = i + 1; j < ballList.length; j++) {
      if (isBallCollision(ballList[i], ballList[j])) {
        ballCollision(ballList[i], ballList[j]);
      }
    }
  }
  requestAnimationFrame(draw);
}

function isBallCollision(ball1, ball2) {
  return Math.pow(ball1.x - ball2.x, 2) + Math.pow(ball1.y - ball2.y, 2) <= Math.pow(ball1.r + ball2.r, 2)
}
//Wall Collision functions - Start
function isWallCollision(ball) {
  return isTopWallCollision(ball) || isBottomWallCollision(ball) || isLeftWallCollision(ball) || isRightWallCollision(ball);
}

function isTopWallCollision(ball) {
  return (ball.y - ball.r) <= 0;
}

function isBottomWallCollision(ball) {
  return (ball.y + ball.r) >= canvas.height;
}

function isLeftWallCollision(ball) {
  return (ball.x - ball.r) <= 0;
}

function isRightWallCollision(ball) {
  return (ball.x + ball.r) >= canvas.width;
}
//Wall Collision functions - End

function ballCollision(ball1, ball2) {
  var clooisionVectorLength = Math.sqrt(Math.pow((ball1.x - ball2.x), 2) + Math.pow((ball1.y - ball2.y), 2));
  var collisionX = (ball1.x - ball2.x) / clooisionVectorLength;
  var collisionY = (ball1.y - ball2.y) / clooisionVectorLength;

  var collision_ball1_x = Math.pow(collisionX, 2) * ball1.vx + collisionX * collisionY * ball1.vy;
  var collision_ball1_y = collisionX * collisionY * ball1.vx + Math.pow(collisionY, 2) * ball1.vy;

  var collision_ball2_x = Math.pow(collisionX, 2) * ball2.vx + collisionX * collisionY * ball2.vy;
  var collision_ball2_y = collisionX * collisionY * ball2.vx + Math.pow(collisionY, 2) * ball2.vy;

  ball1.vx = collision_ball2_x + (ball1.vx - collision_ball1_x);
  ball1.vy = collision_ball2_y + (ball1.vy - collision_ball1_y);
  ball2.vx = collision_ball1_x + (ball2.vx - collision_ball2_x);
  ball2.vy = collision_ball1_y + (ball2.vy - collision_ball2_y);

  ball1.vx *= restitutionCoefficient_of_ball;
  ball1.vy *= restitutionCoefficient_of_ball;
  ball2.vx *= restitutionCoefficient_of_ball;
  ball2.vy *= restitutionCoefficient_of_ball;

  ball1.x += collisionX * (ball1.r + ball2.r - clooisionVectorLength);
  ball1.y += collisionY * (ball1.r + ball2.r - clooisionVectorLength);
  ball2.x += -1 * collisionX * (ball1.r + ball2.r - clooisionVectorLength);
  ball2.y += -1 * collisionY * (ball1.r + ball2.r - clooisionVectorLength);
}

2019年5月2日 星期四

Javascript - Canvas 文字粒子特效

今天要來演示如何利用Javascript的canvas來制作文字子特效,
成品效果就像下面這樣,只要在<input>裡面打字,下面的canvas就會出現相應的粒子特效:
其他別人做的更厲害的版本可以參考這裡:
Text particle

不過不管是簡單還是複雜酷炫的版本,其基本原理都相同,就是利用了
Javascript的canvas,繪制了文字之後,擷取繪制了文字的canvas各pixel顏色和透明度資訊 (RGBA),進行處理後再依我們想要的效果再次重繪。

下面就來進行說明:
  1. 建立畫布
    在頁面中建立一個<canvas>用來當做畫布
    <canvas width="500" hiehgt="500"></canvas>
  2. 取得canvas物件後先繪制文字(text)上去
    var canvas = document.querySelectorAll("canvas")[0];
    var context = canvas.getContext("2d");
    context.textAlign = "center";
    context.font = "100px arial";
    context.fillText(text, 200, 100);
  3. 取得canvas的RGBA資訊
     var imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
    其中data是一個類陣列物件,每四個一組分別代表canvas各pixel位置 Red, Green, Blue, Alpha 的值 (0~255),
    例如 {R1, G1, B1, A1, R2, G2, B2 ,A2, R3, G3, B3, A3, ..........}
    以此類推,pixel位置由canvas的由左至右,由上至下。
    有了RGBA資訊後,我們就能來進行自已想要的粒子化處理了
  4. 粒子化處理
    再處理前,先把canvas 清空
    context.clearRect(0, 0, canvas.width, canvas.height);
    接著對data取得每四組的RGBA值,其中我們不想拿取每個pixel的RGBA,
    只想取得間隔為5 pixel (sampleRate ) 的 RGBA資訊,
    並且檢查透明度 (Alpha),如果大於0的話,我們就用canvas繪制一個圓在相應的位置上。

    如此一來,就會呈現原來的文字被間隔取樣重繪成圓圈的子效果了。

    var sampleRate = 5;
      for (var j = 0; j < canvas.height; j += sampleRate) {
        for (var i = 0; i < canvas.width; i += sampleRate) {
          //Get RGBA data
          var red = imageData[(i + j * canvas.width) * 4];
          var green = imageData[(i + j * canvas.width) * 4 + 1];
          var blue = imageData[(i + j * canvas.width) * 4 + 2];
          var alpha = imageData[(i + j * canvas.width) * 4 + 3];
          if (alpha > 0) { //If alpha > 0, draw circle
            context.beginPath();
            //context.strokeStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
            context.fillStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
            context.arc(i, j, 2, 0, 2 * Math.PI);
            context.fill();
          }
        }
      }
下面是完整的程式碼 :
Html :
<div>Please insert text here to see partical special effect.</div>
<div><input type="text" /></div>
<div><canvas width="500" hiehgt="500"></canvas></div>

Javascript :
//Text to draw
var sampleText = "👚👕 H";
drawParticleText(sampleText);

document.querySelectorAll("input")[0].addEventListener("input", function(event) {
 drawParticleText( this.value ? this.value : sampleText);
});

function drawParticleText(text) {
  var x = 200;
  var y = 100;
  var canvas = document.querySelectorAll("canvas")[0];
  var context = canvas.getContext("2d");
  
  //clear canvas first
  context.clearRect(0, 0, canvas.width, canvas.height);
  
  //Draw text first
  context.textAlign = "center";
  context.font = "100px arial";
  context.fillText(text, 200, 100);
  //Get canvas data (RGB and alpha),
  //Data in imageData : [(r, g, b, a) of 1st place, (r, g, b, a) of 2nd place......]
  //from left to right, from top to bottom
  var imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
  //Clear drawing
  context.clearRect(0, 0, canvas.width, canvas.height);

  //Sampling rate
  var sampleRate = 5;
  for (var j = 0; j < canvas.height; j += sampleRate) {
    for (var i = 0; i < canvas.width; i += sampleRate) {
      //Get RGBA data
      var red = imageData[(i + j * canvas.width) * 4];
      var green = imageData[(i + j * canvas.width) * 4 + 1];
      var blue = imageData[(i + j * canvas.width) * 4 + 2];
      var alpha = imageData[(i + j * canvas.width) * 4 + 3];
      if (alpha > 0) { //If alpha > 0, draw circle
        context.beginPath();
        //context.strokeStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
        context.fillStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
        context.arc(i, j, 2, 0, 2 * Math.PI);
        context.fill();
      }
    }
  }
}

CSS (加框只是方便觀察):
canvas {
  border: 1px solid blue;
}

參考資料:

  1. 随便谈谈用canvas来实现文字图片粒子化
  2. 《每周一点canvas动画》—— 文字粒子
  3. Text particle