코딩 공부/JAVASCRIPT

GSAP( GreenSock Animation Platform) 라이브러리

천서리 2023. 5. 5. 20:58
QUOTE THE DAY

“ 당신이 6개월 이상 한 번도 보지 않은 코드는 다른 사람이 다시 만드는 게 훨씬 더 나을 수 있다. ”

- 이글슨 (Eagleson)
반응형

 GSAP( GreenSock Animation Platform) 라이브러리

 

 

 


HTML

<header id="header">
    <h1>Seol Hee PORTFOLIO</h1>
    <nav>
        <ul>
            <li><a href="#">work</a></li>
            <li><a href="#">about</a></li>
        </ul>
    </nav>
</header>
<!-- header -->

<main id="main">
    <div class="text__inner">
        <div class="ti1">let`s <em>introduce</em></div>
        <div class="ti2 split">frontend developer's</div>
        <div class="ti3"><em>all</em> work <em>of</em> portfolio</div>
    </div>
    <div class="img__inner">
        <img class="ii1" src="img/figure01.png" alt="이미지1">
        <img class="ii2" src="img/figure02.png" alt="이미지2">
        <img class="ii3" src="img/figure03.png" alt="이미지3">
    </div>
    <div id="webgl">
        <iframe src="three.html" frameborder="0"></iframe>
    </div>
</main>
<!-- main -->

웹 페이지에서 Three.js 라이브러리를 사용하여 3D 그래픽을 렌더링하는데 필요한 코드가 포함된 "three.html" 파일을 iframe 요소로 로드하여 웹 페이지에 표시하는 것입니다.

 

CSS

<link href="https://fonts.googleapis.com/css2?family=Abel&display=swap" rel="stylesheet">

<style>
    @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-ExtendedThin';
        font-weight: normal;
        font-style: normal;
        src: url(fonts/PPNeueWorld-ExtendedThin.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;
    }
    @font-face {
        font-family: 'PPNeueWorld-SemiCondensedUltrabold';
        font-weight: normal;
        font-style: normal;
        src: url(fonts/PPNeueWorld-SemiCondensedUltrabold.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-SuperCondensedLight';
        font-weight: normal;
        font-style: normal;
        src: url(fonts/PPNeueWorld-SuperCondensedLight.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;
    }
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    body {
        width: 100%;
        height: 100vh;
        background-color: #0b130d;
        overflow: hidden;
    }
    /* header */
    #header {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        display: flex;
        align-items: flex-end;
        justify-content: space-between;
        padding: 10px 20px;
        font-family: 'Abel';
        z-index: 1000;
    }
    #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 {
        position: fixed;
        left: 50%;
        bottom: 1vw;
        transform: translateX(-50%);
        z-index: 1000;
    }
    #footer a {
        color: #fff;
        font-family: 'Abel';
        text-decoration: none;
    }
    #footer a:hover {
        text-decoration: underline;
        text-underline-position: under;
    }
    #main {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 100vh;
        position: relative;
    }
    .text__inner {
        text-align: center;
        color: #d2e3c0;
        position: relative;
        z-index: 3000;
    }
    .text__inner > div {
        font-size: 8vw;
        line-height: 1.2;
    }
    .text__inner > div.ti1 {
        font-family: 'PPNeueWorld-SemiCondensedUltrabold';
    }
    .text__inner > div.ti1 em {
        font-style: normal;
        font-family: 'PPNeueWorld-Thin';
    }
    .text__inner > div.ti2 {
        font-family: 'PPNeueWorld-SemiCondensedUltrabold';
    }
    .text__inner > div.ti3 {
        font-family: 'PPNeueWorld-Thin';
    }
    .text__inner > div.ti3 em {
        font-style: normal;
        font-family: 'PPNeueWorld-Regular';
    }
    .img__inner > img {
        position: absolute;
        width: 10vw;
        z-index: 1000;
    }
    .img__inner .ii1 {
        left: 45%;
        top: 50%;
        transform: translateY(-210%);
    } 
    .img__inner .ii2 {
        left: 10%;
        top: 50%;
        transform: rotate(-10deg);
    } 
    .img__inner .ii3 {
        left: 80%;
        top: 60%;
        transform: translateY(-30%) rotate(20deg) ;
    } 
    #webgl {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100vh;
        z-index: 1;
    }
    #webgl iframe {
        width: 100%;
        height: 100%;
    }
</style>

 


script

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
<script>
    // 글씨 분리하기
    document.querySelectorAll(".split").forEach(desc => {
        let sliteText = desc.innerText;
        let splitWrap = sliteText.split('').join("</span><span aria-hidden='true'>");
            splitWrap = "<span>" + splitWrap + "</span>";
            desc.innerHTML = splitWrap;
            desc.setAttribute("aria-label", sliteText);

        console.log(splitWrap)
    });
    // 메인 셋팅
    gsap.set(".text__inner .ti1", {opacity: 0, y: "10vh"});
    gsap.set(".text__inner .ti2 span", {opacity: 0, x: 500, scale: 10, display: "inline-block", minWith: "1.4vw"});
    gsap.set(".text__inner .ti3", {opacity: 0, y: "-10vh"});
    gsap.set(".img__inner .ii1", {opacity: 0, y: -240, scale: 10, rotation: 720});
    gsap.set(".img__inner .ii2", {opacity: 0, rotation: 360});
    gsap.set(".img__inner .ii3", {opacity: 0});
    gsap.set("#header", {opacity: 0});
    gsap.set("#footer", {opacity: 0});
    gsap.set("#webgl", {opacity: 0});

    // 메인 애니메이션
    setTimeout(() => {
        let tl = gsap.timeline();

        tl.to(".text__inner .ti2 span", {opacity: 1, x: 0, scale: 1, duration: 0.6, stagger: 0.1, ease: Power3.easeInOut})
        tl.to(".text__inner .ti1", {opacity: 1, y: 0, duration: 0.5, ease: Circ.easeOut}, "cheon +=0.5")
        tl.to(".text__inner .ti3", {opacity: 1, y: 0, duration: 0.5, ease: Circ.easeOut}, "cheon +=0.5")
        tl.to(".img__inner .ii1", {duration: 0.5, opacity: 1, scale: 1, y: -240, rotation: 0})
        tl.to(".img__inner .ii2", {duration: 0.5, opacity: 1, rotation: 0})
        tl.to(".img__inner .ii3", {duration: 0.5, opacity: 1, duration: 0.5})
        tl.to("#header", {opacity: 1})
        tl.to("#footer", {opacity: 1})
        tl.to("#webgl", {duration: 1, opacity: 1})
    }, 3000);
</script>
  1. GSAP 라이브러리를 외부 스크립트로 불러옵니다.
  2. .split 클래스를 가진 요소의 내용을 각 글자별로 분리(split)하고, 각 글자를 <span> 요소로 감싸는 작업을 수행합니다.
  3. 각 span 요소는 스크린 리더기를 사용하는 사용자들을 위해 aria-label 속성에 원본 텍스트를 포함시켜주는 작업도 수행합니다.
  4. 다양한 요소들의 초기 스타일을 지정하고, setTimeout 함수를 사용하여 페이지 로드 후 3초 후에 애니메이션이 실행되도록 지연시킵니다.
  5. GSAP의 timeline을 사용하여 각 요소들을 순차적으로 애니메이션 효과를 줍니다.
  6. 각 요소의 opacity, 위치, 회전 등의 속성을 조절하여 보다 다이나믹한 효과를 줍니다.
  7. 각 요소의 애니메이션이 시작되는 시점을 조절하기 위해 각각의 애니메이션을 "cheon" 라는 라벨로 지정하여 순서를 조절합니다.

 


three.html

<!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 onRenderFcts = [];

        //화면 설정
        var renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: true
        });

        //렌더러 설정
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        
        //카메라 설정
        var scene = new THREE.Scene();
        var camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.01, 1000);
        camera.position.z = 10;
        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)
            })
        })

        // 화면 사이즈 설정
        function resize(){
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
        window.addEventListener("resize", resize);
    </script>
</body>

</html>

 


 

<!-- 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>

Three.js는 3D 그래픽을 만들기 위한 JavaScript 라이브러리이며, Threex는 Three.js를 이용한 다양한 기능들을 제공하는 라이브러리입니다. SimplexNoise.js는 Threex에서 제공하는 노이즈 생성 라이브러리로, 지형 모양을 생성하는 데 사용됩니다. threex.terrain.js는 Threex에서 제공하는 지형 생성기입니다. 이 코드들을 사용하여 지형을 만들고 Three.js의 WebGLRenderer를 사용하여 3D 지형을 화면에 렌더링합니다.

 

 

var onRenderFcts = [];

//화면 설정
var renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});

WebGLRenderer는 Three.js에서 사용하는 3D 그래픽을 렌더링하는 WebGL을 기반으로한 렌더러입니다. 생성자 함수를 호출할 때 antialias와 alpha 값을 인자로 전달하는데, antialias는 WebGL에서 Anti-Aliasing 기능을 활성화하는데 사용되는 옵션으로, 3D 모델의 모서리를 부드럽게 처리합니다. alpha는 캔버스(렌더링 된 이미지가 출력되는 영역)의 배경을 투명하게 만드는데 사용됩니다. 이 값을 true로 설정하면 배경이 투명한 캔버스가 생성됩니다.

 

또한 onRenderFcts 배열은 렌더링 루프에서 매 프레임마다 실행할 함수를 저장하는데 사용됩니다. 이 배열에 추가된 함수는 렌더링 루프의 requestAnimationFrame() 메서드에서 호출됩니다.

 

 

 

//렌더러 설정
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
  1. 세부적으로 설명하면, setSize 메소드를 사용하여 렌더러의 크기를 현재 창의 크기(window.innerWidthwindow.innerHeight)로 설정하고, renderer.domElement를 통해 렌더러의 DOM 엘리먼트를 가져와서 현재 HTML 문서의 body 요소에 추가하고 있습니다.

이렇게 하면 Three.js로 만든 3D 씬을 렌더링할 수 있는 렌더러가 HTML 문서에 추가되어, 실제로 브라우저에서 3D 씬을 볼 수 있게 됩니다.

 

 

 

//카메라 설정
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.01, 1000);
camera.position.z = 10;
camera.position.y = 2;
scene.fog = new THREE.Fog(0x000, 0, 45);
  1. THREE.Scene()은 Three.js에서 씬(Scene)을 생성하는 생성자 함수입니다. 씬은 카메라, 빛, 오브젝트 등을 담고 있는 Three.js의 공간입니다.
  2. THREE.PerspectiveCamera()는 Three.js에서 카메라(Camera)를 생성하는 생성자 함수입니다. 첫번째 인자는 시야각(Field of view)입니다. 두번째 인자는 카메라의 종횡비(aspect ratio)입니다. 세번째와 네번째 인자는 카메라의 클리핑 가까운면(near)과 클리핑 멀리 먼면(far)입니다.
  3. camera.position은 카메라의 위치(position)를 지정하는 속성입니다. camera.position.z는 카메라의 z축 방향 위치를, camera.position.y는 카메라의 y축 방향 위치를 지정합니다.
  4. scene.fog는 씬(Scene)에 포그(fog) 효과를 추가하는 속성입니다. THREE.Fog()는 Three.js에서 포그 효과를 생성하는 생성자 함수입니다. 첫번째 인자는 포그의 색상을, 두번째 인자는 포그의 시작 거리를, 세번째 인자는 포그의 끝 거리를 나타냅니다.

 

 

// 조명 설정
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)
  1. AmbientLight: 씬 전체에 균일하게 빛을 비추는 조명으로, 일반적으로 전체적인 밝기 조절에 사용됩니다. 인자로는 색상값을 입력하며, 이 예제에서는 회색으로 지정되어 있습니다.
  2. DirectionalLight: 평행한 방향으로 빛을 내보내는 조명으로, 태양 등 자연광을 모방하는 데 사용됩니다. 첫 번째 인자는 색상값이며, 두 번째 인자는 조명의 세기를 나타내는 값입니다. position 속성을 사용하여 조명의 위치를 설정할 수 있습니다. 이 예제에서는 두 개의 DirectionalLight가 사용되었는데, 하나는 카메라 앞쪽 위에서 빛을 비추고, 다른 하나는 카메라 뒤쪽 아래에서 빛을 비추도록 설정되어 있습니다.

 

 

// 모양 설정
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
  1. 먼저 256x256 크기의 높이 맵(heightMap)을 할당하고, Simplex noise 알고리즘을 사용하여 높이 값을 계산합니다.
  2. 해당 높이 맵(heightMap)을 이용하여 지형의 Geometry를 생성합니다.
  3. 생성된 지형의 각 꼭짓점 색상을 높이값에 맞게 설정합니다.
  4. 다음으로 지형에 적용할 재질(material)을 생성합니다. 이 재질은 와이어프레임을 끄고, 색상을 "#d2e3c0"로 설정합니다. 이어서 Mesh를 생성하여 Geometry와 Material을 결합합니다.
  5. 생성된 Mesh의 위치를 (0, 0, 0)으로 이동시키고, y축 방향으로 3.5배, x축 방향으로 3배, z축 방향으로 0.20배 확대합니다.
  6. Mesh를 y축 방향으로 10배 확대합니다.
  7. 애니메이션 효과를 위해 Render loop에서 매 프레임마다 Mesh를 회전시킵니다.
  8. 생성된 Mesh를 Scene에 추가하고, Render loop를 시작합니다.

 

 

// 애니메이션 설정
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)
    })
})
  1. requestAnimationFrame 함수를 사용하여 브라우저가 다음 애니메이션 프레임을 렌더링할 때마다 animate 함수를 호출하도록 합니다.
  2. lastTimeMsec 변수는 이전 애니메이션 프레임이 렌더링된 시간을 저장하고, deltaMsec 변수는 현재 프레임과 이전 프레임 사이의 시간 간격을 계산합니다.
  3. 이 값은 각 애니메이션 프레임에서 사용됩니다. onRenderFcts 배열에는 각 프레임마다 실행될 콜백 함수들이 등록되어 있습니다.
  4. animate 함수 내부에서 onRenderFcts 배열의 모든 콜백 함수들을 호출합니다.
  5. deltaMsec / 1000는 초 단위의 시간 간격을 나타내며, nowMsec / 1000는 초 단위의 현재 시간을 나타냅니다.
  6. 각 콜백 함수는 이러한 값들을 인자로 받아 사용합니다. 마지막으로, renderer.render 함수를 호출하여 씬과 카메라를 렌더링합니다.

 

 

// 화면 사이즈 설정
function resize(){
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener("resize", resize);

브라우저 창의 크기가 변경될 때마다 호출되는 resize() 함수를 정의합니다. resize() 함수는 cameraaspect 값을 현재 창의 가로세로 비율로 설정하고, camera를 업데이트하며, renderer의 크기를 현재 창의 크기로 설정합니다.

 

이렇게 함으로써 창의 크기가 변경될 때마다 3D 장면이 적절하게 조정되어 창에 맞게 표시됩니다. 이 코드는 window 객체의 resize 이벤트에 대한 이벤트 리스너를 등록하여 창 크기가 변경될 때마다 이 함수가 호출되도록 합니다.

 

 

반응형
Adventure Time - BMO