이전 글 : [Canvas-불꽃놀이-15] CircleParticle 구현과 업데이트
불꽃놀이에 잔상 효과를 적용해 생동감 더해주기
앞서 완성한 불꽃놀이를 보면 움직임은 있으나 잔상이 없어서 정적인 느낌이 강하고 단조로워 보였다.
이에 각 파티클에 붙는 잔상 효과를 추가하여 움직임이 좀 더 자연스럽고 생동감 있는 불꽃놀이를 만들어보기로 했다.
1. SparkParticle 클래스 구현
- SparkParticle은 파티클의 이동 경로를 따라 잔상을 남기는 역할을 한다. 불꽃의 생동감을 추가하고 더 현실적인 애니메이션 효과를 추가한다. 이에 자연스러운 효과를 내기 위한 목적으로 대부분의 속성은 외부에서 공급받는다.
- 빠르게 사라지므로 마찰력은 적용하지 않고 속도만 기존 파티클에 맞춰 따라가도록 업데이트하도록 한다.
- 파티클의 투명도와 반지름을 점점 감소시겨 자연스럽게 사라지도록 한다.
1-1. 생성자
- SparkParticle의 속성은 잔상 효과를 추가할 파티클에 맞춰 설정해야 자연스러운 효과를 낼 수 있으므로 외부에서 공급받는다.
class SparkParticle extends Particle {
/**
* 불꽃 놀이 잔상 효과
* @param {object} params
* @param {CanvasRenderingContext2D} params.ctx
* @param {boolean} params.isSmallScreen
* @param {number} params.x - x좌표 위치
* @param {number} params.y - y좌표 위치
* @param {number} params.vx - x좌표 속도
* @param {number} params.vy - y좌표 속도
* @param {number} params.radius - 반지름
* @param {number} params.opacity - 투명도
* @param {string} params.color - 파티클 색상
*/
constructor({ ctx, isSmallScreen, x, y, vx, vy, radius, opacity, color }) {
super({ ctx, isSmallScreen, x, y, vx, vy, radius, opacity, color });
}
}
1-2. draw와 update
- 부모의 draw 메서드를 사용하여 파티클을 그린다.
- 자연스럽게 사라지게 하기 위해 반지름과 투명도를 감소시킨다.
부모의 updatePosition 메서드를 사용하여 x, y의 위치를 업데이트 시킨다.
// constants.js
export const SPARK = {
OPACITY_ADJUST_RATE: 0.016, // 투명도 조정 비율
RADIUS_ADJUST_RATE: 0.99, // 반지름 조정 비율
};
// SparkParticle.js
update() {
// opacity와 radius를 감소시킨다.
this.opacity -= SPARK.OPACITY_ADJUST_RATE;
this.radius *= SPARK.RADIUS_ADJUST_RATE;
// x, y 좌표의 위치를 업데이트 시긴다.
super.updatePosition();
}
2. 잔상 효과 생성하기
- 잔상 효과는 꼬리와 원형 효과에 적용하도록 한다. 텍스트는 제외한다. 잔상효과 때문에 오히려 텍스트의 선명함이 떨어지고 지저분해 보이기 때문이다.
- 애니메이션 시 활성화 되는 스파크 배열을 멤버변수로 추가한다.
// Canvas.js
initCanvasVars() {
// 생략...
// 스파크 파티클 활성화 배열
this.sparkParticles = [];
// 생략...
}
2-1. 꼬리에 SparkParticle 추가하기
- 잔상 효과를 추가하려면 현재 꼬리의 상태를 보고 추가해야 하므로 업데이트 메서드 내 추가하도록 한다.
- SparkParticle의 개수는 꼬리의 속도에 비례해 추가한다.
// constants.js
export const SPARK = {
// 생략...
TAIL_CREATION_RATE: 0.35, // 꼬리 생성 비율
TAIL_MIN_VX: -0.1, // x좌표 속도 최저, 최고
TAIL_MAX_VX: 0.1,
TAIL_MIN_VY: -0.1, // y좌표 속도 최저, 최고
TAIL_MAX_VY: 0.05,
TAIL_MIN_OPACITY: 0.3, // 투명도 최저, 최고
TAIL_MAX_OPACITY: 0.5,
}
// Canvas.js
updateTailParticle() {
for (let i = this.tailParticles.length - 1; i >= 0; i--) {
const tail = this.tailParticles[i];
tail.update();
tail.draw();
// 꼬리 속도에 비례해서 파티클 개수 생성
const sparkQty = Math.round(Math.abs(tail.vy * SPARK.TAIL_CREATION_RATE));
for (let i = 0; i < sparkQty; i++) {
// tail의 위치와 색상을 그대로 사용하고
// vx,vy, opacity는 꼬리를 따라가기 적당한 값으로 설정한다.
const sparkParams = {
x: tail.x,
y: tail.y,
vx: randomFloat(SPARK.TAIL_MIN_VX, SPARK.TAIL_MAX_VX),
vy: randomFloat(SPARK.TAIL_MIN_VY, SPARK.TAIL_MAX_VY),
opacity: randomFloat(SPARK.TAIL_MIN_OPACITY, SPARK.TAIL_MAX_OPACITY),
color: tail.fillColor,
};
this.sparkParticles.push(this.pm.acquireParticle(TYPE_SPARK, sparkParams));
}
if (tail.belowOpacityLimit(TAIL.OPACITY_LIMIT)) {
this.createTextParticle(tail.x, tail.y);
this.createCircleParticle(tail.x, tail.y);
this.tailParticles.splice(i, 1);
this.pm.returnToPool(TYPE_TAIL, tail);
}
}
}
2-2. 원형 효과에 SparkParticle 추가하기
- 원형 효과도 똑같이 업데이트 메서드 내 SparkParticle을 생성하도록 한다.
- SparkParticle의 개수는 랜덤값에 글자 수를 곱한 값 이하로 생성해 자연스럽게 보이도록 한다.
// constants.js
export const SPARK = {
// 생략...
CIRCLE_CREATION_RATE: 0.025, // 스파크 생성율
CIRCLE_MIN_VX: -0.08, // x좌표 속도 최저, 최고
CIRCLE_MAX_VX: 0.08,
CIRCLE_VY: 0.1, // y좌표 속도
CIRCLE_OPACITY_OFFSET: 0.1, // 투명도 조절값
};
// Canvas.js
updateCircleParticle() {
for (let i = this.circleParticles.length - 1; i >= 0; i--) {
const circle = this.circleParticles[i];
circle.update();
circle.draw();
// 글자 길이별 랜덤값에 따라 스파크 생성
if (Math.random() < SPARK.CIRCLE_CREATION_RATE * this.textLength) {
const sparkParams = {
x: circle.x, // CircleParticle의 x, y위치 전달
y: circle.y,
vx: randomFloat(SPARK.CIRCLE_MIN_VX, SPARK.CIRCLE_MAX_VX), // x좌표 속도
vy: SPARK.CIRCLE_VY, // y좌표 속도
color: circle.fillColor, // CircleParticle 색상 전달
radius: circle.radius, // CircleParticle 반지름 전달
opacity: circle.opacity + SPARK.CIRCLE_OPACITY_OFFSET, // CircleParticle 투명도에 조정값 더해 전달
};
// 스파크 생성 또는 재활용하여 스파크 활성화 배열에 추가
this.sparkParticles.push(this.pm.acquireParticle(TYPE_SPARK, sparkParams));
}
if (circle.belowOpacityLimit() || this.isOutOfCanvasArea(circle.x, circle.y)) {
this.circleParticles.splice(i, 1);
this.pm.returnToPool(TYPE_CIRCLE, circle);
}
}
}
3. 잔상 효과 업데이트하기
- 다른 파티클 업데이트와 마찬가지로 SparkParticle도 역순으로 순회하여 혹시 모를 인덱스 밀림을 방지하도록 한다.
- 투명도가 기준치 이하 거나 캔버스 영역을 벗어나면 활성화 배열에서 제거해 풀에 반납하도록 한다.
// Canvas.js
updateSparkParticle() {
// 배열에서 제거 시 인덱스 밀림 문제 방지를 위하여 역순으로 순회
for (let i = this.sparkParticles.length - 1; i >= 0; i--) {
// 스파크 파티클을 업데이트 하고 화면에 그린다.
const spark = this.sparkParticles[i];
spark.update();
spark.draw();
// 스파크가 기준치 투명도 이하이거나 캔버스 영역을 벗어났을때 활성화 배열에서 제거하고 풀에 반납한다.
if (spark.belowOpacityLimit() || this.isOutOfCanvasArea(spark.x, spark.y)) {
this.sparkParticles.splice(i, 1);
this.pm.returnToPool(TYPE_SPARK, spark);
}
}
}
4. 잔상 효과 애니메이션 적용
- 생성은 꼬리, 원형 효과를 업데이트할 때 생성되므로 따로 추가할 내용은 없고 업데이트 부분만 실행해 준다.
animateFireworks() {
let then = document.timeline.currentTime;
let frameCount = 0;
const bgCleanUp = setRgbaColor(SCREEN.BG_RGB, SCREEN.ALPHA_CLEANUP);
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();
}
// Tail, Circle 업데이트 시 SparkParticle 생성
this.updateTailParticle();
this.updateCircleParticle();
this.updateTextParticle();
// SparkParticle 업데이트
this.updateSparkParticle();
frameCount = (frameCount + 1) % (ANIMATION.TAIL_FPS + this.textLength);
then = now - (delta % this.interval);
}
this.animationId = requestAnimationFrame(frame);
};
this.animationId = requestAnimationFrame(frame);
}
- 모든 애니메이션을 완성하였으므로 index.js에서 hashchange 이벤트가 발생할떄 애니메이션 부분도 추가하도록 한다.
// index.js
const handleHashChange = () => {
if (isHashFireworks()) {
canvas.init();
canvas.render(); // 애니메이션 렌더
switchScreen(FIREWORKS);
} else {
cancelAnimationFrame(canvas.animationId); // 홈화면으로 돌아갈 시 애니메이션 취소
domElements.userInput.value = "";
canvas.initCanvasVars();
switchScreen(HOME);
}
};
잔상효과 추가 전과 후
(왼) 추가 전 : 꼬리의 움직임도 잘 확인할 수 없고 원형 효과도 점이 찍힌것 같은 모습이다.
(오) 추가 후 : 꼬리의 움직임을 잘 확인할 수 있고 원형 효과도 훨씬 풍푸하고 자연스러워 보인다.
5. 실행 화면
- 잔상 효과가 추가되면서 한결 자연스러워진 모습을 볼 수 있다.
Github Repo
'Canvas > 불꽃놀이 프로젝트' 카테고리의 다른 글
[Canvas-불꽃놀이-18] utils.js 단위 테스트 (0) | 2024.12.04 |
---|---|
[Canvas-불꽃놀이-17] Jest 설정과 테스트 진행 순서 (1) | 2024.12.03 |
[Canvas-불꽃놀이-15] CircleParticle 구현과 업데이트 (1) | 2024.11.30 |
[Canvas-불꽃놀이-14] 원형 불꽃 효과 생성 (2) | 2024.11.29 |
[Canvas-불꽃놀이-13] TextParticle 구현과 적용 (1) | 2024.11.26 |