프로그래밍/Vue.js

vuejs 반응형 sprite canvas 만들기

p-a-r-k 2023. 7. 19. 19:31
반응형

canvas와 감싸는 container div를 만든다. 이때, 상위 부모의 사이즈가 반응적이어야 한다.

보닌(본인)은 sprite 진행 이전 poster 이미지를 하나 두고 사이즈를 가져올수 있게끔 하였다.

poster이미지의 가로세로가 줄어들든 늘어나든 canvas를 감싸는 container의 가로세로를

해당 poster 이미지 가로세로로 세팅해주면 되기때문이다.

<div class="animation-container">
    <div ref="spritePosterRef" class="content" :class="{start: isStart}">
      <img src="/images/event/img_phone.png" alt="poster image"/>
    </div>

    <div ref="spriteContainerRef" class="sprite-container">
      <canvas ref="spriteRef" :width="width" :height="height"></canvas>
    </div>
</div>

<a href="javascript:;" @click="onStart">시작</a>

각 ref변수와 sprite에 사용할 변수도 만들어준다.

const spritePosterRef = ref<HTMLDivElement>(); // 애니메이션 이전 폴더블 이미지 영역
const spriteContainerRef = ref<HTMLDivElement>(); // 애니메이션 전체 영역
const spriteRef = ref<HTMLCanvasElement>(); // 애니메이션 캔버스 영역

const isLoading = ref(false);
const canvas = computed(() => {
  return spriteRef.value;
});
const ctx = computed(() => {
  return canvas.value?.getContext('2d');
});

/**
 * # 캔버스 사이즈
 */
const width = ref(0);
const height = ref(0);

let totalImagesCount = 38; // 총 프레임 수
let progress = 0; // 애니메이션 진행 인덱스
let currentFrame; // 현재 프레임


const isStart = ref(false); // 이벤트 시작 여부
const isAnimeStart = ref(false); // sprite 시작 여부
const isAnimeEnd = ref(false); // sprite 완료 여부

 

반응성이 있는변수는 ref, computed로 선언하였고, sprite 상태관련 변수도 선언했다.

먼저 sprite시킬 이미지를 저장해주는 변수와 함수도 생성한다. 

Image객체에 src를 지정하여 배열에 담아두고 sprite돌릴 때 하나씩 꺼내 그릴예정이다.

const videoImages: HTMLImageElement[] = [];

/**
 * # 이미지 sprite 정보를 배열에 세팅한다.
 */
function setImages() {
  for (let i = 0; i < totalImagesCount; i++) {
    const imgElem = new Image();
    let imgUrl = imgUrl = `/images/event/animation/unit/ZFlip4_DeMain_840ae000${String(i).padStart(2, "0")}.png`;
    
    imgElem.src = imgUrl;
    videoImages.push(imgElem);
  }
}

 

이후, 시작이벤트와 캔버스사이즈를 설정하는 함수들을 만들어준다.

/**
 * # 캔버스 사이즈 조정
 */
function initCanvasSize() {
  width.value = spriteContainerRef.value?.clientWidth ?? 0;
  height.value = spriteContainerRef.value?.clientHeight ?? 0;
}


/**
 * # 캔버스에 이미지를 그린다
 * @param index
 */
function draw(index: number) {
  // ctx.value.clearRect(0, 0, width.value, height.value);
  ctx.value?.drawImage(videoImages[index], 0, 0, 840, 1630, 0, 0, width.value, height.value);
}

/**
 * # 캔버스초기화
 * - 캔버스 사이즈조정
 * - 현재 인덱스로 그림 그리기
 */
function init() {
  if (!isStart.value) return;
  initCanvasSize();
  nextTick(() => draw(progress));
}

/**
 * # sprite 시작
 */
function start() {
  isStart.value = true;

  nextTick(() => {
    isAnimeStart.value = true;
    setTimeout(() => {
      if (spritePosterRef.value) {
        spritePosterRef.value.style.visibility = 'hidden';
        spritePosterRef.value.style.opacity = '0';
      }

      if (rootRef.value) {
        // 애니메이션 영역 최하단을 기준으로 스크롤 이동
        rootRef.value.scrollIntoView(false);
      }
      init();
      loop();
    }, 600);
  });
}


let frameCount = 0; // 프레임 속도 조절변수
/**
 * # 애니메이션 진행
 */
function loop() {
  frameCount++;
  if (frameCount < 5) {
    window.requestAnimationFrame(loop);
    return;
  }
  frameCount = 0;
  if (progress < 0) progress = 0;
  if (progress >= totalImagesCount) { // sprite 모두 완료
    progress = totalImagesCount - 1;
    isAnimeEnd.value = true;
    return;
  }

  currentFrame = Math.round(progress);
  draw(currentFrame);
  progress++;

  requestAnimationFrame(loop);
}

initCanvasSize: 캔버스사이즈를 조정해준다. computed로 만들어둔 width, height 변수에 현재 사이즈값을 대입한다.

draw: 현재 프레임 index를 받아 하나의 이미지를 배열에서꺼내 canvas에 그려준다. 각 인자 순서별로 이미지, 원본 이미지 x위치, 원본 이미지 y위치, 그릴 이미지 원본 가로, 그릴 이미지 원본 세로, 캔버스그릴 x위치, 캔버스그릴 y위치, 캔버스가로, 캔버스세로 이다.

init: 캔버스를 초기화한다. initCanvasSize와 사이즈가 조정된 tick 이후에 현재 canvas에 변경된 사이즈로 1개의 그림을 업데이트해주는 draw함수를 실행한다.

start: isStart를 true로 바꾸어 시작된 상태를 가지고, tick 이후에 poster영역을 숨기고 canvas의 최하단으로 화면을 스크롤시킨다. 이후 캔버스를 초기화해주는 init함수와 실제 애니메이션을 진행시키는 loop함수를 실행시켜준다.

loop: 보닌이 sprite를 위해 사용한 이미지는 38장인데 progress변수를 0부터 37까지 draw 시키는 함수이다. frameCount변수를 두어 sprite 속도를 조절한다. frameCount < 5 부분이 프레임을 좀 더 낮추어 천천히 움직이게 해준다. 5보다 커지면 실제 draw하여 이미지도 그리고 인덱스도 +1 해주어 다음이미지를 그리는것을 반복하게된다.

 

추가로.. 모바일에서 스크롤하면 resize되는 현상이 있어서 resize이벤트로 init을 호출하게 했다.ㅋ

onMounted(() => window.addEventListener('resize', init));
onBeforeUnmount(() => window.removeEventListener('resize', init));

 

반응형