이전 글 : [Canvas-불꽃놀이-10] 불꽃놀이 꼬리의 위치와 속도 구현
불꽃놀이의 꼬리 최종 완성
이전글에서 꼬리의 위치와 속도를 정의했다.
이번 글에선 TailParticle을 생성하고 세부 값들을 조절하여 화면에 어떻게 구현되는지 확인해 보도록 한다.
1. TailParticle 구현
TailParticle 클래스는 불꽃놀이의 꼬리를 나타내며, 캔버스 하단에서부터 시작해 x의 진동(코사인 주기)과 y의 감속 효과를 결합해 자연스러운 물결 모양으로 올라가도록 구현하였다.
1-1. 생성자와 멤버변수
|
// constants.js
export const TAIL = {
// 생략...
RADIAN: 0, // 라디안 값 초기화
};
class TailParticle extends Particle {
constructor({ ctx, isSmallScreen, x, y, vy }) {
// 부모클래스에 값을 전달
// x, y, vy는 canvas클래스에서 계산된 값을 넘겨받는다.
super({ ctx, isSmallScreen, x, y, vy);
// TailParticle 고유 멤버 변수 초기화
this.initTailParticleVars();
}
initTailParticleVars() {
// x좌표가 업데이트 시 초기 값으로 복귀할 수 있도록 최초 x 좌표 저장
this.initialX = this.x;
// vx계산시 필요한 라디안 초기값
this.radian = TAIL.RADIAN;
}
}
1-2. Draw와 Update 메서드
|
// constants.js
export const TAIL = {
// 생략...
RADIAN_OFFSET: 1,
X_ADJUST_RATE: 0.4,
INITIAL_X_RETURN_RATE: 0.1, // 초기값으로 돌아가는 속도, 작을수록 부드럽게 이동한다.
};
// TailParticle.js
update() {
// 위로 올라갈수록 마찰력(friction)으로 인해 y의 속도가 줄어든다.
this.vy *= this.friction;
// y의 속도(vy)에 따른 x의 좌우진동(cos) 크기(X_ADJUST_RATE)를 계산해 vx에 반영한다.
this.vx = Math.cos(this.radian) * this.vy * TAIL.X_ADJUST_RATE;
// 위로 갈수록 투명도가 감소한다.
this.opacity = -this.vy;
// 라디안값을 일정하게 증가시켜 시간이 지남에 따라 좌우진동 모션이 자연스럽게 보이도록 한다.
this.radian += TAIL.RADIAN_OFFSET;
// x,y 좌표의 위치를 업데이트 한다.
this.updatePosition();
// x 좌표가 속도(vx)에 따라 초기값(initialX)에서 벗어나므로
// 부드럽게 초기값에 복귀하도록 보정값(INITIAL_X_RETURN_RATE)을 추가 (중심 복귀 효과)
this.x += (this.initialX - this.x) * TAIL.INITIAL_X_RETURN_RATE;
}
update 메서드 좀 더 자세한 설명
this.vy *= this.friction;
- friction은 마찰력이다. 부모에 있는 마찰력 값(0.93)을 그대로 쓰므로 y의 속도에 마찰력을 곱하면 속도가 점점 줄어들게 된다.
- 시간이 지날수록 꼬리가 점점 더 천천히 올라가게해 자연스러운 감속효과를 구현한다.
this.vx = Math.cos(this.radian) * this.vy * TAIL.X_ADJUST_RATE;
- Math.cos(this.radian)
-. 코사인 값은 단위원에서 x축 좌표를 나타내므로, x에 대해 진동하는 좌우 흔들림(파동)을 표현하기에 적당하다.
-. 라디안의 초기값이 0이면 cos(0)은 1이므로 오른쪽 끝에서부터 진동이 시작돼 자연스러운 파동을 볼 수 있다.
각도 | 코사인 값 | 그래프 |
cos(0°, 0 라디안) | 1 | |
cos(90°, π/2 라디안) | 0 | |
cos(180°, π 라디안) | -1 | |
cos(270°, 3π/2 라디안) | 0 | |
cos(360°, 2π 라디안) | 1 |
- this.vy
-. vx에 vy를 곱해주는 이유는 y의 속도에 비례해 x의 움직임의 크기(수직방향)도 조절되기 때문이다.
-. vy이 값이 클수록 x의 움직임의 크기도 커지고, 줄어들수록 x의 움직임도 작아져 y와 더불어 자연스럽게 감속하는 효과를 만든다. - TAIL.X_ADJUST_RATE
-. X_ADJUST_RATE는 x의 좌우 흔들림의 폭인 진폭의 크기(수평방향)를 조절하기 때문에 마지막으로 곱해준다.
-. 이 값이 작을 수록 x의 좌우 흔들림이 적고 클수록 좌우 흔들림이 커진다
-. 예) 0.2인 작은 좌우 진동 -> 꼬리가 거의 직선으로 올라가는 느낌 (왼쪽 사진 참고)
-. 예) 0.8인 큰 좌우 진동 -> 꼬리가 강하게 흔들리면서 올라가는 느낌 (오른쪽 사진 참고)
this.opacity = -this.vy;
- 꼬리가 위로 올라갈수록 y의 속도가 점점 감소하니, vy를 투명도로 설정하여 꼬리가 자연스럽게 사라지도록 한다.
- y의 속도는 아래에서 위로 올라가기 때문에 음수이므로, 부호만 바꿔주면 양수가 되어 opacity가 0에 가까워지면서 꼬리를 사라지게 만들 수 있다.
this.radian += TAIL.RADIAN_OFFSET;
- 처음 0에서 시작해 일정 크기를 더해 자연스러운 파동 모양이 될수록 한다. 1 -> 0 -> -1 -> 0 -> 1 순으로 무한히 반복할 수 있게 한다.
1-3. reset 메서드
- 객체 풀링을 사용하면 매번 새로운 객체를 생성하는 것이 아니라 풀에서 재활용하므로 성능을 향상할 수 있다. 그래서 reset 메서드는 객체 풀링을 사용하는 애니메이션에선 필수적으로 실행해야 한다.
- 객체를 풀에 반환할때 이전 상태가 남아있으면 예상치 못한 동작이 발생할 수 있으므로 reset을 통해 모든 멤버 변수를 초기화시켜 줘야 새로운 파티클처럼 동작할 수 있게 해 준다. (+ TailParticle은 고유 멤버 변수를 가지므로 초기 상태를 보장하기 위해 부모 클래스의 초기화와 함께 고유멤버변수도 초기화해야 함)
- 객체를 풀에서 가져와서 사용할 때도 생성 시 상태로 초기화해줘야 하므로 param객체를 통해 공통적으로 사용하는 멤버변수와 TailParticle 만의 고유 멤버변수를 초기화시켜야 활성상태를 만들 수 있다.
// TailParticle.js
reset(params) {
// 부모의 reset 메서드 실행
super.reset(params);
// 위에서 선언한 TailParticle의 고유 멤버변수 초기화
this.initTailParticleVars();
}
2. TailParticle 애니메이션 적용하기
이제 TailParticle 클래스를 완성하였으니 Canvas 클래스에서 애니메이션을 적용시켜 보도록 하겠다.
애니메이션을 적용하기 위해서 파티클 생성과 업데이트가 필요하다.
요약해서 설명하자면,
- createTailParticle 메서드는 새로운 파티클을 생성하거나 풀에서 재사용하며 좌우 균등하게 생성하도록 설정한다.
- updateTailParticle 메서드는 파티클의 상태를 업데이트하며, 투명도에 따라 파티클을 풀에 반납해 효율적으로 관리한다.
2-1. TailParticle 생성
|
- 생성에 필요한 변수 추가와 초기화
class Canvas extends CanvasOption {
constructor() {
super();
this.initCanvasVars();
}
init() {
super.init();
// 생략...
}
initCanvasVars() {
// 생략...
// 꼬리의 개수를 카운트하여 화면에 보여주는 꼬리의 위치를 통제
this.tailCount = 0;
// x좌표 값이 true면 왼쪽 x좌표 배열, false면 오른쪽 x좌표 배열값들 사이에서 랜덤 선택
this.isLeft = false;
// 애니메이션 실행중인 파티클 배열
this.tailParticles = [];
// 파티클 생성, 반납을 관리하는 파티클 매니저 인스턴스 생성
this.pm = new ParticleManager(this.ctx, this.isSmallScreen);
}
// 생략...
}
- TailParticle 생성 메서드 구현
-. x좌표의 위치에 따라 파리마터 값이 달라지므로 공통되는 파라미터를 만든 다음에 조건에 따라 필요한 값들을 대입한다.
-. 중앙을 제외한 x좌표의 위치는 좌, 우 x좌표 배열들 가운데 랜덤하게 선택해서 다양한 패턴을 보여주도록 한다.
// Canvas.js
// 새로운 TailParticle을 생성하거나 풀에서 재활용
// 초기 상태를 설정하며 x좌표와 vy를 랜덤하게 배치
createTailParticle() {
// x좌표와 달리 y좌표는 변하지 않으므로 미리 추가
let params = { y: this.canvasCssHeight };
// 첫번째로 시작될 때는 메인(중앙)에서 꼬리 생성
if (this.tailCount === 0) {
params.x = this.mainX;
params.vy = this.mainTailVY;
// 그 이외는 좌우 위치에 따라 랜덤 생성
} else {
// 좌표 배열 마지막 인덱스
const max = this.tailQty - 1;
// isLeft가 true 이면 왼쪽 좌표 배열, false면 오른쪽 좌표 배열에서 랜덤 선택
params.x = (this.isLeft ? this.tailsLeftPosX : this.tailsRightPosX)[randomInt(0, max)];
// vy도 배열 안에서 랜덤 선택
params.vy = this.tailsVY[randomInt(0, max)];
}
// 활성화된 tailParticle 배열에 새롭게 생성되거나 재사용한 파티클 추가
this.tailParticles.push(this.pm.acquireParticle(TYPE_TAIL, params));
// 새로운 파티클 추가 후 개수를 변경해준다. 배열의 길이를 초과하지 않도록 나머지 연산 적용
this.tailCount = (this.tailCount + 1) % this.tailQty;
// 좌, 우 균등하게 나오도록 isLeft의 값을 반전시킨다.
this.isLeft = !this.isLeft;
}
2-2. TailParticle 업데이트
|
export const TAIL = {
//생략...
// 파티클 배열에서 제외할 투명도 기준, 0이 아닌 이유는 투명도가 vy 값과 동일하기 때문이다.
OPACITY_LIMIT: 0.05,
// 생략...
};
// Canvas.js
// 모든 TailParticle의 상태를 업데이트하고 풀에 반환을 관리
updateTailParticle() {
// 인덱스 밀림을 방지하기 위해 역순으로 순회
for (let i = this.tailParticles.length - 1; i >= 0; i--) {
// 각 파티클의 상태를 업데이트하고 다시 그린다.
const tail = this.tailParticles[i];
tail.update();
tail.draw();
// 파티클의 투명도가 OPACITY_LIMIT 이하면 배열에서 추출하고 다시 풀에 반납한다.
if (tail.opacity <= TAIL.OPACITY_LIMIT) {
this.tailParticles.splice(i, 1);
this.pm.returnToPool(TYPE_TAIL, tail);
}
}
}
2-3. 애니메이션 적용
|
필요 상수 추가
// constants.js
export const SCREEN = {
MAX_DPR: 3,
SMALL_WIDTH: 480,
// 덮을 배경색
BG_RGB: "0, 0, 0", // 기본 배경 rgb
ALPHA_BASE: 0.1, // alpha 베이스 값
ALPHA_OFFSET: 0.02, // alpha 조절 값
SPEED_CONTROL: 10, // 속도 조절 값
ALPHA_CLEANUP: 0.3, // 잔상효과를 지울 alph 값
};
export const ANIMATION = {
FPS: 60,
// 꼬리 생성 간격
TAIL_FPS: 70,
};
전체 화면을 덮을 캔버스 공통 메서드 추가
// CanvasOption.js
fillFullCanvas(color) {
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
애니메이션 메서드에서 꼬리 생성하고 업데이트
- 이전 프레임의 파티클 지우기
-. 이전 프레임의 파티클을 지워야 애니메이션이 매끄럽게 연결되며 상태 변화를 확인할 수 있다. 지우지 않으면 화면에 계속 남아있기 때문이다.
-. 그래서 프레임을 업데이트 할때 fillFullCanvas 메서드로 캔버스를 덮어 이전 프레임의 내용을 지운다.
-. 이때 알파값을 sin을 이용해 동적으로 변화시키는 이유는 잔상의 강약을 주기적으로 변화시켜 부드럽고 자연스러운 시각효과를 연출할 수 있기 때문이다.
-. sin은 주기성을 가진 곡선형태로 값이 변하기 때문에 잔상이 선형적으로 줄어드는것 보다 더 부드럽게 줄어들도록 한다.
// Canvas.js
animateFireworks() {
let then = document.timeline.currentTime;
// 꼬리 생성 기준을 카운트할 변수 (프레임 기반)
let frameCount = 0;
// 화면에 남는 잔상을 제거할 색상값
const bgCleanUp = setRgbaColor(SCREEN.BG_RGB, SCREEN.ALPHA_CLEANUP);
// 꼬리 생성과 화면에 남는 잔상을 제거하기 위한 프레임 생성 간격을 계산한 상수
// 기본 FPS 값에 (글자수 * 간격 조정 값)을 더해 산출한다.
const addFrameCountDivisor = ANIMATION.TAIL_FPS + this.textLength * ANIMATION.TEXT_LEN_MULTIPLIER;
/**
* requestAnimationFrame에 전달할 콜백 함수
* @param {number} now requestAnimationFrame의 timestamp (밀리 초)
*/
const frame = (now) => {
const delta = now - then;
if (delta >= this.interval) {
// TODO: 애니메이션 코드 업데이트
// 캔버스를 배경색으로 채워 이전 프레임의 파티클을 지움
// 기본 알파값과 사인값을 이용하여 이전 파티클들이 부드럽게 사라지도록 설정
const alpha = SCREEN.ALPHA_BASE + SCREEN.ALPHA_OFFSET * Math.sin(frameCount / SCREEN.SPEED_CONTROL);
this.fillFullCanvas(setRgbaColor(SCREEN.BG_RGB, alpha));
// 특정 조건에서 배경에 남아있는 잔상 효과를 지우고 꼬리를 생성
if (frameCount === 0) {
this.fillFullCanvas(bgCleanUp);
this.createTailParticle();
}
// 생성된 꼬리들을 업데이트 한다.
this.updateTailParticle();
// 프레임 카운트를 업데이트 하며, 나머지 연산으로 addFrameCountDivisor 보다 커지지 않도록 제한
// 글자 수에 따라 생성 간격을 조정하여 성능 최적화를 도모
frameCount = (frameCount + 1) % addFrameCountDivisor;
then = now - (delta % this.interval);
}
this.animationId = requestAnimationFrame(frame);
};
this.animationId = requestAnimationFrame(frame);
}
3. 실행 화면
아직 스파크를 추가하기 전이라 잘 안 보일 수 있지만 원하는 효과가 잘 나온 걸 확인할 수 있다.
Github 링크
'Canvas > 불꽃놀이 프로젝트' 카테고리의 다른 글
[Canvas-불꽃놀이-13] TextParticle 구현과 적용 (1) | 2024.11.26 |
---|---|
[Canvas-불꽃놀이-12] text 픽셀 데이터 생성 (1) | 2024.11.25 |
[Canvas-불꽃놀이-10] 불꽃놀이 꼬리의 위치와 속도 구현 (0) | 2024.11.22 |
[Canvas-불꽃놀이-09] 파티클 관리와 객체 폴링 (1) | 2024.11.22 |
[Canvas-불꽃놀이-08] 애니메이션 기본 틀잡기 (0) | 2024.11.20 |