개인 노트 정리) Canvas - 1-04. 파티클 애니메이션 시키기
인터랙티브 웹 개발 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();