자기소개 페이지 만들기

정승옥(seungok)·2021년 1월 1일
0

프로젝트

목록 보기
1/4
post-thumbnail

1. 클래스명을 이용한 이미지 변경

// 클래스명 배열
const directionClassName = ['left','center','right'];
// 이전 버튼
const fadeBack = () => {
    const shiftDirection = directionClassName.shift();
    directionClassName.push(shiftDirection);
    sliderEls.forEach((el,idx)=>{
        el.classList.remove(el.classList[1]);
        el.classList.add(directionClassName[idx]);
    })
}
// 다음 버튼
const fadeFront = () => {
    const popDirection = directionClassName.pop();
    directionClassName.unshift(popDirection);
    sliderEls.forEach((el,idx)=>{
        el.classList.remove(el.classList[1]);
        el.classList.add(directionClassName[idx]);
    })
}

2. mousemove & mouseleave

// rotate 초기화
const resetRotate = () => {
  if(document.body.offsetWidth<=650)
    return false;
  contactInnerEl.style.transform = `rotateY(0deg) rotateX(0deg)`;
}
// contact 영역에 마우스가 움직일때
contactEl.addEventListener('mousemove',(e)=>{
  if(document.body.offsetWidth<=650)
    return false;
  if(cursorOnInfo) // 커서의 위치가 Info영역에 있는지 확인하기 위한 Boolean
    return false;
  let xAxis = (window.innerWidth/2 - e.pageX)/15;
  let yAxis = (window.innerHeight/2 - e.pageY)/15;

  contactInnerEl.style.transform = `rotateY(${xAxis}deg) rotateX(${yAxis}deg)`;
});
// contact 영역에서 마우스가 벗어날때
contactEl.addEventListener('mouseleave',(e)=>{
  resetRotate();
});
// contactInfo 영역에서 마우스가 움직일때
contactInfoEl.addEventListener('mousemove',(e)=>{
  resetRotate();
  cursorOnInfo = true;
});
// contactInfo 영역에서 마우스가 벗어날때
contactInfoEl.addEventListener('mouseleave',(e)=>{
  if(document.body.offsetWidth<=650)
    return false;
  cursorOnInfo = false;
});
  • window.innerWidth: 브라우저 윈도우의 뷰포트 너비, 수직 스크롤바가 존재하면 포함
  • HTMLElement.offsetWidth(Height): padding값, border값까지 계산한 값(너비 또는 높이)
  • pageX : 브라우저 페이지에서의 x좌표 위치를 반환
  • pageY : 브라우저 페이지에서의 Y좌표 위치를 반환

3. blur & keyup & resize

blur 이벤트

infoTextEl.forEach(input=>{input.addEventListener('blur', toggleInfoValue);});

keyup 이벤트

document.addEventListener('keyup',(e)=>{
    if(e.key === 'ArrowDown'){
        ++nowIndex;
        if(nowIndex > lastNav)
            nowIndex = 0;
        toggleSlide();
        toggleSection();
        toggleOutNav();
        toggleMoreBtn();
    }
    else if(e.key === 'ArrowUp'){
        --nowIndex;
        if(nowIndex<0)
            nowIndex = lastNav;
        toggleSlide();
        toggleSection();
        toggleOutNav();
        toggleMoreBtn();
    }
});

resize 이벤트

window.addEventListener('resize', function(){
    if(document.body.offsetWidth <= 1180){
        if(miniGameTitle.innerText = '🎮 미니게임 🎮'){
            miniGameTitle.innerText = '🔒 모바일 미지원';
        }else{
            return false;
        }
    }else if(document.body.offsetWidth > 1180){
        if(miniGameTitle.innerText = '🔒 모바일 미지원'){
            miniGameTitle.innerText = '🎮 미니게임 🎮';
        }else{
            return false;
        }
    }
    if(document.body.offsetWidth <= 365){
        logoEl.children[1].innerText = 'Jeong';
    }else{
        logoEl.children[1].innerText = 'Jeong SeungOk';
    }
})
  • blur 이벤트 : 엘리먼트에 포커스가 있다가 사라질때 발생
  • keyup 이벤트 : 키보드를 눌렀다가 떼는 순간 발생
  • resize 이벤트 : 윈도우나 프레임의 크기가 변경되는 순간 발생

4. 미니게임

클래스를 이용해 속성, 메소드 정의

// 플레이어
class Player{
    constructor(x,y,radius,color,score){
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.score = score;
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.fillStyle = this.color
        ctx.fill();
    }
}
// 발사체
class Projectile{
    constructor(x,y,radius,color,velocity){
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.velocity = velocity;
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.fillStyle = this.color;
        ctx.fill();
    }
    update(){
        this.draw();
        this.x = this.x + this.velocity.x;
        this.y = this.y + this.velocity.y;
    }
}
// 적
class Enemy{
    constructor(x,y,radius,color,velocity){
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.velocity = velocity;
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.fillStyle = this.color;
        ctx.fill();
    }
    update(){
        this.draw();
        this.x = this.x + this.velocity.x;
        this.y = this.y + this.velocity.y;
    }
}
  • constructor에 (x,y)좌표, 반지름, 색상, 속도 속성을 정의
  • 메소드 draw()는 객체를 캔버스에 생성하고 update()를 통해 draw()를 실행하고 좌표를 변경함

적 리스폰 타임 구현

const spawnEnemies = () => {
    const spawnTime = 3000 - (100 * stage) >= 1000 ? 3000 - (100 * stage) : 1000;
    setInterval(()=>{
        const radius = Math.random() * (30 - 20) + 20;
        let x;
        let y;
        if(Math.random() < 0.5){
            x = Math.random() < 0.5 ? 0 - radius : miniGameCanvasEl.width + radius;
            y = Math.random() * miniGameCanvasEl.height;
        }else{
            x = Math.random() * miniGameCanvasEl.width;
            y = Math.random() < 0.5 ? 0 - radius : miniGameCanvasEl.width + radius;
        }
        const color = `hsl(${Math.random() * 360},50%,50%)`;
        const angle = Math.atan2(yCenter - y, xCenter - x);
        const velocity = {
            x: stage > 1 ? Math.cos(angle) * (stage * 0.7) : Math.cos(angle) * 0.5,
            y: stage > 1 ? Math.sin(angle) * (stage * 0.7) : Math.sin(angle) * 0.5
        };
        enemies.push(new Enemy(x, y, radius, color, velocity));
    },spawnTime)
}
  • spawnTime 변수를 통해 시간 간격을 정의
  • setInterval()을 통해 spawTime 간격으로 첫번째 매개변수로 정의된 함수를 실행
  • Math.random() * (30 - 20) + 20 : 20 ~ 30 사이의 난수 생성
  • Math.random() < 0.5 : 일정 확률로 x 좌표 또는 y 좌표를 0 또는 끝 지점으로 고정
  • hsl(): 색상, 채도, 명도 순으로 정의
  • atan2(): 두 좌표와 양의 X축 사이의 각도를 라디안으로 반환
  • atan2()는 -π ~ π의 라디안을 반환하고 매개변수로 음수를 허용
  • 라디안(radian): 반지름과 호의 길이가 같을 때 중심각
  • 라디안을 디그리로 표현: radian * 180 / Math.PI ( π radian = 180 degree)
  • Math.sin(), Math.cos(): 라디안으로 주어진 각도를 사인값과 코사인값으로 -1 ~ 1 사이 값을 반환

애니메이션 구현

const animate = () => {
    requestId = window.requestAnimationFrame(animate);
    ctx.fillStyle = 'rgba(0,0,0,0.1)';
    ctx.fillRect(0, 0, miniGameCanvasEl.width, miniGameCanvasEl.height);
    player.draw();
    projectiles.forEach((projectile,projectileIdx) => {
        projectile.update();

        if( projectile.x + projectile.radius < 0 ||
            projectile.x - projectile.radius > miniGameCanvasEl.width ||
            projectile.y + projectile.radius < 0 || 
            projectile.y - projectile.radius > miniGameCanvasEl.height){
            setTimeout(()=>{
                projectiles.splice(projectileIdx,1);
            },0);
        }
    });
    enemies.forEach((enemy, enemyIdx) => {
        enemy.update();
        const distance = Math.hypot(player.x - enemy.x, player.y - enemy.y);
        // End Game
        // 적과 플레이어가 접촉하거나 200점을 획득했을 경우 애니메이션 중지
        if(distance - player.radius - enemy.radius < 1){
            window.cancelAnimationFrame(requestId);
            setTimeout(()=>{
                miniGameFinishEl.classList.add('is-active')
                finishInfoEl.innerHTML = `
                    <span>Name: <input type="text" placeholder="닉네임"></span>
                `
                finishBtn.innerText = 'RESTART';
            }, 0)
        }else if(score >= 200 * stage){
            window.cancelAnimationFrame(requestId);
            setTimeout(() => {                
                startBtnEl.classList.remove('is-click');
                startBtnEl.innerText = 'NEXT STAGE';
            }, 0);
        }
        projectiles.forEach((projectile, projectileIdx) => {
            const distance = Math.hypot(projectile.x - enemy.x, projectile.y - enemy.y);
            if(distance - projectile.radius - enemy.radius < 1){
                if(enemy.radius - 10 > 10){
                    enemy.radius -= 10;
                    setTimeout(()=>{
                        projectiles.splice(projectileIdx,1);
                    },0);
                }else{
                    setTimeout(()=>{
                        score += 10;
                        pointEl.innerText = score;
                        enemies.splice(enemyIdx,1);
                        projectiles.splice(projectileIdx,1);
                    },0);
                }
            }
        })
    })
}
  • window.requestAnimationFrame() : 수행하고자 하는 애니메이션을 알리고 다음 리페인트 진행 전에 애니메이션을 업데이트하는 함수를 호출
    👉 callback을 매개변수로 받고 callback의 수는 디스플레이 주사율과 일치
    👉 callback 리스트 항목을 정의하는 고유한 id정수값을 반환
  • window.cancelAnimationFrame() : requestAnimationFrame 메소드의 콜백 요청을 취소하며 매개변수로 requestAnimationFrame 메소드의 반환값을 받음
  • ctx.fillStyle, ctx.fillRect()로 배경을 검정색으로 설정하고 알파값을 설정하여 blur 효과 발생
  • projectiles.forEach((projectile,projectileIdx)
    👉 발사체의 (x, y)좌표를 변경하고 캔버스 범위를 벗어날 경우 splice 메소드를 통해 배열에서 해당 발사체를 제거
    👉 적과 부딪힐 경우 적의 반지름 - 10의 크기가 10보다 클 경우 10을 빼고 해당 발사체 제거
  • distance
    👉 적과 플레이어의 좌표 사이의 거리
    👉 적과 발사체의 좌표 사이의 거리

발사체 이동 구현

miniGameCanvasEl.addEventListener('click',(e)=>{
    // 팝업이 떴을 경우
    if(startBtnEl.classList.contains('is-click'))
        e.preventDefault();
    const angle = Math.atan2(e.offsetY - yCenter, e.offsetX - xCenter);
    const velocity = {
        x: Math.cos(angle)*5,
        y: Math.sin(angle)*5
    };
    projectiles.push(new Projectile(xCenter, yCenter, 30, 'red', velocity));
})
  • e.offsetX, e.offsetY: 해당 노드의 가장자리에서 클릭한 포인터 사이의 x와 y의 좌표값
  • projectiles.push() : new Projectile()로 생성된 객체를 배열에 추가

START, RESTART, NEXT STAGE 구현

// 시작, 다음 스테이지 버튼
startBtnEl.addEventListener('click',()=>{
    const reset = () =>{
        ctx.clearRect(0,0,miniGameCanvasEl.width, miniGameCanvasEl.height);
        enemies.splice(0,enemies.length);
        projectiles.splice(0,projectiles.length);
        stageEl.innerText = stage;
        player.score = score;
    }
    if(startBtnEl.innerText === 'NEXT STAGE'){
        ++stage;
        reset();
    }
    startBtnEl.classList.add('is-click');
    setTimeout(()=>{
        animate();
        spawnEnemies();
    },1000);
})
// 다시 시작, 유저이름 입력
finishBtn.addEventListener('click',()=>{
    const reset = () =>{
      	stage = 1;
        score = 0;
        ctx.clearRect(0,0,miniGameCanvasEl.width, miniGameCanvasEl.height);
        enemies.splice(0,enemies.length);
        projectiles.splice(0,projectiles.length);
        stageEl.innerText = stage;
        pointEl.innerText = score;
    }
    player.score = score;
    const inputEl = finishInfoEl.querySelector('input');
    const userDataObj = {
        name: inputEl.value,
        score: player.score,
        stage: stage
    }
    rankList.push(userDataObj);
    setData(rankList);
    miniGameFinishEl.classList.remove('is-active');
    setTimeout(()=>{
        reset();
        animate();
        spawnEnemies();
        player.score = 0;
    },1000);
})
  • reset 함수
    👉 공통으로 캔버스 내부 초기화, 적 / 발사체 배열 초기화
    👉 START, NEXT STAGE 인 경우 현재 stage, score 유지
    👉 RESTART 인 경우 stage와 score을 각각 0,1로 초기화
  • userDataObj : 플레이어의 이름, 점수, 스테이지를 담은 객체
  • rankList : 플레이어 객체를 담은 배열
  • setData() : 로컬저장소에 'user'로 설정된 키의 값으로 rankList 배열을 할당함

모달창 구현

miniGameBtnEl.addEventListener('click',(e)=>{
    const target = e.target;
    if(target === modalInfoBtn){
        modalTitle.innerText = '💣게임설명';
        miniGameModalEl.classList.add('is-active');
        modalDescription.innerHTML = `
        <ul class="info--rule">
            <li>1. 점수가 200점이 되면 다음 스테이지로 넘어갑니다.</li>
            <li>2. 플레이어가 장애물과 닿는 순간 게임이 끝나게 됩니다.</li>
            <li>3. 스테이지가 올라갈수록 난이도가 올라가니 집중하세요.</li>
        </ul>
        `;
    }else if(target == modalRankBtn){
        let medal;
        modalTitle.innerText = '📊 랭킹';
        miniGameModalEl.classList.add('is-active');
        modalDescription.innerHTML = '';
        const rankListEl = document.createElement('ul');
        rankListEl.classList.add('rank--list');
        rankList.sort((a,b)=>b.score-a.score).map((user,idx)=>{
            if(idx>2)
                return false;
            if(idx === 0)
                medal = '🥇';
            else if(idx === 1)
                medal = '🥈';
            else
                medal = '🥉';
            const liEl = document.createElement('li');
            liEl.innerHTML = `${medal} ${user.name} | stage: ${user.stage} | score: ${user.score}`;
            rankListEl.append(liEl);
        })
        modalDescription.append(rankListEl);
    }else
        return false;
});
modalCloseBtn.addEventListener('click',()=>{
    miniGameModalEl.classList.remove('is-active');
})
  • modalInfoBtn, modalRankBtn을 눌렀을 때만 각각 게임설명과 순위 모달 실행
  • modalInfoBtn, modalRankBtn 각각 다른 title, description 구현
  • rankList.sort((a,b)=>b.score-a.score) : 플레이어의 점수를 기준으로 내림차순으로 정렬
  • ParentNode.append()를 이용하여 ranklistEl를 description 자식 노드에 삽입

LocalStorage에 플레이어 정보 저장하기

// 랭크 업데이트
const setData = (value) => {
    localStorage.setItem('user',JSON.stringify(value));
}
// 초기 랭크 업데이트
const initData = () => {
    const userData = JSON.parse(localStorage.getItem('user'));
    const defaultData = [
    {
        name: '정승옥',
        score: 550,
        stage: 3
    },
    {
        name: 'BOB',
        score: 450,
        stage: 3
    },
    {
        name: 'STEVE',
        score: 250,
        stage: 2
    }];
    rankList = defaultData;
    if(userData === null){
        localStorage.setItem('user',JSON.stringify(defaultData));
    }
}
initData(); // 초기 랭크 업데이트 실행
  • setData()
    👉 매개변수 value에 업데이트된 rankList 배열을 인자로 전달
    👉 setItem 메소드로 key는 'user', value는 JSON.stringify(value)로 로컬저장소에 저장
  • initData()로 defaultData 변수에 담긴 객체 원소들로 저장된 배열을 로컬저장소에 저장
  • 초기 DOM 렌더링 시 initData 함수를 실행하여 defaultData에 저장된 객체들로 순위 구현

깃허브 : https://github.com/Jeong-seungok/WECODE_self-introduction
호스팅 : https://jeong-seungok.github.io/WECODE_self-introduction/

profile
Front-End Developer 😁

0개의 댓글