개요
38가지 자바스크립트 튜토리얼에서 갑자기 뜬금포로 플랫포머 게임 만들기가 나와버려서..
해당건은 진행 안하는걸루 ^^.. 포스트 한 5개분량은 써야될것 같아서, 추후 시간이 나면 진행하는걸로 결정했다.
그런고로 이번에는 요즘 회사에서 진행하는 프로젝트중 이벤트 페이지에 들어가는 룰렛 기능을 작업했었는데,
그 룰렛을 만드는 법에 대한 튜토리얼을 작성해보려고 한다.
역시나 완성본 링크부터 공유를 하고 시작을 하겠다.
https://mooky1007.github.io/roulette/
Roulette
mooky1007.github.io
아, 룰렛의 이미지는 그냥 구글에서 검색해서 아무거나 나온걸로 쓴건데..
만약 문제가 된다면 댓글남겨주시면 바로 교체하도록 하겠습니다~
이번 룰렛 개발 작업의 주요 요청 사항은 다음과 같았다.
- 쫄리는 맛이 있어야 하므로 최소 n바퀴 만큼 돌고 천~천히 멈춰야 한다.
- 랜덤으로 돌아갈 수도 있어야 하지만, 특정 결과가 나오게 조작할 수 있어야 한다.
- 결과 값중 특정 값을 절대 안나오게 할 수 있어야 한다.
- 엔터, 스페이스바, ESC키로 조작이 가능했으면 좋겠다.
- 룰렛의 초기 위치를 정할 수 있었으면 좋겠다.
- 결과가 정해질때, 매번 같은 위치가 아니라 그 결과의 영역안에서 랜덤으로 멈췄으면 좋겠다.
- 룰렛의 영역은 동일할수도, 다를 수도 있는데 결정되면 전달 예정이다.
음.. 다른건 그렇다 쳐도, 6번과 7번 항목때문에 쉽게쉽게 만들려는 꿈이 박살났다.
예를들어 위 룰렛이라고 치면, 200P에 당첨되었을때 200P 결과 핀이 영역의 거의 시작부분에 있을 수도 있고,
중간에 있을 수도 있고, 마지막에 있을 수도 있어야 하고, 이건 특정 결과를 강제적으로 나오게 할때도 적용되는 부분이였다.
애초에 그냥 결과 갯수만큼 뽑아서 rotate로 360*돌릴 바퀴수 + 해당 영역 중간 각도 더해줘서 결과 뿅! 나오게 하려고 했는데.. 언제나 인생은 생각한데로 흘러가지 않는것 같다.
우선 늘 하던데로 html 먼저 작업하고, class 기반으로 자바스크립트를 작성하기로 했다.
HTML
<div class="roulette_container">
// roulette_container: 룰렛 이벤트의 전체 영역
<div class="roulette_screen">
// roulette_screen: 실제 룰렛이 들어갈 영역, 원래 밑에 시작하기 버튼이 있어서 넣었었는데,
// 시작하기가 룰렛 중앙으로 옮겨가는 바람에 그냥 남겨 두었다. 나중에 추가 될 수도 있으니까.
<div class="point">
<img src="./src/pointer.png" alt="roulette pointer">
</div>
// point : 룰렛 위에 그.. 당첨인 부분을 표시해주는 핀의 영역이다
<div class="roulette_box">
// roulette_box : 실제로 돌아가는 룰렛의 이미지가 있는 영역
<img src="./src/roulette.png" alt="roulette board">
</div>
<button class="center btn" type="button">
// center btn : 룰렛 가운데이 있는 버튼 이건 안돌아간다.
<img src="./src/center.png" alt="roulette start button">
</button>
</div>
<div class="modal hide">
// 결과를 알려주기위한 modal, hide class가 붙어 숨겨진 상태다.
<div class="result_text">
<span class="point_text">100p</span>
<span class="prefix_text">에 당첨되셨습니다!</span>
</div>
<button type="button">확인</button>
</div>
</div>
CSS
언제나 그렇듯 CSS는 따로 안쓸예정. CSS 설명하는게 제일 어렵다.
각자의 취향에 맞게 작업하면 될듯?..
주의할점은, roulette_box 클래스에는 transform 속성을 주면 안된다는것 정도?..
그리고 roulette_screen > point 클래스는 딱 정중앙 상단에 위치시켜주자. 안그러면 계산할때 복잡하니까.
음.. 그리고 뭐 별거 없다.
JS
class Roulette {
constructor(el, config = {}, result = []) {
this.el = document.querySelector(el);
// DOM Elements
this.roulette = this.el.querySelector('.roulette_screen .roulette_box');
this.rouletteButton = this.el.querySelector('.center.btn');
this.resultModal = this.el.querySelector('.modal');
this.resultModalText = this.el.querySelector('.modal .result_text .prefix_text');
this.resultModalTextPoint = this.el.querySelector('.modal .result_text .point_text');
this.resultModalButton = this.el.querySelector('.modal button');
// Roulette Config
this.duration = config.duration || 3500;
this.defaultSpin = config.defaultSpin || 4;
this.offset = config.offset || 45;
this.startPosition = config.startPosition || 0;
this.exceptResult = config.exceptResult || [];
// Roulette State
this.rotatePos = 0;
this.spin = false;
this.modalStatus = false;
// Roulette Result
this.result = result;
// Event Binding
this.rouletteButton.addEventListener('click', () => this.start());
this.resultModalButton.addEventListener('click', () => this.hideModal());
window.addEventListener('keydown', (e) => {
if(this.modalStatus){
if(String(e.keyCode).match(/13|32|27/i)) this.hideModal();
}else{
if(String(e.keyCode).match(/13|32/i)) this.start();
}
});
this.init();
}
init() {
const { roulette, startPosition } = this;
this.timer && clearTimeout(this.timer);
roulette.style.cssText = `
transition: none;
transform: rotate(${startPosition}deg);
`;
this.hideModal();
}
start(fixedAmount) {
if (this.spin) return;
this.init();
const { result, roulette, duration, defaultSpin, offset, exceptResult, getRangeRandom } = this;
this.spin = true;
this.timer = setTimeout(() => {
this.rotatePos = (fixedAmount !== undefined
? getRangeRandom(result[fixedAmount].range[0], result[fixedAmount].range[1])
: Math.floor(Math.random() * 360)
) + (360 * defaultSpin) - offset;
if(exceptResult.length > 0){
let except = exceptResult.map(el => result[el].range);
except.forEach(([start, end]) => {
if((this.rotatePos%360 + offset) >= start && (this.rotatePos%360 + offset) <= end){
const noneExcept = result.filter(({range}) => !except.includes(range));
const ranNumber = Math.floor(Math.random() * noneExcept.length);
this.rotatePos = getRangeRandom(noneExcept[ranNumber].range[0], noneExcept[ranNumber].range[1]) + (360 * defaultSpin) - offset;
}
});
}
roulette.style.cssText = `
transition: all ${duration}ms cubic-bezier(0, 0, 0, 1);
transform: rotate(${this.rotatePos}deg);
`;
this.getResult();
}, 0);
}
getRangeRandom(min, max) {
const result = Math.floor(Math.random() * (max - min + 1)) + min;
return result;
}
getResult() {
const { rotatePos, duration, result, offset } = this;
const realPos = ((rotatePos % 360) + offset) % 360;
this.timer = setTimeout(() => {
result.forEach(({name, range : [start, end]}) => {
if (start <= realPos && end >= realPos) {
this.renderModalText(name);
this.spin = false;
this.showModal();
}
});
}, duration);
}
renderModalText(text) {
this.resultModalTextPoint.innerHTML = text;
this.resultModalText.innerHTML = text === '꽝!' ? '아쉽지만 다음 기회에...' : '축하합니다!';
}
showModal() {
this.resultModal.classList.remove('hide');
this.modalStatus = true;
}
hideModal() {
this.resultModal.classList.add('hide');
this.modalStatus = false;
}
}
끝!