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

이전 글 : [Canvas-불꽃놀이-13] TextParticle 구현과 적용

 

[Canvas-불꽃놀이-13] TextParticle 구현과 적용

이전 글 : [Canvas-불꽃놀이-12] text 픽셀 데이터 생성 [Canvas-불꽃놀이-12] text 픽셀 데이터 생성이전 글 : [Canvas-불꽃놀이-11] 불꽃놀이 꼬리 완성 [Canvas-불꽃놀이-11] 불꽃놀이 꼬리 완성이전 글 : [Ca

jinsk-joy.tistory.com

 

 

원형 불꽃 효과 구현

텍스트 파티클 만으로는 시각적으로 밋밋하고 불꽃놀이의 느낌을 전달하기엔 현재의 상태가 부족하다고 느꼈다.

그래서 원형 모양의 효과를 추가해 좀 더 화려하고 생생한 느낌의 불꽃놀이를 만들어 보기로 했다.

처음에 도넛 모양을 유지하면서 퍼지는 형태를 구현하거나, 계층별로 원이 퍼지는 모습을 구현해보기도 했는데 결과물이 뭔가 썩 마음에 들지 않았다.
풍성한 느낌을 주기 위해서 파티클의 수를 늘려봤지만, 오히려 답답하거나 좌표 위치를 무작위로 생성하다 보니 듬성듬성 비어있는 느낌이 있어 불꽃놀이처럼 느껴지지 않았다. 시각적으로 불균형한 모습이 있으니 예뻐 보이지도 않았다. 게다가 텍스트 길이에 따라 더 많은 파티클을 필요로 했으므로 불꽃 당 보통 600개 이상의 파티클을 생성해야 한다는 부담도 있었다. (아래 사진 참조)

불꽃 놀이 효과 테스트

 

그러다 실제 불꽃놀이 장면을 다시 관찰해 보니 생각보다 정형화된 모양이 많아, 이를 바탕으로 더 간결하고 효과적으로 불꽃 원형 효과를  재설계하였다.

 

1. 원형 효과 구현하기

  • 계층 구조를 통해 중심에서 부터 퍼져나가는 원형 효과를 구현함으로써 불꽃놀이의 생생한 느낌을 더했다.
  • 원형 효과의 색상은 불꽃마다 모두 동일하게 적용하기로 한다.
    불꽃마다 다른 색상을 사용하면 시각적인 부담감이 있을 수 있고, 텍스트보다 원형 효과에 시선이 더 집중될 우려가 있기 때문이다.
    대신 각 계층의 색을 무지개 색으로 설정해 불꽃놀이의 화려함과 생동감을 표현했다.
  • 속도와 투명도를 계층별로 다르게 설정하면 중심에서 바깥으로 퍼져나가는 동작이 더 자연스러워 보여 입체감을 더할 수 있다.
  • 원형의 크기도 텍스트 길이에 맞춰 유동적으로 생성하록 했다.

 

2. 계층구조를 정의하기 위한 필요값 계산

  • 원형 효과 역시 꼬리가 사라진 시점의 x, y위치를 기준으로 생성한다.
  • 각 계층별로 생성할 파티클의 개수는 60개이며, 파티클 당 6도(360 / 60)의 간격을 둔다.
  • 계층별 간격값을 구해 일정한 거리로 각 계층을 생성한다.
  • 바깥 계층으로 갈수록 속도와 불투명도를 증가시켜 자연스럽게 퍼져나가면서 사라지도록 한다.

 

2-1. 계층 간 간격 계산하기

  • 계층 간 간격은 파티클을 분포를 결정짓는 중요한 요소로 간격이 좁을수록 밀도가 작은 원이 간격이 클수록 큰 원이 생성돼 적절한 간격을 조정해야 한다. text의 width가 작으면 작은 원이 생성되므로 css로 변환한 textWidth 또는 글자당 width 중 더 큰 값을 선택해 반지름을 계산하는 것이 글자 길이의 상황에 맞는 자연스러운 원을 생성할 수 있다.
  • 프로젝트에서는 7개의 계층을 가지고 있고 위에 반지름을 바탕으로해 일정한 간격으로 생성된다. 
// constants.js
export const CIRCLE = {
    LAYERS: 7, // 계층의 개수
}
// Canvas.js
/**
 * @param {number} textWidth
 * @returns {number} 문자열 width의 절반 또는 글자당 width중 더 큰 값을 기준으로 계층별 간격 반환
 */
calculateLayerOffset(textWidth) {
    // css로 변환한 문자열 width 기준으로 반지름 계산
    const radiusFromCssWidth = textWidth / 2 / this.dpr;
    
    // 글자당 width를 반지름으로 계산
    const radiusFromCharWidth = textWidth / this.textLength;

    // 둘 중 더 큰 큰값을 선택해 계층별 간격을 반환
    return Math.max(radiusFromCssWidth, radiusFromCharWidth) / CIRCLE.LAYERS;
}

 

2-2. 삼각함숫값을 이용한 원형 모양 구현

  • 삼각함수는 원의 중심과 반지름, 각도를 이용해 2D 공간에서 점의 좌표를 정확히 계산할 수 있으므로 원형 효과를 구현하는 가장 간단하고 효율적인 방법이다.
  • 원의 점은 중심에서 일정한 거리에 있는 모든 점의 집합이다. 원의 좌표는 x = 반지름 * cos(⍬) , y = 반지름 * sin(⍬)
  • 삼각함수 cos(⍬)와 sin(⍬)는 주어진 각도에서 x와 y좌표를 계산하는 데 사용한다.
  • 중심을 기준으로 각도를 일정하게 변화시키면서 삼각함수를 이용해 x와 y좌표를 계산하면 일정한 간격을 가진 원의 좌표 배열을 만들 수 있다.
// constants.js
const PER_QTY = 60;  // 각 계층별 파티클 개수
const PER_ANGLE = 360 / PER_QTY; // 파티클 별 각도
export const CIRCLE = {
    LAYERS: 7,
    PER_QTY,
    PER_ANGLE,
    PER_HALF_ANGLE: PER_ANGLE / 2,
}

export const PARTICLE = {
    // 생략...
    DEGREE_TO_RADIAN: Math.PI / 180, // 각도를 라디안으로 변환하기 위한 변환값
}
// Canvas.js
/**
 * @param {number} circleIdx
 * @returns {object} 삼각함수 계산에 필요한 코사인, 사인값 반환
 */
calculateCosSin(circleIdx) {  
    // 각 파티클 별 각도값
    const angleDegree = CIRCLE.PER_ANGLE * circleIdx;

    // 코사인, 사인값 산출을 위해 각도값을 라디안으로 변환
    const radian = PARTICLE.DEGREE_TO_RADIAN * angleDegree;

    return { xCos: Math.cos(radian), ySin: Math.sin(radian) };
}

 

2-3. CircleParticle 거리 계산

  • 계층별로 CircleParticle이 배치된 모습을 지그재그 형태로 보이게 하기 위해,  계층 안에서 생성지점으로부터의 거리를 다르게 적용해야 한다.
  • 계층 간 간격과 텍스트 길이별 추가 간격을 바탕으로 기본 거리를 정했다.
  • 원점에서부터 계층 간 거리는 기본거리와 계층의 순서를 곱한 후 글자의 길이가 길수록 너무 커질 수 있으므로 반으로 나눈다.
  • 지그재그 모양을 만들기 위해 홀수 파티클에 기본 거리를 반으로 나눈 값을 추가해 준다.
// Canvas.js
/**
 * @param {number} layerIdx
 * @param {number} circleIdx
 * @param {number} layerOffset
 * @param {number} textLengthOffset
 * @returns {number} 생성지점으로부터의 레이어별 CircleParticle의 거리 반환
 */
calculateDistFromCreationPoint(layerIdx, circleIdx, layerOffset, textLengthOffset) {
    // 기본 거리
    const dist = layerOffset + textLengthOffset;
    
    // 계층간 거리
    const layerDist = (dist * layerIdx) / 2;
    
    // 지그재그 모양으로 만들기 위해 홀수 파티클에 추가 거리를 더한다.
    const additionalDist = isEven(circleIdx) ? 0 : dist / 2;

    // 기본거리에 추가거리를 더한다.
    return baseDist + additionalDist;
}

 

3. 원형 효과 생성

  • 앞에 계산한 값들로 원형 효과를 구현한다.
  • 반지름 : 바깥 원으로 갈수록 점점 증가 돼 풍성한 느낌을 주도록 한다.
  • 투명도 : 바깥 원으로 갈수록 불투명해져 안쪽 원부터 차례대로 자연스럽게 사라지도록 한다.
  • 속도 : 바깥 원으로 갈수록 속도가 빨라져 퍼져 역동적인 모션을 강조한다.
  • 색상 : 안쪽 계층 부터 빨, 주, 노, 초, 파, 남, 보의 무지개 색상을 적용한다. 무지개 색상은 불꽃 놀이의 화려함을 표현하기에 적합하며 각 계층의 시각적인 구분을 쉽게 만들어 준다.

 

// Canvas.js
initCanvasVars() {
    // 생략...
    // 활성화된 CircleParticle을 담을 배열
    this.circleParticles = [];

    // 생략...
}
// constants.js
export const CIRCLE = {
    // 생략...
    BASE_TEXT_OFFSET: 10,
    RADII: [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1],
    OPACITY_BASE: 0.5,
    OPACITY_OFFSET: 0.085,
    HUES: [280, 240, 200, 120, 60, 30, 0],
    SATURATION: 60,
    LIGHTNESS: 60,
};
// Canvas.js
/**
 * @param {number} x
 * @param {number} y
 */
createCircleParticle(x, y) {
    // 계층간 간격
    const layerOffset = this.calculateLayerOffset(this.isMain(x) ? this.mainTextData.width : this.subTextData.width);
    
    // 텍스트 길이별 간격
    const textLengthOffset = CIRCLE.BASE_TEXT_OFFSET - this.textLength;
    
    // 기본 스피드
    const baseSpeed = layerOffset / this.interval;

    // 계층 순회
    for (let layerIdx = 0; layerIdx < CIRCLE.LAYERS; layerIdx++) {
        // 현재 계층의 속도 계수
        const speedFactor = baseSpeed + layerIdx;
        
        // 현재 계층에 속한 원형 파티클의 반지름
        const radius = CIRCLE.RADII[layerIdx];
        
        // 현재 계층에 속한 투명도 (바깥으로 갈수록 불투명도가 올라간다.)
        const opacity = CIRCLE.OPACITY_BASE + layerIdx * CIRCLE.OPACITY_OFFSET;
        
        // 현재 계층의 원형 파티클에 적용할 색상
        const color = setHslaColor({ hue: CIRCLE.HUES[layerIdx], saturation: CIRCLE.SATURATION, lightness: CIRCLE.LIGHTNESS });

        // 계층에 속한 원형 파티클 순회
        for (let circleIdx = 0; circleIdx < CIRCLE.PER_QTY; circleIdx++) {
            // 생성지점으로 부터의 거리
            const distFromCreationPoint = this.calculateDistFromCreationPoint(layerIdx, circleIdx, layerOffset, textLengthOffset);
            // 좌표 계산을 위한 cos(⍬), sin(⍬)
            const { xCos, ySin } = this.calculateCosSin(circleIdx);

            // 원형 파티클 생성시 필요한 파라미터 객체
            const circleParams = {
                x: x + distFromCreationPoint * xCos, // 계층별 원 x좌표
                y: y + distFromCreationPoint * ySin, // 계층별 원 y좌표
                vx: speedFactor * xCos, // 원형으로 퍼지기 위한 x속도
                vy: speedFactor * ySin, // 원형으로 퍼지기 위한 y속도
                radius,
                opacity,
                color,
            };
            this.circleParticles.push(this.pm.acquireParticle(TYPE_CIRCLE, circleParams));
        }
    }
}

 

 

Github 주소