개인 노트 정리) Canvas - 2-02. 파티클과 폭죽의 기본 원리
인터랙티브 웹 개발 Canvas 불꽃놀이 인강 정리
# 파티클과 폭죽의 기본 원리
1. js 폴더 안에 파티클을 만들기 위한 파티클 클래스를 만듭니다.Particle.js
import CanvasOption from "./CanvasOption.js";
export default class Particle extends CanvasOption {
constructor(x, y) {
super();
this.x = x;
this.y = y;
}
update() {}
draw() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, 10, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.closePath();
}
}
2. 보일러 플레이트의 Canvas 클래스안에 파티클을 만들기 위한 새로운 함수를 정의합니다.
class Canvas extends CanvasOption {
// ...
init() {
// ...
this.createParticles();
}
createParticles() {
const PARTICLE_NUM = 1;
for (let i = 0; i < PARTICLE_NUM; i++) {
const x = 300;
const y = 300;
this.particles.push(new Particle(x, y));
}
}
// ...
}
3. 파티클이 만들어졌으니 이제 render에서 그려줍니다.
class Canvas extends CanvasOption {
// ...
render() {
// ...
this.particles.forEach((particle) => {
particle.update();
particle.draw();
});
// ...
}
}
4. 중력효과가 적용된 파티클을 만들기 위해 Particle클래스의 업데이트 함수 안에 this.y += 1 를 추가하면 원 모양의 파티클이 아래로 움직이는 것처럼 보이지 않고, 긴 선이 그려지듯이 보입니다. 이유는 기존에 그려진 그림을 지우지 않았기 때문입니다. 그래서 기존의 canvas를 지우기위해 render 함수에 파티클이 지나간 자리를 지우는 코드(clear)를 작성할 수도 있지만 이번에는 색을 채우는(fill) 코드를 이용하여 색을 칠해서 지우겠습니다. 이렇게 배경색을 지정하는 코드도 CanvasOption 클래스 안에 넣어줍니다.
CanvasOptoin.js
export default class CanvasOption {
constructor() {
// ...
this.bgColor = '#000000';
}
}
index.js
render() {
// ...
this.ctx.fillStyle = this.bgColor;
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
this.particles.forEach((particle) => {
particle.update();
particle.draw();
});
// ...
}
Particle.js
export default class Particle extends CanvasOption {
// ...
update() {
this.y += 1;
}
draw() {
this.ctx.fillStyle = '#fff';
// ...
}
}
여기까지 구현하면 중력효과가 적용된 원 형태의 파티클이 완성되었습니다.
5. 점의 위치를 랜덤하게 바꿔줍니다. 랜덤 숫자를 만들어주는 유틸성 함수가 필요합니다.
randomNumBetween.js
export const randomNumBetween = (min, max) => {
return Math.random() * (max - min) + min;
};
index.js
// ...
createParticles() {
const PARTICLE_NUM = 1;
for (let i = 0; i < PARTICLE_NUM; i++) {
const x = randomNumBetween(0, this.canvasWidth);
const y = randomNumBetween(0, this.canvasHeight);
this.particles.push(new Particle(x, y));
}
}
// ...
6. 공을 한 개가 아닌 10개를 생성해봅니다.
// ...
createParticles() {
const PARTICLE_NUM = 10;
for (let i = 0; i < PARTICLE_NUM; i++) {
const x = randomNumBetween(0, this.canvasWidth);
const y = randomNumBetween(0, this.canvasHeight);
this.particles.push(new Particle(x, y));
}
}
// ...
공이 각각 랜덤한 위치에 생성이 됩니다.
여기서 불꽃놀이 이펙트는 폭발하는 한 점에서 동시에 시작되서 사방으로 퍼져야 합니다.
x, y가 생성이 될 때 처음에만 한 점에서 시작이 되고, 속도를 각각 다르게 된다면 불꽃이 퍼져나가는 연출이 나올 것 같습니다.
7. 여러개의 파티클들이 처음 시작은 한점에서 모여서 시작하기 위해 x와 y값을 for문에서 밖으로 꺼냅니다.
createParticles() {
const PARTICLE_NUM = 10;
const x = randomNumBetween(0, this.canvasWidth);
const y = randomNumBetween(0, this.canvasHeight);
for (let i = 0; i < PARTICLE_NUM; i++) {
this.particles.push(new Particle(x, y));
}
}
8. 랜덤 속도를 적용하기 위해 각각의 Paricle 클래스에 랜덤속도 x와 y를 보내줍니다.
// ...
createParticles() {
const PARTICLE_NUM = 10;
const x = randomNumBetween(0, this.canvasWidth);
const y = randomNumBetween(0, this.canvasHeight);
for (let i = 0; i < PARTICLE_NUM; i++) {
const vx = randomNumBetween(-5, 5);
const vy = randomNumBetween(-5, 5);
this.particles.push(new Particle(x, y, vx, vy));
}
}
// ...
Paricle.js
export default class Particle extends CanvasOption {
constructor(x, y, vx, vy) {
super();
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
update() {
this.x += this.vx;
this.y += this.vy;
// this.y += 1;
}
// ...
여기까지 진행하면 여러 방향과 다양한 속도 값을 가지고 한 점에 모인 10개의 파티클들이 사방으로 퍼져 나가게 됩니다.
하지만 문제가 몇 가지가 있습니다.
1. 파티클이 화면 밖으로 나가도 render 함수 안에서 보이지도 않는 2000개의 파티클에 애니메이션 연산 update와 draw를 하고 있다는 점입니다. 화면에서만 보이지 않을 뿐, cpu 연산은 계속 돌아가고 있는 중입니다. 그래서 화면에서 사라진 파티클들을 지우지 않는다면 나중에 성능에 큰 문제가 될 것입니다.
2. 파티클의 수를 2000개로 설정하고 퍼져나가는 모양을 보면, 폭죽이 원으로 퍼져나가는 것이 아니라 네모난 상자 모양으로 퍼져나가고 있는 것을 확인할 수 있습니다.
9. 문제점 1 해결하기
파티클에 opacity를 적용 하여 터지고 난 후 opacity를 점점 줄여가며 0으로 됐을 때, this.paticles 배열에서 지워주는 방법으로 해결합니다. 그러면 시각적으로나 내부적으로나 파티클이 완전히 사라질 것입니다.
9-1. 먼저, Particle 클래스 안에 this.opacity 를 만들어 줍니다. 처음에는 1로 초기화하고 update에서 점차 감소시켜줍니다. 그리고 opacity를 적용시켜주기 위해 fillStyle rgba 값을 수정해줍니다.
export default class Particle extends CanvasOption {
constructor(x, y, vx, vy) {
super();
// ...
this.opacity = 1;
}
update() {
// ...
this.opacity -= 0.01;
}
draw() {
this.ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
// ...
}
// ...
9-2. 이제 배열에서 완전히 제거해 줍니다.
render() {
// ...
this.particles.forEach((particle, index) => {
particle.update();
particle.draw();
if (particle.opacity < 0) this.particles.splice(index, 1);
});
// ...
}