自己紹介ページの作成


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.オフセット幅(Height):padding値、border値(幅または高さ)
  • に計算
  • pageX:ブラウザページのx座標位置に戻る
  • pageY:ブラウザページのY座標位置に戻る
  • 3. blur & keyup & resize


    ファジイイベント

    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():2つの座標と正X軸の間の角度を円弧に戻す
  • atan 2()は−π~πの弧度を返し、パラメータが負の値
  • であることを許容する.
  • 弧度:半径や弧長などの場合の中心角
  • Digreyは弧度*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の数は表示リフレッシュ率と一致します.
    👉 コールバックリスト項目を定義する一意のID
  • である整数値を返します.
  • window.cancelAnimationFrame():AnimationFrameメソッドを要求するコールバック要求をキャンセルし、パラメータとしてAnimationFrameメソッドを要求する戻り値
  • を受信する
  • ctx.fillStyle, ctx.FillRect()を使用してバックグラウンドを黒に設定し、alpha値を設定してブラー効果
  • を生成します.
  • projectiles.forEach((projectile,projectileIdx)
    👉 エミッタの(x,y)座標を変更し、キャンバスの範囲を超えた場合、エミッタは、パッチ法によってアレイから除去される.
    👉 敵と衝突した場合、敵の半径-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():新しい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は現在のステージとscoreを保持する
    👉 RESTARTの場合、ステージとscoreはそれぞれ0と1
  • に初期化されます.
  • userDataObject:プレーヤー名、スコア、ステージを含むオブジェクト
  • 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.RAnklistElをappend()を使用して記述サブノード
  • に挿入

    プレイヤー情報を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()
    👉 更新されたRankList配列をパラメータとしてパラメータ値に渡す
    👉 setItemメソッド、キーは「user」、値はJSONです.stringify(value)ローカルリポジトリ
  • に格納
  • initData()defaultData変数に格納されているオブジェクト要素の配列をローカルリポジトリ
  • に保存
  • 初期DOMレンダリング時にinitData関数を実行し、defaultDataに格納されているオブジェクト実装ランキング
  • 羽香草:https://github.com/Jeong-seungok/WECODE_self-introduction
    管理:https://jeong-seungok.github.io/WECODE_self-introduction/