[Canvas-불꽃놀이-16] SparkParticle로 잔상효과 적용하기

이전 글 : [Canvas-불꽃놀이-15] CircleParticle 구현과 업데이트

 

[Canvas-불꽃놀이-15] CircleParticle 구현과 업데이트

이전 글 : [Canvas-불꽃놀이-14] 원형 불꽃 효과 생성 [Canvas-불꽃놀이-14] 원형 불꽃 효과 생성이전 글 : [Canvas-불꽃놀이-13] TextParticle 구현과 적용 [Canvas-불꽃놀이-13] TextParticle 구현과 적용이전 글 :

jinsk-joy.tistory.com

 

불꽃놀이에 잔상 효과를 적용해 생동감 더해주기

 

앞서 완성한 불꽃놀이를 보면 움직임은 있으나 잔상이 없어서 정적인 느낌이 강하고 단조로워 보였다.

이에 각 파티클에 붙는 잔상 효과를 추가하여 움직임이 좀 더 자연스럽고 생동감 있는 불꽃놀이를 만들어보기로 했다.

 

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

 

GitHub - jinsk9268/text-fireworks

Contribute to jinsk9268/text-fireworks development by creating an account on GitHub.

github.com