개발/js

[JS] model-viewer 사용법 3 - model control 하기 (zoom in/out, animation toggle, full screen)

waterpole-dev 2022. 9. 14. 16:50
반응형

이전 글 보기 (더보기 클릭)

더보기

 

 

저번 글에서 로드된 모델의 사용 가능한 애니메이션이 있는지 체크 후 있다면 model-veiwer에 animation-name 속성을 추가해주는 준비 단계에 이어서 이번 포스팅에서는

이런 버튼들을 만들어 각 버튼에 맞는 기능을 추가해 보겠습니다.

 

아래 모든 소스는 이전 포스팅을 이어갑니다.


버튼 만들기

<model-viewer id="model-ex" camera-controls>
    <!-- new -->
    <div class="model-nav">
        <div class="model-control">
            <button class="zoom-in-btn">+</button>
            <button class="zoom-out-btn">-</button>
            <button class="animation-btn">ani</button>
            <button class="fullscreen-btn">full screen</button>
        </div>
        <div class="open-ar">
            <button class="open-ar-btn">AR</button>
        </div>
    </div>
</model-viewer>

우선 <model-viewer> 안에 컨트롤 버튼이 들어갈 영역을 만들어줍니다.


줌 인/아웃 기능 구현

zoom in/out은 공식 문서에 있는 zoom methods를 사용합니다.

문서 설명에 보면

키보드 또는 마우스 휠 입력을 모방하여, 확대/축소 동작을 적용합니다.

라고 나와있습니다. (영어를 잘 못해서 이게 정확하게 맞나...? 싶지만.. 여하튼)

 

즉, zoom(10)의 경우 확대 zoom(-10)은 축소가 됩니다.

// new function
function modelController() {
    const zoomInBtn = document.querySelector('.zoom-in-btn');
    const zoomOutBtn = document.querySelector('.zoom-out-btn');
    
    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(5);
    });

    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(-5);
    });
}

해당 기능은 손쉽게 구현이 가능합니다.


애니메이션 토글 기능 구현

저번 포스팅에서 로드된 모델의 사용 가능한 애니메이션이 있는지 체크 후 있다면 model-veiwer에 animation-name 속성을 추가해주는 준비 단계까지 맞추어놓은 상태가 되어 있어야 합니다.

function modelController() {
    const zoomInBtn = document.querySelector('.zoom-in-btn');
    const zoomOutBtn = document.querySelector('.zoom-out-btn');
    // new variable
    const animationBtn = document.querySelector('.animation-btn');
    
    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(5);
    });

    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(-5);
    });
    
    // new eventListener
    animationBtn.addEventListener('click', () => {
        if (animationArr.length > 0) {
            MODEL_VIEWER.classList.toggle('activeAni');
            toggleAnimation();
        }
    });
}

// new function
function toggleAnimation() {
    const isActive = MODEL_VIEWER.classList.contains('activeAni');

    if (isActive) {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[animationArr.length - 1]);
    } else {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[0]);
    }
}

애니메이션 토글 버튼을 클릭할 때마다 한번 더 animationArr가 존재하는지 확인 후

현재 애니메이션이 활성화되었는지 체크하기 위해 'activeAni' 클래스를 toggle 시켜줍니다.

 

'toggleAnimation' 함수에서 toggle 시켰던 'activeAni' 클래스를 가지고 있는지 확인하는 변수 'isActive'를 만들어 줍니다.

이제 isActive 변수를 통해 조건문으로 true라면 aniamtionArr의 마지막, false라면 첫 번째(기본)로 animation-name 속성을 설정해줍니다.

 

저는 저번 포스팅에서도 말했듯 제가 사용한 모든 model 파일은 사용 가능한 animation이 2개로 고정되어있는데 가끔 사용하지 않는 애니메이션이 중간에 포함돼서 3개가 들어있는 경우가 있어서 [0]으로 첫 번째는 고정, Array.length - 1로 배열의 length가 2이던 3이던 마지막 요소를 선택하도록 해놨습니다.

본인 프로젝트에 맞게 바꾸시면 될 거 같습니다.


풀 스크린 기능 구현하기

function modelController() {
    const zoomInBtn = document.querySelector('.zoom-in-btn');
    const zoomOutBtn = document.querySelector('.zoom-out-btn');
    const animationBtn = document.querySelector('.animation-btn');
    // new variable
    const fullScreenBtn = document.querySelector('.fullscreen-btn');
    
    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(5);
    });

    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(-5);
    });
    
    animationBtn.addEventListener('click', () => {
        if (animationArr.length > 0) {
            MODEL_VIEWER.classList.toggle('activeAni');
            toggleAnimation();
        }
    });
    
    // new eventListener
    fullScreenBtn.addEventListener('click', () => {
        toggleFullScreen(MODEL_VIEWER.parentElement);
    });
}

// new function
function toggleFullScreen(element) {
    const agent = navigator.userAgent.toLowerCase();
    const target =
        agent.indexOf('safari') > -1
            ? !document.webkitFullscreenElement
            : !document.fullscreenElement;
            
    if (target) {
        if (element.requestFullscreen) return element.requestFullscreen();
        if (element.webkitRequestFullscreen) return element.webkitRequestFullscreen();
        if (element.mozRequestFullScreen) return element.mozRequestFullScreen();
        if (element.msRequestFullscreen) return element.msRequestFullscreen();
    } else {
        if (document.exitFullscreen) return document.exitFullscreen();
        if (document.webkitExitFullscreen) return document.webkitExitFullscreen();
        if (document.mozCancelFullScreen) return document.mozCancelFullScreen();
        if (document.msExitFullscreen) return document.msExitFullscreen();
    }
}

풀 스크린은 엄청 많은 예제가 있기에 자세한 설명은 생략하겠습니다.

 


현재 최종 소스

<button class="open-model-btn" data-product-name="robot">3D - 01</button>
<button class="open-model-btn" data-product-name="phone">3D - 02</button>
<button class="open-model-btn" data-product-name="car">3D - 03</button>

<div class="mv-modal-box">
    <button class="close-modal-btn">X</button>
    <model-viewer id="model-ex" camera-controls>
        <div class="model-nav">
            <div class="model-control">
                <button class="zoom-in-btn">+</button>
                <button class="zoom-out-btn">-</button>
                <button class="animation-btn">ani</button>
                <button class="fullscreen-btn">full screen</button>
            </div>
            <div class="open-ar">
                <button class="open-ar-btn">AR</button>
            </div>
        </div>
    </model-viewer>
</div>
const MV_MODAL_BOX = document.querySelector('.mv-modal-box');
const MODAL_VIEWER = document.querySelector('#model-ex');
const modelOpenBtn = document.querySelector('.open-model-btn');
const modalCloseBtn = document.querySelector('.close-modal-btn');

let animationArr = [];

MODEL_VIEWER.addEventListener('progress', (e) => {
    if (e.detail.totalProgress === 1) {
        MODEL_VIEWER.cameraOrbit = '0deg 75deg 105%';
        animationArr = MODEL_VIEWER.availableAnimations;
    }
});

modelOpenBtn.addEventListener('click', (e) => {
    const target = e.currentTarget;
    const productName = target.getAttribute('data-product-name');
    
    MV_MODAL_BOX.style.display = 'block';
    loadModel(productName)
});

modalCloseBtn.addEventListener('click', () => {
    MODEL_VIEWER.removeAttribute('src');
    MODEL_VIEWER.removeAttribute('animation-name');
    animationArr = [];
    MV_MODAL_BOX.style.display = 'none';
});

function loadModel(name) {
    const modelSrc = `./assets/model/${name}.glb`;
    MODEL_VIEWER.setAttribute('src', modelSrc);
}

function setAnimation() {
    if (animationArr.length > 0) {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[0]);
    } else {
        console.log("사용 가능한 애니메이션이 없습니다.")
    }
}

function modelController() {
    const zoomInBtn = document.querySelector('.zoom-in-btn');
    const zoomOutBtn = document.querySelector('.zoom-out-btn');
    const animationBtn = document.querySelector('.animation-btn');
    const fullScreenBtn = document.querySelector('.fullscreen-btn');
    
    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(5);
    });

    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(-5);
    });
    
    animationBtn.addEventListener('click', () => {
        if (animationArr.length > 0) {
            MODEL_VIEWER.classList.toggle('activeAni');
            toggleAnimation();
        }
    });
    
    fullScreenBtn.addEventListener('click', () => {
        toggleFullScreen(MODEL_VIEWER.parentElement);
    });
}

function toggleAnimation() {
    const isActive = MODEL_VIEWER.classList.contains('activeAni');

    if (isActive) {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[animationArr.length - 1]);
    } else {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[0]);
    }
}

function toggleFullScreen(element) {
    const agent = navigator.userAgent.toLowerCase();
    const target =
        agent.indexOf('safari') > -1
            ? !document.webkitFullscreenElement
            : !document.fullscreenElement;
            
    if (target) {
        if (element.requestFullscreen) return element.requestFullscreen();
        if (element.webkitRequestFullscreen) return element.webkitRequestFullscreen();
        if (element.mozRequestFullScreen) return element.mozRequestFullScreen();
        if (element.msRequestFullscreen) return element.msRequestFullscreen();
    } else {
        if (document.exitFullscreen) return document.exitFullscreen();
        if (document.webkitExitFullscreen) return document.webkitExitFullscreen();
        if (document.mozCancelFullScreen) return document.mozCancelFullScreen();
        if (document.msExitFullscreen) return document.msExitFullscreen();
    }
}

modelController()

 

다음 포스팅에선 마지막으로 AR버튼을 추가해보겠습니다.


이전 포스팅들과 다르게 기능 추가시마다 계속 소스를 이어나가는 게 아닌, 해당 추가 기능의 소스만 적은 후 마지막에 최종 소스를 한번 보여주는 방식으로 포스팅을 해봤는데 어떤 게 더 보기 편하신가요?
보기 편한 방식에 대해서 조언 남겨주시면 정말 감사하겠습니다!!

util, ui등 평소 개발 스타일대로 포스팅 소스의 파일을 나눠서 export/import 해서 진행하는 것은 포스팅 가독성을 떨어뜨린다고 판단해서 해당 포스팅은 별도의 리팩토링 없이 한 파일에 진행됩니다.

 

피드백, 질문, 댓글 언제나 환영입니다!

감사합니다 :)

반응형