프로그래밍/Vue.js

vuejs 슬롯머신 만들기

p-a-r-k 2023. 5. 22. 17:13
반응형

vuejs slotmachine 만들기. (슬롯에 사용한 이미지대신 텍스트로만 기록하겠다.)

3가지 라인이 있는 슬롯머신인데, 가운데는 그대로있고 첫번째와 세번째만 회전하는 슬롯머신을 구현하게 되었다.

첫번째 슬롯에는 1,2,3이 들어간다. 두번째는 0값으로 고정. 세번째는 각 나라별 화폐단위가 위치한다.

ex) 첫번째슬롯이 2, 세번째 슬롯이 jpy가 오게되면 20 jpy가 된다.

먼저 머신 div 객체에 해당하는 ref객체를 만들고, 관련 배열을 생성한다.

// 슬롯머신 진행중 여부
const isSpining = ref(false);
// 슬롯머신 라인 div 접근 객체 ref
const slotNumberRef = ref(); // 첫번째 라인
const slotUnitRef = ref(); // 세번째 라인
// 슬롯 div 엘리먼트들을 가져온다
const doors = computed(() => [
    slotNumberRef.value,
    slotUnitRef.value,
]);
// resize시에 re-render key값으로 사용
const machineKey = ref(new Date().getTime());


// 슬롯머신 라인에 사용될 데이터
const slotNumberItems = ['1', '2', '3'];
const slotUnitItems = ['AED', 'CZK', 'EUR', 'HKD', 'INR', 'JPY', 'MXN', 'THB', 'USD', 'ZAR'];

다음은 html 구성이다. container와 버튼 container로 구성한다.

.machine 내부에는 슬롯을 가리면서 머신 비주얼을 나타낼 png 영역. 이미지는 아래 파일에 첨부.

exam.zip
0.22MB

각 .slot은 첫번째 슬롯과 세번째 슬롯에 위치시킬 예정이다. 

.slot내부에 .slot-item에 실제 슬롯 내용이 들어가게된다. 처음엔 ? 로 세팅된다.

'시작하기' 버튼엔 onClickStart 메서드가 연결 되었다.

<div class="machine-container">
    <div class="machine">
      <div class="machine-front"></div>
      <!-- 숫자 슬롯 영역 //-->
      <div class="slot number" ref="slotNumberRef" :key="`number-${machineKey}`">
        <div class="slots">
          <div class="slot-item">
            <img src="/images/step-two/slot_number_nomal.png" alt=""/>
          </div>
        </div>
      </div>
      <!-- 단위 슬롯 영역 //-->
      <div class="slot unit" ref="slotUnitRef" :key="`unit-${machineKey}`">
        <div class="slots">
          <div class="slot-item">
            <img src="/images/step-two/slot_unit_nomal.png" alt=""/>
          </div>
        </div>
      </div>
    </div>
  </div>
  <p class="button-container">
    <a
        href="javascript:void(0)"
        class="btn-start"
        @click="onClickStart">시작하기</a>
  </p>

다음은 스타일 적용 scss 버전. 

.machine-container {
  .machine {
    position: relative;
    width: 788px;
    height: 546px;
    margin-top: 110px;
    margin-left: auto;

    .machine-front {
      position: relative;
      z-index: 1;
      width: 100%;
      height: 100%;
      background-image: url(/images/step-two/img_slotmachine.png);
      background-size: 788px 546px;
      background-position: 9px;
      background-repeat: no-repeat;
    }

    .slot {
      overflow: hidden;
      position: absolute;
      transition: transform 1s ease-in-out;
      width: 194px;
      height: 267px;
      top: 98px;

      &.number {
        left: 79px;
      }
      &.unit {
        right: 115px;
      }

      .slot-item {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 267px;
      }
    }

    // 인증전인경우 슬롯 깜빡깜빡 효과
    &.disabled {
      .slot {
        animation: blink-effect infinite 1.4s ease-in-out;
      }
    }
  }
}
.button-container {
  margin-top: 110px;
  a {
    position: relative;
    display: inline-block;
    &:active {
      top: 1px;
    }
    &.disabled {
      opacity: .5;
    }
  }
}

html구조와 관련 변수들을 생성했으니, 메서드들을 추가한다.

초기화해주는함수, 작동시키는 함수. 그리고 시작버튼 클릭시 함수

/**
 * # 슬롯머신 초기화
 */
function init() {
  const groups = 3, duration = 3;
  for (const door of doors.value) {
    const index = doors.value.indexOf(door);
    const boxes = door.querySelector('.slots');
    const boxesClone = boxes.cloneNode(false);
    const pool = ['nomal'];

    const arr = [];
    for (let n = 0; n < (groups > 0 ? groups : 1); n++) {
      if (index === 0) arr.push(...slotNumberItems);
      if (index === 1) arr.push(...slotUnitItems);
    }
    pool.push(...arr);

    boxesClone.addEventListener(
        'transitionstart',
        function (e: any) {
          e.target?.querySelectorAll('.slot-item').forEach((box: HTMLDivElement) => {
            // 돌아가는동안 blur 처리
            box.style.filter = 'blur(1px)';
          });
        },
        { once: true }
    );

    boxesClone.addEventListener(
        'transitionend',
        function (e: any) {
          e.target?.querySelectorAll('.slot-item').forEach((box: HTMLDivElement, index: number) => {
            box.style.filter = 'blur(0)';
          });
        },
        { once: true }
    );

    for (let i = pool.length - 1; i >= 0; i--) {
      const box = document.createElement('div');
      const txt = document.createElement('span');
      box.classList.add('slot-item');
      box.dataset.value = pool[i];
      box.style.display = 'flex';
      box.style.alignItems = 'center';
      box.style.justifyContent = 'center';
      box.style.width = door.clientWidth + 'px';
      box.style.height = door.clientHeight + 'px';

      // 이미지 사용시
      const img = document.createElement('img');
      if (index === 0) img.src = getAssetUrl(`/images/step-two/slot_number_${pool[i]}.png`);
      if (index === 1) img.src = getAssetUrl(`/images/step-two/slot_unit_${pool[i]}.png`);
      img.style.height = `${(148*100)/840}vw`;
      img.style.maxHeight = '148px';
      box.append(img);

      // TEST :: 글자만 사용시
      // if (index === 0) txt.innerText = pool[i];
      // if (index === 1) txt.innerText = pool[i];
      // box.append(txt);
      
      boxesClone.appendChild(box);
    }
    boxesClone.style.transitionDuration = `${duration > 0 ? duration : 1}s`;

    boxesClone.style.transform = `translateY(-${door.clientHeight * (pool.length - 1)}px)`;
    door.replaceChild(boxesClone, boxes);
  }
}

/**
 * 슬롯머신 작동
 * @param resultNumber
 * @param resultUnit
 */
async function spin(resultNumber: string, resultUnit: string) {
  init();
  // 처음상태에서 0.5초 지연시킨다.
  await delay(500);

  for (const door of doors.value) {
    const index = doors.value.indexOf(door);
    const boxes = door.querySelector('.slots');
    const boxDataList = [...boxes.querySelectorAll('.slot-item')].map(box => box.dataset.value);
    const duration = parseInt(boxes.style.transitionDuration);

    let resultIndex = 0;
    // 숫자슬롯 결과 인덱스
    if (index === 0) {
      resultIndex = boxDataList.indexOf(resultNumber);
    }
    // 단위슬롯 결과 인덱스
    if (index === 1) {
      resultIndex = boxDataList.indexOf(resultUnit);
    }
    boxes.style.transform = `translateY(-${door.clientHeight * resultIndex}px)`;
    // 슬롯별 지연시간 추가
    await new Promise((resolve) => setTimeout(resolve, duration * 100));
  }
  // 함수 완료시간 설정
  await delay(3000);
}



/**
 * # 슬롯머신 start 클릭
 */
async function onClickStart() {
  // 진행중이면 클릭 무시
  if (isSpining.value) return;

  isSpining.value = true;
  try {
    // spin함수에 결과값 전달
    await spin('2', 'JPY');
    await delay(400);
    
    // after logic..
  } catch (error: any) {
    Alert.error("에러");
  } finally {
    isSpining.value = false;
  }
}

html구조를 보면 알 수 있듯이 슬롯에관한 이미지들은 없이 ? 이미지만 존재하고,

초기화시에 스크립트에서 배열정보를 가지고 생성한다.

 

init 메서드에서 doors.length만큼 for문이 도는것은 슬롯2개 (첫번째, 세번째)가 도는것이다.

이후 인덱스가 0이냐 1이냐(첫번재슬롯이냐 세번째슬롯이냐)에 따라서 

0이면 number배열을가지고 돌고, 1이면 unit(화폐단위)배열을 가지고 돈다.

.slot-item인 div에 img를 생성하여 배열과매칭된 이미지를 src로 세팅하면 슬롯머신이 해당 이미지들을 가지고 뿌려질 준비가 된다.

 

spin 메서드에서는 인자로받은 2가지 결과값을가지고 배열에서 인덱스를 구한다.

위 코드에서는 '2', 'jpy'로 고정값을 결과값으로 넘겼다. ('시작하기'를 누른 onClickStart 메서드에서 넘김)

인덱스를구하여 해당 translateY 값을 계산하여 적용시키면 해당 위치에 멈추게된다.

 

추가로 지금 스크립트로 구현이되어서 vue엘리먼트로 렌더링되지 않는다.

resize시에는 이미지위치가 깨지게되어서, 추가로 리스너등록이 필요하다.

function setMachineKey() {
  machineKey.value = window.innerWidth;
}
const _setMachineKey = debounce(() => setMachineKey());
onMounted(() => {
  window.addEventListener('resize', _setMachineKey);
});
onUnmounted(() => {
  window.removeEventListener('resize', _setMachineKey);
});

결과..

반응형