개인노트-인강

개인 노트 정리) Canvas - 1-04. 파티클 애니메이션 시키기

roroism 2023. 5. 22. 21:33

인터랙티브 웹 개발 Canvas 인강 정리

 

# 파티클 애니메이션 시키기

이제 Particle class를 animate 함수 안에서 업데이트를 시키며 움직이게 하겠습니다.

그 전에 requestAnimationFrame 함수를 좀 더 효율적으로 사용하는 방법에 대해 알아보겠습니다.

 

## requestAnimationFrame 와 fps

- 만약 144Hz 모니터라면 requestAnimationFrame 함수는 1초에 144번 실행이 되고, 60Hz 모니터라면 1초에 60번 실행이 됩니다.

- 이것이 의미하는 바는 예를들어 animate 함수안에 x를 1px만큼 이동시키는 코드를 작성한다면, 144Hz 모니터에서는 1초에 144px 만큼 이동하게 되고 60Hz 모니터에서는 60px 만큼 이동하게 됩니다.

- 어떤 모니터를 사용하든 같은 길이만큼 움직이게 하고 싶다면 이는 잘못된 결과를 가져오게됩니다.

 

- FPS란? frame per second의 약자입니다. 초당 프레임 횟수를 의미합니다.

- canvas에 적용하여 해석해 본다면, 1초에 requestAnimationFrame 함수를 몇 번을 실행시킬까라고 보면 됩니다.

- javascript 내장 객체인 date 함수를 이용하면 모니터 주사율에 관계없이 항상 같은 frame의 애니메이션이 나오게됩니다.

 

### 동작 코드

- 아래코드를 실행하면 1초에 60frame으로 파티클이 아래로 1px씩 움직이게 됩니다.

// ...
// canvas를 전체화면으로 초기화
const canvasWidth = innerWidth;
const canvasHeight = innerHeight;
// ...
// 1초에 60frame으로 동작
let interval = 1000 / 60;
let now, delta;
let then = Date.now();

function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;

  if (delta < interval) return;

  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  // y를 1px 이동시키기
  particle.y += 1;
  particle.draw();

  then = now - (delta % interval);
}

animate();

 

## 여러개의 파티클 생성

- for문을 이용하여 여러개의 인스턴스를 생성합니다.

- Math.random을 이용하여 임의의 크기와 임의의 위치에 생성하게 합니다.

- class안에 update() 를 만들어 이 메소드 안에서 파티클 위치값을 바꾸어줍니다.

 

### 전체 코드

const canvas = document.querySelector("canvas");

const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio; // 1. devicePixelRatio 값을 구한 뒤,

// const canvasWidth = 300;
// const canvasHeight = 300;
const canvasWidth = innerWidth;
const canvasHeight = innerHeight;

canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";

// 2. dpr 값을 canvas의 width와 height에 곱해줍니다.
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;

ctx.scale(dpr, dpr); // 3. 그리려는 객체에도 가로와 세로에 dpr값을 각각 곱해줍니다.

// ctx.fillRect(10, 10, 50, 50);

/*
ctx.beginPath();
ctx.arc(100, 100, 50, 0, (Math.PI / 180) * 360);
ctx.fillStyle = "red";
ctx.fill(); // 안에 색상을 채워줍니다.
// ctx.stroke(); // 선을 그립니다.
ctx.closePath(); // 100, 100 위치에 반지름이 50인 원이 그려지게 됩니다.
*/

class Particle {
  constructor(x, y, radius) {
    this.x = x;
    this.y = y;
    this.radius = radius;
  }
  update() {
    this.y += 1;
  }
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, (Math.PI / 180) * 360);
    ctx.fillStyle = "orange";
    ctx.fill();
    ctx.closePath();
  }
}

const TOTAL = 5;
const randomNumBetween = (min, max) => {
  return Math.random() * (max - min + 1) + min;
};
let particles = [];

for (let i = 0; i < TOTAL; i++) {
  const x = randomNumBetween(0, canvasWidth);
  const y = randomNumBetween(0, canvasHeight);
  const radius = randomNumBetween(50, 100);
  const particle = new Particle(x, y, radius);
  particles.push(particle);
}

console.log(particles);

// 1초에 60frame으로 동작
let interval = 1000 / 60;
let now, delta;
let then = Date.now();

function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;

  if (delta < interval) return;

  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  particles.forEach((particle) => {
    particle.update();
    particle.draw();
  });

  then = now - (delta % interval);
}

animate();

 

## 속도값도 랜덤으로 적용

// ...
class Particle {
  constructor(x, y, radius, vy) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.vy = vy;
  }
  update() {
    this.y += this.vy;
  }
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, (Math.PI / 180) * 360);
    ctx.fillStyle = "orange";
    ctx.fill();
    ctx.closePath();
  }
}

const TOTAL = 5;
const randomNumBetween = (min, max) => {
  return Math.random() * (max - min + 1) + min;
};
let particles = [];

for (let i = 0; i < TOTAL; i++) {
  const x = randomNumBetween(0, canvasWidth);
  const y = randomNumBetween(0, canvasHeight);
  const radius = randomNumBetween(50, 100);
  const vy = randomNumBetween(1, 5);
  const particle = new Particle(x, y, radius, vy);
  particles.push(particle);
}

// ...

 

## 파티클이 사라졌을 때 다시 생성하게 만들기

- 생각보다 쉽습니다. y 값이 canvas바닥위치보다 크면 y 값을 0으로 변경해줍니다.

// ...
function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;

  if (delta < interval) return;

  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  particles.forEach((particle) => {
    particle.update();
    particle.draw();

    if (particle.y - particle.radius > canvasHeight) {
      particle.y = -particle.radius;
    }
  });

  then = now - (delta % interval);
}

animate();

 

## 다시 생성할 때 생성위치x와 반지름, 속도도 랜덤하게 만들기.

// ...
function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;

  if (delta < interval) return;

  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  particles.forEach((particle) => {
    particle.update();
    particle.draw();

    if (particle.y - particle.radius > canvasHeight) {
      particle.y = -particle.radius;
      particle.x = randomNumBetween(0, canvasWidth);
      particle.radius = randomNumBetween(50, 100);
      particle.vy = randomNumBetween(1, 5);
    }
  });

  then = now - (delta % interval);
}

animate();

 

## 최종 전체 코드

const canvas = document.querySelector("canvas");

const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio; // 1. devicePixelRatio 값을 구한 뒤,

const canvasWidth = innerWidth;
const canvasHeight = innerHeight;

canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";

// 2. dpr 값을 canvas의 width와 height에 곱해줍니다.
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;

ctx.scale(dpr, dpr); // 3. 그리려는 객체에도 가로와 세로에 dpr값을 각각 곱해줍니다.

class Particle {
  constructor(x, y, radius, vy) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.vy = vy;
  }
  update() {
    this.y += this.vy;
  }
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, (Math.PI / 180) * 360);
    ctx.fillStyle = "orange";
    ctx.fill();
    ctx.closePath();
  }
}

const TOTAL = 20;
const randomNumBetween = (min, max) => {
  return Math.random() * (max - min + 1) + min;
};
let particles = [];

for (let i = 0; i < TOTAL; i++) {
  const x = randomNumBetween(0, canvasWidth);
  const y = randomNumBetween(0, canvasHeight);
  const radius = randomNumBetween(50, 100);
  const vy = randomNumBetween(1, 5);
  const particle = new Particle(x, y, radius, vy);
  particles.push(particle);
}

console.log(particles);

// 1초에 60frame으로 동작
let interval = 1000 / 60;
let now, delta;
let then = Date.now();

function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;

  if (delta < interval) return;

  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  particles.forEach((particle) => {
    particle.update();
    particle.draw();

    if (particle.y - particle.radius > canvasHeight) {
      particle.y = -particle.radius;
      particle.x = randomNumBetween(0, canvasWidth);
      particle.radius = randomNumBetween(50, 100);
      particle.vy = randomNumBetween(1, 5);
    }
  });

  then = now - (delta % interval);
}

animate();