본문 바로가기
Tutorial/Portfolio

GSAP, THREE.JS를 이용한 포트폴리오 메인페이지 만들기

by @webs 2022. 12. 16.
GSAP, THREE.JS를 이용한 포트폴리오 메인페이지 만들기
Tutorial/Portfolio

포트폴리오 메인 페이지 만들기 - GSAP, THREE.JS

by @webs 2022. 12. 15.
Tutorial
포폴 메인페이지 만들기
난이도 중간

소개

안녕하세요! 오늘은 포트폴리오 메인페이지를 만들어 보겠습니다. 배경에 THREE.JS1 효과와 메인 로딩애니메이션은 GSAP2를 사용할 것입니다. 사이트는 studyhall3 사이트를 참고하였으며, 폰트는 pangrampangram4에서 무료 버전과 구글폰트를 사용하였습니다. 그럼 시작해보겠습니다.


01. 기본 코딩

기본 골격부터 작업하겠습니다. 헤더, 메인, 푸터 영역을 설정하겠습니다.

<header id="header"></header><!-- //header -->
<main id="main"></main><!-- //main -->
<footer id="footer"></footer><!-- //footer -->

기본 css를 설정하겠습니다.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    width: 100%;
    height: 100vh;
    background-color: #0B130D;
}

02. 헤더/푸터 코딩

header 태그를 만들고 로고와 네비게이션을 추가하겠습니다.

<header id="header">
    <h1>web's PORTFOLIO</h1>
    <nav>
        <ul>
            <li><a href="#">work</a></li>
            <li><a href="#">about</a></li>
        </ul>
    </nav>
</header>
<!-- //header -->

위치를 상단으로 고정하고, 레이아웃은 flex를 사용하여, 양쪽 정렬 space-between을 사용했습니다. 폰트는 구글 웹폰트를 사용하였습니다.

<link href="https://fonts.googleapis.com/css2?family=Abel&display=swap" rel="stylesheet">
/* header */
#header {
    position: fixed;
    left: 0; 
    top: 0;
    width: 100%;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 20px;
    font-family: 'Abel';
    z-index: 10000;
}
#header h1 {
    font-weight: normal;
    color: #D2E3C0;
    font-size: 28px;
}
#header nav li {
    list-style: none; 
    display: inline-block;
}
#header nav li a {
    color: #D2E3C0;
    text-transform: uppercase;
    font-weight: bold;
    padding: 10px;
    font-size: 18px;
}

footer 태그도 별거 없으니 바로 만들어 보겠습니다. 살짝 메일만 보낼 수 있도록 하겠습니다.

<footer id="footer">
    <a href="https://webstoryboy.co.kr" target="_blank">webstoryboy.co.kr</a>
</footer>
<!-- //footer -->

css도 별건 없습니다. 가운데로 위치해주기 위해서 translateXabsolute를 사용하였습니다. 간격은 반응형까지 고려해서 1vw를 사용했습니다. 폰트는 구글 폰트의 Abel 폰트를 사용했습니다. 여기까지 잘 따라오셨죠 😃

/* footer */
#footer {
    position: absolute;
    left: 50%;
    bottom: 1vw;
    z-index: 1000;
    transform: translateX(-50%);
}
#footer a {
    color: #fff;
    font-family: 'Abel';
    text-decoration: none;
}

03. 메인 텍스트 코딩

메인 텍스트 코딩을 시작하겠습니다. text__inner 클래스를 만들어서 3개의 박스를 만들고 글씨를 넣어줬습니다. 강조하고 싶은 부분은 em 태그를 사용하였고, 차후 애니메이션을 주기 위해서 각자 클래스 이름을 주었습니다. 또한 한글자씩 나오게 효과를 주기 위해서 한줄에 하나의 박스로 구성하였습니다.

<main id="main">
    <div class="text__inner">
        <div class="ti1">let’s <em>introduce</em></div>
        <div class="ti2">frontend developer’s</div>
        <div class="ti3"><em>all</em> works <em>of</em> portfolio</div>
    </div>
</main>

디자인을 작업을 들어가기 전에 웹 폰트를 먼저 설정해야 합니다. 폰트는 eiko 폰트와, neue-world 폰트를 다운 받았습니다. 상업적으로 쓰면 유로지만 개인적인 포폴이나 작업물은 무료이기 때문에 사용했습니다. 자세한건 라이센스 꼭 확인해보세요! 🤭 유튜브에서 교육용으로 쓰는건 무료인지 모르겠네요! 그래도 여러분을 위해서 사용해보겠습니다.

폰트 무료 버전을 클릭하시면 이메일로 다운로드 경로가 옵니다. 무료 버전 폰트는 모든 폰트를 다 주는 것이 아니고, 일부만 샘플로 사용할 수 있도록 한것 같습니다. otf 또는 ttf 파일을 제공하는데, 이 폰트 파일을 woff2 파일로 변경해야 웹 폰트로 사용이 가능합니다. 웹 폰트 변경 사이트5에서 폰트를 변경하고 폰트 폴더에 해당 폴더를 넣습니다.

@font-face {
    font-family: 'PPEiko-BlackItalic';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPEiko-BlackItalic.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPEiko-LightItalic';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPEiko-LightItalic.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPNeueWorld-CondensedRegular';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPNeueWorld-CondensedRegular.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPNeueWorld-SemiExtendedBlack';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPNeueWorld-SemiExtendedBlack.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPNeueWorld-SemiCondensedUltrabold.woff2';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPNeueWorld-SemiCondensedUltrabold.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPNeueWorld-Thin';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPNeueWorld-Thin.woff2') format('woff2');
    font-display: swap;
}
@font-face {
    font-family: 'PPNeueWorld-Regular';
    font-weight: normal;
    font-style: normal;
    src: url('fonts/PPNeueWorld-Regular.woff2') format('woff2');
    font-display: swap;
}

웹 폰트는 제일 위에 선언해야 작동이 되니, 이 점 주의 바랍니다. 웹 폰트를 설정했으면, 사용을 해보겠습니다. 가운데 정렬을 위해서 flew를 사용하였으며, 중앙 정렬을 위하여 height: 100vh를 설정했습니다. font-family를 설정하면, 사용자 컴퓨터에 해당 폰트가 없어도, 사용 할 수 있습니다.

/* main */
#main {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
}
.text__inner {
    text-align: center;
}
.text__inner > div {
    font-size: 8vw;
    line-height: 1;
    color: #D2E3C0
}
.text__inner > div.ti1 {
    font-family: 'PPNeueWorld-Thin';
}
.text__inner > div.ti1 em {
    font-family: 'PPNeueWorld-Regular';
    font-style: normal;
}
.text__inner > div.ti2 { 
    color: #fff;
    font-family: 'PPNeueWorld-SemiCondensedUltrabold.woff2';
}
.text__inner > div.ti3 {
    font-family: 'PPNeueWorld-Regular';
}
.text__inner > div.ti3 em {
    font-family: 'PPNeueWorld-Thin';
    font-style: normal;
}

04. 메인 이미지 코딩

이미지도 추가해주겠습니다. 이미지는 studyhall.design3 사이트에서 다운 받았습니다. 같은거 쓰셔도 되구요! 다른거 쓰셔도 됩니다. 이미지 셋팅도 다음과 같이 해주겠습니다.

<div class="img__inner">
    <img class="ii1" src="img/figure01.png" alt="figure01">
    <img class="ii2" src="img/figure02.png" alt="figure02">
    <img class="ii3" src="img/figure03.png" alt="figure03">
</div>
/* img__inner */
.img__inner > img {
    position: absolute;
    width: 10vw;
}
.img__inner .ii1 {
    left: 45%;
    top: 50%;
    transform: translateY(-210%);
}
.img__inner .ii2 {
    left: 10%;
    top: 50%;
    transform: translateY(-100%);
}
.img__inner .ii3 {
    left: 80%;
    top: 50%;
}

텍스트가 이미지 위에 올라오게 하기 위해서 z-index를 추가하였습니다.

.text__inner {
    text-align: center;
    position: relative;
    z-index: 100;
}

여기까지 기본 코딩이 완료되었습니다. 반응형을 고려하여 작성하였기 때문에 모바일용도 크게 틀어지는 부분이 없습니다. 이제 여기에 메인 애니메이션 효과와 Three.js 효과를 줄 것입니다. 이 부분부터는 조금 어려울 수 있으니 천천히 잘 따라오세요! 🤗


05. 메인 GSAP 애니메이션

우선 GSAP를 설정하기 위해서는 다음과 같은 파일이 연동되어 있어야 합니다. CDN 파일을 추가하고 애니메이션을 만들어보겠습니다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js"></script>

가운데 글씨는 하나씩 쪼개주기 위해서 span태그로 만들어줘야 합니다. 자바스크립트를 통해 설정하겠습니다. 접근성을 위해서 aria 효과도 추가해주었습니다.

document.querySelectorAll(".split").forEach(desc => {
    let splitText = desc.innerText;
    let splitWrap = splitText.split('').join("</span><span aria-hidden='true'>");
        splitWrap = "<span aria-hidden='true'>" + splitWrap + "</span>";
        desc.innerHTML = splitWrap;
        desc.setAttribute("aria-label", splitText);
})

애니메이션을 주기 위한 기본 CSS 세팅은 gsap.set을 통해 설정했습니다. CSS에서 설정을 할 수 있지만, CSS가 복잡해지고 수정이 어려워, GSAP를 통해 설정했습니다. 움직이기 전 요소들은 set을 통해 설정하고, setTimeout 메서드를 통해 2초 후에 작동하도록 설정했습니다. 변수에 타임라인을 설정하여, 모든 애니메이션은 순서대로 나오도록 했습니다. 동시에 나와야 하는 부분은 "text"처럼 같은 이름을 설정해주면 됩니다.

// 메인 GSAP 애니메이션
gsap.set(".text__inner .ti2 span", {opacity: 0, display: "inline-block", x: 500, minWidth: "1.4vw"});
gsap.set(".text__inner .ti1", {opacity: 0, y: 100});
gsap.set(".text__inner .ti3", {opacity: 0, y: -100});
gsap.set(".img__inner .ii1", {opacity: 0, scale: 1.4, y: "-210%", rotation: 0});
gsap.set(".img__inner .ii2", {opacity: 0, scale: 1.4, y: "-100%", rotation: 0});
gsap.set(".img__inner .ii3", {opacity: 0, scale: 1.4, rotation: 0});
gsap.set("#header", {y: -100});
gsap.set("#footer", {y: 100});

setTimeout(() => {
    let tl = gsap.timeline();

    tl.to(".text__inner .ti2 span", {duration: 0.6, x: 0, stagger: 0.09, scale: 1, opacity: 1, ease: Power3.easeInOut})
    tl.to(".text__inner .ti1", {duration: 0.5, y:0, opacity: 1, ease: Circ.easeOut}, "text")
    tl.to(".text__inner .ti3", {duration: 0.5, y:0, opacity: 1, ease: Circ.easeOut}, "text")
    tl.to(".img__inner .ii1", {duration: 0.5, scale:1, rotation: 360, opacity: 1, ease: Power3.easeOut})
    tl.to(".img__inner .ii2", {duration: 0.5, scale:1, rotation: 360, opacity: 1, ease: Power3.easeOut})
    tl.to(".img__inner .ii3", {duration: 0.5, scale:1, rotation: 360, opacity: 1, ease: Power3.easeOut})
    tl.to("#header", {duration: 0.5, y:0, ease: Power3.easeOut}, "side")
    tl.to("#footer", {duration: 0.5, y:0, ease: Power3.easeOut}, "side")
}, 2000)

06. THREE.JS 연동하기

이번에는 배경이 조금 심심하기 때문에 Three.js를 구현해보겠습니다. Three.js를 직접 작성하기에는 시간이 오래 걸리고, 어렵기 때문에 차후에 만드는 법을 따로 올릴 예정이구요. 오늘은 간단하게 가져와서 사용하는 법만 해보겠습니다.

main 섹션에 다음 코드를 추가하고 CSS로 영역을 잡겠습니다.

<div id="webgl">
    <iframe src="three.html" frameborder="0"></iframe>
</div>
/* webgl */
#webgl {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100vh;
    z-index: 1;
}
#webgl iframe {
    width: 100%;
    height: 100%;
}

iframe.html 파일을 하나 만들고, webgl 사이트 https://wiss.tistory.com/502 에서 파일을 가져와서 iframe에 추가하겠습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0px;
            background-color: #0B130D;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <!-- Using Threejs & Jerome Etienne's Threex -->
    <script src='https://jeromeetienne.github.io/threex.terrain/examples/vendor/three.js/build/three-min.js'></script>
    <script src='https://jeromeetienne.github.io/threex.terrain/examples/vendor/three.js/examples/js/SimplexNoise.js'></script>
    <script src='https://jeromeetienne.github.io/threex.terrain/threex.terrain.js'></script>
    <script>
        var renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: true
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        
        var onRenderFcts = [];
        var scene = new THREE.Scene();
        var camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.01, 1000);
        camera.position.z = 15;
        camera.position.y = 2;
        scene.fog = new THREE.Fog(0x000, 0, 45);;

        // 조명 설정
        var light = new THREE.AmbientLight(0x202020)
        scene.add(light)
        var light = new THREE.DirectionalLight('white', 5)
        light.position.set(0.5, 0.0, 2)
        scene.add(light)
        var light = new THREE.DirectionalLight('white', 0.75 * 2)
        light.position.set(-0.5, -0.5, -2)
        scene.add(light)
        
        var heightMap = THREEx.Terrain.allocateHeightMap(256, 256)
        THREEx.Terrain.simplexHeightMap(heightMap)
        var geometry = THREEx.Terrain.heightMapToPlaneGeometry(heightMap)
        THREEx.Terrain.heightMapToVertexColor(heightMap, geometry)
        var material = new THREE.MeshBasicMaterial({
            wireframe: false,
            color: "#D2E3C0"
        });
        var mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);
        mesh.lookAt(new THREE.Vector3(0, 1, 0));

        mesh.scale.y = 3.5;
        mesh.scale.x = 3;
        mesh.scale.z = 0.20;
        mesh.scale.multiplyScalar(10);

        onRenderFcts.push(function (delta, now) {
            mesh.rotation.z += 0.02 * delta;
        })
        onRenderFcts.push(function () {
            renderer.render(scene, camera);
        })
        var lastTimeMsec = null
        
        requestAnimationFrame(function animate(nowMsec) {
            requestAnimationFrame(animate);
            lastTimeMsec = lastTimeMsec || nowMsec - 1000 / 60
            var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
            lastTimeMsec = nowMsec
            onRenderFcts.forEach(function (onRenderFct) {
                onRenderFct(deltaMsec / 1000, nowMsec / 1000)
            })
        })
    </script>
</body>

</html>

마지막으로 애니메이션과 위치 등을 설정하면, 메인 페이지가 완성됩니다. 포트폴리오 메인 페이지를 GSAP와 Three.js를 이용하여 간단하게 꾸며보았습니다. 간단하게 맛만 본 구성이라고 생각하시면 되구, 앞으로 GSAP 애니메이션과 Three.js를 좀 더 구체적으로 공부해보겠습니다. 여기까지 성공하시 분은 댓글은 주소와 작품을 올려주시고, 서로 피드백을 받으면서 더 멋있는 포트폴리오를 만들었으면 좋겠습니다.

마지막으로 막히는 부분이나 에러나는 부분들은 댓글로 달아주시고, 전체 소스나 완성 소스를 보시고 안되는 부분들을 확인해주세요.


참고(Reference)


댓글0