언어/자바스크립트

Canvas를 이용한 animation(빗방울효과)

youngble 2023. 11. 12. 12:46

 

 

사용자의 컴퓨터마다 fps(hz)가 다르기떄문에 animate를 실행하여 1초마다 y값을 1씩 아래로 이동하게될경우 어떤 컴퓨터는 빠르고 어떤 컴퓨너튼 느리게 움직이게 되서 같은 유저 경험을 할수가없다. 이를 방지하기위해

아래 표처럼 interval과 delta, date를 이용하여 fps 값을 구하고 해당 delta값이 interval 보다 클때마다 동작하도록 구현한다. 아래의 표를 보며 fps에 따라 화면에 그려주는 밀도를 확인해보자. requestAnimationFrame 메서드를 사용하는것을 참고

 

 

해당 구현코드

class Particle {
  constructor(x, y, radius) {
    this.x = x;
    this.y = y;
    this.radius = radius;
  }

  draw() {
    // ctx.fillRect(10, 10, 50, 50);
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, (Math.PI / 180) * 360);
    ctx.fillStyle = 'red';
    ctx.fill();
    // ctx.stroke();
    ctx.closePath();
  }
}

const x = 100;
const y = 100;
const radius = 50;

const particle = new Particle(x, y, radius);

let interval = 1000 / 60; // 60 fps
let now, delta;
let then = Date.now();

function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;
  particle.y += 1;
  if (delta < interval) {
    return;
  }
  ctx.clearRect(0, 0, canvas.width, canvasHeight); // 매번 다시 그리기위해 클리어해줌

  particle.draw();

  then = now - (delta % interval);
}

animate();

 

구현화면

이를 활용하여 여러개의 구를 생성하고 랜덤한 위치와 속도로 애니메이션 효과를 주면 다음과 같다.

 

가속도

위에 처럼 일정한 속도는 물제가 실제 떨어지는 느낌이없으므로 마치 중력이 있는 느낌으로 acc변수를 만들어 update 함수에서

기존 속도 * 가속도 를 y위치에 매번 update하도록 수정한다.

 

// Particle 클래스 일부
...
constructor(x, y, radius, vy) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.vy = vy;
    this.acc = 1.03; // 가속도
  }
  update() {
    this.vy *= this.acc;
    this.y += this.vy;
  }
...

 

구현모습

 

 

통합 구현 코드

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

const ctx = canvas.getContext('2d'); // 우리가 그릴 도구

const dpr = window.devicePixelRatio;
const canvasWidth = innerWidth;
const canvasHeight = innerHeight;

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

canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;
ctx.scale(dpr, dpr); // dpr에 따라 스케일 조절

class Particle {
  constructor(x, y, radius, vy) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.vy = vy;
    this.acc = 1.03;
  }
  update() {
    this.vy *= this.acc;
    this.y += this.vy;
  }
  draw() {
    // ctx.fillRect(10, 10, 50, 50);
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, (Math.PI / 180) * 360);
    ctx.fillStyle = 'red';
    ctx.fill();
    // ctx.stroke();
    ctx.closePath();
  }
}

const x = 100;
const y = 100;
const radius = 50;

// const particle = new Particle(x, y, radius);
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);
}

let interval = 1000 / 60; // 60 fps
let now, delta;
let then = Date.now();

function animate() {
  window.requestAnimationFrame(animate);
  now = Date.now();
  delta = now - then;
  particles.forEach((particle) => particle.update());
  if (delta < interval) {
    return;
  }
  ctx.clearRect(0, 0, canvas.width, canvasHeight); // 매번 다시 그리기위해 클리어해줌

  particles.forEach((particle) => {
    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();