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

이전 글 : [Canvas-불꽃놀이-14] 원형 불꽃 효과 생성

 

[Canvas-불꽃놀이-14] 원형 불꽃 효과 생성

이전 글 : [Canvas-불꽃놀이-13] TextParticle 구현과 적용 [Canvas-불꽃놀이-13] TextParticle 구현과 적용이전 글 : [Canvas-불꽃놀이-12] text 픽셀 데이터 생성 [Canvas-불꽃놀이-12] text 픽셀 데이터 생성이전 글

jinsk-joy.tistory.com

 

CircleParticle 클래스를 완성하여 화면에 렌더 해보기

이전 글에 원형 불꽃 효과를 생성하였다. 

이번에는 이 효과를 적용할 CircleParticle 클래스를 완성하여 실제로 화면에 렌더 해보겠다.

 

1. CircleParticle 클래스 구현

이전의 원형 불꽃 효과의 내용을 바탕으로 CircleParticle을 구현하도록 하자.

 

1-1. 생성자와 멤버변수

  • 부모 클래스인 Particle에 공통 속성을 넘겨 관리하고, CircleParticle에 중력 효과를 적용하기위해 고유 속성을 추가한다.
  • 재사용성을 높이기 위해 마찰력을 초기화 상태(initialState)에 저장하고 추후 재사용시 원래 상태로 복원하도록 한다.
// constants.js
export const CIRCLE = {
    // 생략...
    FRICTION: 0.94, // 마찰력
    GRAVITY: 0.01, // 중력
};
class CircleParticle extends Particle {
    constructor({ ctx, isSmallScreen, x, y, vx, vy, radius, opacity, color }) {
        // 부모 클래스를 이용하여 공통 멤버 변수 초기화
        super({ ctx, isSmallScreen, x, y, vx, vy, radius, opacity, friction: CIRCLE.FRICTION, color });
        
        // CircleParticle 고유 멤버 변수 : 중력을 추가하여 하강 효과 적용
        this.gravity = CIRCLE.GRAVITY;
        
        // 재사용을 위한 초기 상태 저장
        this.initialState = { friction: CIRCLE.FRICTION };
    }
}

 

1-2. draw

  • 파티클을 그릴때 빛번짐 효과를 주기위해 ctx.shadowBlur 속성을 추가했다.
  • 캔버스는 상태 기반으로 동작하기 때문에 한번 설정한 속성은 이후에 그려지는 모든 도형에 동일하게 적용된다.
    이러한 상태를 개별적으로 관리하려면  ctx.save() 와 ctx.restore() 메서드를 사용해야 한다.
  • ctx.save()
    -. 현재 캔버스의 drawing state(그리기 상태)를 저장하여, 복원 시 이전의 상태를 사용할 수 있게 한다.
    -. 캔버스 상태(drawing state)란 scale, rotate, fillStyle, shadowBlur, shadowColor, lineWidth등 과 같은 속성값을 포함한다.
  • ctx.restore()
    -. 이전의 drawing state을 복원한다.
    -. 캔버스 상태와 픽셀 데이터가 분리되어 있기 때문에, 복원을 해도 화면에 그려진 픽셀 데이터는 사라지지 않고 블러가 적용된 상태로 남아있으며 이전에 저장한 drawing state만 복원된다. 
    -. 만약 복원하지 않는다면 이후에 그려지는 text나 tail 파티클에도 블러 효과가 적용되게 된다.
        따라서 save를 통해 저장한 이전의 상태를 꼭 restore 시켜줘야 파티클의 블러 효과를 개별적으로 적용할 수 있다.
  • 아래와 같은 절차로 블러 효과를 파티클에 개별적으로 적용할 수 있다.
    -. 현재 상태 저장 -> 파티클 a를 그리기 위해 블러 설정 -> 파티클 a 그리기 -> 블러 설정 전 상태로 복원
    -. 이런 복원 작업 덕분에 다음 파티클을 그릴때 새로운 블러 설정을 독립적으로 적용할 수 있다.
// constants.js
export const CIRCLE = {
    // 생략...
    
    // 번짐의 정도
    SHADOW_BLUR: 10,
};
// CircleParticle.js
draw() {
    // 현재 캔버스 상태 저장: 이후 복원을 위해 기록
    this.ctx.save();
    
    // 블러 효과 적용 : 파티클 주변에 빛 번짐같은 느낌 추가
    this.ctx.shadowBlur = CIRCLE.SHADOW_BLUR;
    this.ctx.shadowColor = this.fillColor; // 파티클 색상과 동일한 그림자 색상 적용

    // 부모 클래스의 메서드를 사용하여 화면에 파티클을 그린다.
    super.draw();

    // 복원: 블러 효과 적용 후 이전에 저장한 캔버스 상태 복원하여 효과를 개별적으로 적용하도록 한다.
    this.ctx.restore();
}

 

1-3. update

  • 파티클의 반지름은 점점 크게 증가시켜 입체감을 주다가, 마지막에 살짝 감소시켜 소멸 전 단계에서 자연스러움을 더했다.
  • 투명도를 점진적으로 줄여 파티클이 화면에 자연스럽게 사라지도록 한다.
  • y축 속도에 중력을 더해 아래로 자연스럽게 하강하도록 한다.
// constants.js
export const CIRCLE = {
    // 생략...
    RADIUS_ADJUST_OFFSET: 0.15, // 반지름 조정 간격
    OPACITY_ADJUST_OFFSET: 0.01, // 투명도 조정 간격
    RADIUS_ADJUST_RATE: 0.99, // 반지름 조정 비율
};
// CircleParticle.js
update() {
    // 반지름을 점진적으로 증가시켜 입체감을 더함
    this.radius += CIRCLE.RADIUS_ADJUST_OFFSET; 
    
    // 투명도를 점진적으로 감소시켜 자연스럽게 사라지도록 함
    this.opacity -= CIRCLE.OPACITY_ADJUST_OFFSET; 
    
    // y좌표값에 중력을 더해 하강 효과 적용
    this.vy += this.gravity; 

    // 부모의 업데이트 메서드를 사용하여 공통 속성 업데이트하기
    super.update();

    // 반지름이 너무 커지지 않도록 마지막에 일정 비율로 감소
    this.radius *= CIRCLE.RADIUS_ADJUST_RATE;
}

 

2.  CircleParticle 애니메이션 적용

이제 원형효과를 위한 CircleParticle 생성 메서드를 실행하고 업데이트시켜 애니메이션에 적용해 보도록 하겠다.

 

2-1. CircleParticle 생성 메서드 실행

  • 원형 효과 또한 꼬리가 사라진 자리에 생성되어야 하므로 꼬리 파티클을 활성화 배열에서 제거할 시 CircleParticle을 생성한다.
  • 사라진 꼬리의 x, y위치를 생성 메서드에 전달한다.
// Canvas.js
updateTailParticle() {
    for (let i = this.tailParticles.length - 1; i >= 0; i--) {
        const tail = this.tailParticles[i];
        tail.update();
        tail.draw();

        if (tail.belowOpacityLimit(TAIL.OPACITY_LIMIT)) {
            this.createTextParticle(tail.x, tail.y);
            
            // CircleParticle 꼬리가 사라진 x, y 위치에 생성
            this.createCircleParticle(tail.x, tail.y);

            this.tailParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_TAIL, tail);
        }
    }
}

 

2-2. 업데이트 메서드 구현

  • 이제 CircleParticle 클래스의 구현도 끝나서 애니메이션 중 파티클의 상태를 업데이트 하는 메서드를 구현해보겠다.
  • CircleParticle도 활성화 배열에서 제거 시 인덱스 밀림 문제를 방지하기 위해 역순으로 순회한다.
  • CircleParticle을 제거하는 기준은 투명도가 0 이하일떄, 파티클이 캔버스 영역에서 벗어날 때 제거하고 풀에 반납한다.
// Canvas.js
updateCircleParticle() {
    // 인덱스 밀림 문제를 방지하기 위해 역순으로 순회
    for (let i = this.circleParticles.length - 1; i >= 0; i--) {
        // 개별 파티클들의 상태를 업데이트하고 화면에 그린다.
        const circle = this.circleParticles[i];
        circle.update();
        circle.draw();

        // 투명도가 제한선 아래이거나 캔버스 영역을 벗어날 때 활성화 배열에서 제거하고 풀에 반납
        if (circle.belowOpacityLimit() || this.isOutOfCanvasArea()) {
            this.circleParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_CIRCLE, circle);
        }
    }
}

 

2-3. CircleParticle 애니메이션 실행

  • 애니메이션을 관리하는 animateFireworks 메서드에 CircleParticle 업데이트 메서드를 실행하여 원형 효과를 구현한다.
// Canvas.js
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();
            }

            // TailParticle 업데이트 후 활성화 배열 제외할 때 CircleParticle 생성
            this.updateTailParticle();
            
            // CircleParticle 상태 업데이트하여 화면에 다시 렌더
            this.updateCircleParticle();
            
            this.updateTextParticle();

            frameCount = (frameCount + 1) % addFrameCountDivisor;
            then = now - (delta % this.interval);
        }

        this.animationId = requestAnimationFrame(frame);
    };

    this.animationId = requestAnimationFrame(frame);
}

 

3. 실행 화면 사진

글자수 별 대략적인 실행화면이다. 잔상효과는 추후 추가할 예정이다.

 

Github
 

GitHub - jinsk9268/text-fireworks

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

github.com