본문 바로가기
Tutorial/GSAP

06. GSAP Parallax Effect : 텍스트 효과

by @webstoryboy 2023. 6. 19.
Tutorial/webd

GSAP 패럴랙스 이펙트 : 텍스트 효과

by @webs 2023. 06. 01.
06
GSAP Parallax Effect : 텍스트 효과
난이도 중간

소개

안녕하세요! 웹스토리보이입니다. 텍스트 애니메이션을 작업해보겠습니다. 스크롤을 내리면 한 줄씩 또는 한 글자씩 나오는 효과입니다. GSAP에서도 텍스트 효과를 사용할 수 있지만 유로 플러그인을 사용해야 합니다. 그래서 텍스트를 분리하는 작업은 자바스크립트로 작업하거나 글씨만 분리해주는 플러그인을 사용할 것입니다. 글씨를 하나씩 분리한 상태에서 GSAP를 작업하게 됩니다. 조금 복잡할 수도 있고, 쉬울 수도 있습니다. 그럼 같이 한번 해볼까요?

1. 기본 구조 만들기

1-1. 준비하기

GSAP를 배우는 시간이기 때문에 HTML/CSS 코딩은 생략하겠습니다. 기본 코딩은 복사해서 사용하겠습니다. 우선 웹폰트를 설정하고 GSAP에서 필요한 파일을 미리 셋팅해 놓겠습니다. GSAP는 자주 업데이트가 되기 때문에 제일 최선 버전을 사용하는게 좋습니다.

<!DOCTYPE html>
<html lang="ko">
<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>GSAP Scroll Effect</title>

    <!-- 웹폰트 설정 -->
    <link href="https://webfontworld.github.io/NexonLv1Gothic/NexonLv1Gothic.css" rel="stylesheet">
</head>
<body>

    <!-- GSAP 라이브러리 설정 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/ScrollTrigger.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.1/ScrollToPlugin.min.js"></script>
</body>
</html>

1-2. 기본 셋팅하기

소스는 그대로 복사해서 사용하겠습니다.

<!DOCTYPE html>
<html lang="ko">
<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>GSAP Scroll Effect</title>

    <link href="https://webfontworld.github.io/NexonLv1Gothic/NexonLv1Gothic.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Lato:wght@100&display=swap" rel="stylesheet">
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        a {
            color: #fff;
            text-decoration: none;
        }
        body {
            color: #fff;
            font-family: "NexonLv1Gothic";
            font-weight: 300;
            background-color: #111;
        }
        #parallax__title {
            position: fixed;
            left: 20px;
            top: 20px;
            z-index: 1000;
        }
        #parallax__title h1 {
            font-size: 30px;
            border-bottom: 1px dashed #fff;
            margin-bottom: 10px;
            padding-bottom: 5px;
            font-weight: 400;
            display: inline-block;
        }
        #parallax__title p {
            font-size: 16px;
        }
        #parallax__title ul {
            margin-top: 10px;
        }
        #parallax__title li {
            display: inline;
        }
        #parallax__title li a {
            width: 20px; 
            height: 20px;
            border-radius: 50%;
            border: 1px dashed #fff;
            display: inline-block;
            text-align: center;
            line-height: 20px;
            font-size: 12px;
        }
        #parallax__title li.active a {
            background: #fff;
            color: #000;
        }

        /* parallax__cont */
        #parallax__cont {
            max-width: 1600px;
            width: 98%;
            margin: 0 auto;
            /* background-color: rgba(255,255,255,0.1); */
        }
        .parallax__item {
            width: 1000px;
            max-width: 70vw;
            margin: 30vw auto;
            /* background-color: rgba(255,255,255,0.3); */
            text-align: left;
            margin-right: 0;
            position: relative;
            padding-top: 15vw;
        }
        .parallax__item:nth-child(even) {
            margin-left: 0;
            text-align: right;
        }
        .parallax__item__num {
            font-size: 35vw;
            position: absolute;
            left: -5vw;
            top: -13vw;
            opacity: 0.07;
            font-family: "Lato";
            font-weight: 100;
        }
        .parallax__item:nth-child(even) .parallax__item__num {
            left: auto;
            right: -5vw;
        }
        .parallax__item__title {
            padding-bottom: 5px;
            font-weight: 400;
        }
        .parallax__item__imgWrap {
            width: 100%;
            padding-bottom: 56.25%;
            background: #000;
            position: relative;
            overflow: hidden;
        }
        .parallax__item__img {
            position: absolute;
            left: -5%; 
            top: -5%;
            width: 110%;
            height: 110%;
            background-repeat: no-repeat;
            background-position: center center;
            background-size: cover;
            filter: saturate(0%);
            transition: all 1s;
        }
        .parallax__item__img:hover {
            filter: saturate(100%);
            transform: scale(1.025);
        }
        #section1 .parallax__item__img {
            background-image: url(assets/img/images01@2.jpg);
        }
        #section2 .parallax__item__img {
            background-image: url(assets/img/images02@2.jpg);
        }
        #section3 .parallax__item__img {
            background-image: url(assets/img/images03@2.jpg);
        }
        #section4 .parallax__item__img {
            background-image: url(assets/img/images04@2.jpg);
        }
        #section5 .parallax__item__img {
            background-image: url(assets/img/images05@2.jpg);
        }
        #section6 .parallax__item__img {
            background-image: url(assets/img/images06@2.jpg);
        }
        #section7 .parallax__item__img {
            background-image: url(assets/img/images07@2.jpg);
        }
        #section8 .parallax__item__img {
            background-image: url(assets/img/images08@2.jpg);
        }
        #section9 .parallax__item__img {
            background-image: url(assets/img/images09@2.jpg);
        }
        .parallax__item__desc {
            font-size: 4vw;
            line-height: 1.4;
            margin-top: -5vw;
            margin-left: -4vw;
            z-index: 100;
            position: relative;
        }
        .parallax__item:nth-child(even) .parallax__item__desc {
            margin-left: auto;
            margin-right: -4vw;
        }
    </style>
</head>
<body>
    <header id="parallax__title">
        <h1>GSAP Parallax Effect06</h1>
        <p>GSAP scrollTrigger - 텍스트 효과</p>
        <ul>
            <li><a href="gsap01.html">1</a></li>
            <li><a href="gsap02.html">2</a></li>
            <li><a href="gsap03.html">3</a></li>
            <li><a href="gsap04.html">4</a></li>
            <li><a href="gsap05.html">5</a></li>
            <li class="active"><a href="gsap06.html">6</a></li>
            <li><a href="gsap07.html">7</a></li>
            <li><a href="gsap08.html">8</a></li>
            <li><a href="gsap09.html">9</a></li>
            <li><a href="gsap10.html">10</a></li>
            <li><a href="gsap11.html">11</a></li>
            <li><a href="gsap12.html">12</a></li>
            <li><a href="gsap13.html">13</a></li>
            <li><a href="gsap14.html">14</a></li>
            <li><a href="gsap15.html">15</a></li>
        </ul>
    </header>
    <!-- //parallax__title  -->

    <main id="parallax__cont">
        <section id="section1" class="parallax__item">
            <span class="parallax__item__num">01</span>
            <h2 class="parallax__item__title">section1</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">높은 목표를 세우고, 스스로 채찍질 한다.</p>
        </section>
        <!-- //section1 -->

        <section id="section2" class="parallax__item">
            <span class="parallax__item__num">02</span>
            <h2 class="parallax__item__title">section2</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">결과도 중요하지만, 과정을 더 중요하게 생각한다.</p>
        </section>
        <!-- //section2 -->

        <section id="section3" class="parallax__item">
            <span class="parallax__item__num">03</span>
            <h2 class="parallax__item__title">section3</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">매 순간에 최선을 다하고, 끊임없이 변화한다.</p>
        </section>
        <!-- //section3 -->

        <section id="section4" class="parallax__item">
            <span class="parallax__item__num">04</span>
            <h2 class="parallax__item__title">section4</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">모든 일에는 기본을 중요하게 생각한다.</p>
        </section>
        <!-- //section4 -->

        <section id="section5" class="parallax__item">
            <span class="parallax__item__num">05</span>
            <h2 class="parallax__item__title">section5</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">열정을 잃지 않고 실패에서 실패로 걸어가는 것이 성공이다.</p>
        </section>
        <!-- //section5 -->

        <section id="section6" class="parallax__item">
            <span class="parallax__item__num">06</span>
            <h2 class="parallax__item__title">section6</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">천 마디 말보단 하나의 행동이 더 값지다.</p>
        </section>
        <!-- //section6 -->

        <section id="section7" class="parallax__item">
            <span class="parallax__item__num">07</span>
            <h2 class="parallax__item__title">section7</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">조그만 성공에 만족하지 않으며, 방심을 경계한다.</p>
        </section>
        <!-- //section7 -->

        <section id="section8" class="parallax__item">
            <span class="parallax__item__num">08</span>
            <h2 class="parallax__item__title">section8</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">나는 내가 더 노력할수록 운이 더 좋아진다는 걸 발견했다.</p>
        </section>
        <!-- //section8 -->

        <section id="section9" class="parallax__item">
            <span class="parallax__item__num">09</span>
            <h2 class="parallax__item__title">section9</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc split">꿈이 있다면, 그 꿈을 잡고 절대 놓아주지마라.</p>
        </section>
        <!-- //section9 -->
    </main>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/ScrollTrigger.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.1/ScrollToPlugin.min.js"></script>
    <script src="https://unpkg.com/split-type"></script>
    <script>
        
    </script>
</body>
</html>

2. 스크립트 작업하기

2-1. 텍스트 분리하기

분리하고 싶은 텍스트를 선택한 후 split클래스를 추가하겠습니다.

<p class="parallax__item__desc split">높은 목표를 세우고, 스스로 채찍질 한다.</p>

먼저 선택자에 있는 텍스트를 가져옵니다. split()를 통해 글씨를 하나씩 가져오고, 사이에 span을 넣어줍니다. 중간에 들어가기 때문에 닫는 태그부터 시작하고 전체에 시작태그와 끝나는 태그로 감싸줍니다. 그리고 선택자 요소한테 다시 넣어주면 글씨는 span으로 분리되어 있습니다. 개발자 도구를 통해 확인해보세요!

let text = document.querySelector(".split");
let splitText = text.innerText;
let splitWrap = splitText.split('').join('</span><span>');
text.innerHTML = splitWrap = "<span>" + splitWrap + "</span>";

2-2. 모든 텍스트 분리하기

이번에는 모든 글씨를 분리해보겠습니다. 선택자가 많으니 다중 선택을 해야겠죠!

<p class="parallax__item__desc split">높은 목표를 세우고, 스스로 채찍질 한다.</p>
<p class="parallax__item__desc split">결과도 중요하지만, 과정을 더 중요하게 생각한다.</p>
<p class="parallax__item__desc split">매 순간에 최선을 다하고, 끊임없이 변화한다.</p>
<p class="parallax__item__desc split">모든 일에는 기본을 중요하게 생각한다.</p>
<p class="parallax__item__desc split">열정을 잃지 않고 실패에서 실패로 걸어가는 것이 성공이다.</p>
<p class="parallax__item__desc split">천 마디 말보단 하나의 행동이 더 값지다.</p>
<p class="parallax__item__desc split">조그만 성공에 만족하지 않으며, 방심을 경계한다.</p>
<p class="parallax__item__desc split">나는 내가 더 노력할수록 운이 더 좋아진다는 걸 발견했다.</p>
<p class="parallax__item__desc split">나는 내가 더 노력할수록 운이 더 좋아진다는 걸 발견했다.</p>

여러개를 선택하니 querySelectorAll을 사용하겠습니다. split를 사용하여 하나씩 분리하고, join을 사용하여 중간에 span 태그를 넣어줍니다. 이번에는 웹 표준을 준수하기 위해 aria-hidden='true'를 설정했습니다. 분리된 글씨는 접근성을 위해 숨겨 놓고 부모 박스한테 aria-label을 붙여주겠습니다. 분리된 텍스트는 innerHTML을 통해 다시 넣어주고, 속성을 추가하기 위해 setAttribute를 사용했습니다.

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

2-3. 모든 텍스트 분리하기 : 여백 표현하기

모든 텍스트를 분리하는 것까지 성공했습니다. 하지만 문제는 span은 inline 구조이기 때문에 inline-block으로 성질을 변경해야 합니다. 이렇게 되면 공백이 표현이 안되기 때문에 글씨 레이아웃이 깨지게 됩니다. 물론 클래스를 통해서 설정할 수 있지만, 다시 합번 스크립트로 컨트롤 해보겠습니다. 빈 여백을 찾아서 공백 코드를 넣어주는 소스로 변경했습니다. 이렇게 하면 여백도 표현이 되기 때문에 자연스러운 텍스트 효과를 연출할 수 있습니다.

document.querySelectorAll(".split").forEach(text => {
    let theText = text.innerText;
    let newText = "";

    for(let i=0; i<text.innerText.length; i++){
        newText += "<span aria-hidden='true'>";
        if (text.innerText[i] == " "){
            newText += " "
        } else {
            newText += text.innerText[i];
        }
        newText += "</span>";
    }
    text.innerHTML = newText;
    text.setAttribute("aria-label", theText);
});

글씨는 인라인 구조이기 때문에 애니메이션을 주기 위해서는 inline-block을 꼭 주셔야 합니다. 그래야 애니메이션이 자연스럽게 움직입니다. 이 부분은 스크립트로 줄 수 있지만 소스가 복잡해지기 때문에 CSS로 설정하겠습니다.

/* option */
.split > span {
    display: inline-block;
}     

y: "100%"라고 쓸수 있지만 GSAP 스타일로 변경하겠습니다. yPercent: 100 작성하고, 투명도와 지속시간을 설정하고 움직임을 설정했습니다. stagger 속성을 이용하여 여러개의 자식을 순차적으로 나오게 설정했습니다. scrollTrigger 속성은 기존에 배웠던것과 같습니다. 이렇게 하면 애니메이션 효과가 완성됩니다. scrub 속성을 통해 애니메이션은 개인 취향에 맞추어 작업하면 됩니다. duratioinstagger 등을 통해 세밀하게 조절하면 창의적인 애니메이션을 구현할 수 있습니다.

gsap.from(".split span", {
    yPercent: 100,
    autoAlpha: 0,
    duration: 2,
    ease: "circ.out",
    stagger: 0.1,
    
    scrollTrigger: {
        trigger: ".split",
        start: "top center",
        end: "+=400",
        markers: true,
        scrub: 1, 
    }
});

2-4. 모든 텍스트 분리하기 : 여백 표현하기 : 다중 선택

하나만 글씨 효과를 적용할 수 없기 때문에 다중 선택으로 작업해보겠습니다. 여러분들도 한번 해보고 참고하면 더 좋을 것 같습니다. 스크립트의 첫번째 벽이 다중으로 변경했을 때 입니다. 이 부분이 스크립트를 처음 배우는 분들에게 가장 헷갈리는 부분입니다. 이 부분을 마스터 하면 등급이 한 등급 올라간 것입니다. gsap.utils.toArray()를 통해 여러개를 선택하고, 효과를 forEach()를 통해 줍니다. 여러개의 span을 선택해야 하기 때문에 다시 선택합니다. text.querySelectorAll("span")에 기존과 똑같이 효과를 줍니다. stagger에 옵션을 설정하면 좀 더 다양한 효과를 줄 수 있습니다. 직접 눈으로 확인해보세요!

gsap.utils.toArray(".split").forEach((text) => {
    gsap.from(text.querySelectorAll("span"), {
        yPercent: 100,
        outoAlpha: 0,
        duration: 1,
        ease: "circ.out",
        //stagger: 0.04,
        stagger: {
            amount: 1,
            from: "random"
        },
        
        scrollTrigger: {
            trigger: text,
            start: "top bottom",
            end: "+=400",
            markers: true,
        }
    });
});

2-5. split-type 사용하기

이렇게 하면 텍스트 애니메이션이 어느 정도 완성됩니다. 하지만 완벽하진 않죠! 라인 텍스트 효과라든지? 단어 텍스트 효과를 구현하기 위해서는 텍스트 분리를 더 하셔야 합니다. 소스가 복잡해지기 때문에 이번에는 split-type 플러그인을 사용해보겠습니다. 누군가가 만들어 놔서 github에 올려놨네요! 이 소스를 참고해서 다시 한번 만들어보겠습니다. 이렇게만 설정해주면 라인 효과, 단어 효과, 글씨 효과, 반응형까지 구현이 가능합니다. 이렇게만 넣으면 정말 간단하게 텍스트 애니메이션이 가능합니다. 그래도 위에 예제를 이해하고 이거를 갖다 쓰면 훨씬 더 도움이 되고, 수정이 가능합니다. SplitType에는 라인, 워드, 글자로 구분이 가능하면 해당 선택자를 적으면, 애니메이션을 라인으로, 단어별로, 글자별로 줄 수 있습니다.

<script src="https://unpkg.com/split-type"></script>
const targets = gsap.utils.toArray(".split");

targets.forEach((target) => {
    let SplitClient = new SplitType(target, { type: "lines, words, chars" });
    let lines = SplitClient.lines;
    let words = SplitClient.words;
    let chars = SplitClient.chars;

    gsap.from(words, {
        yPercent: 100,
        opacity: 0,
        duration: 1,
        ease: "circ.out",
        stagger: {
            amount: 1,
            from: "random"
        },
        scrollTrigger: {
            trigger: target,
            start: "top bottom",
            end: "+=400",
            markers: true,
        }
    });
});

3. 마무리

수고하셨습니다. 텍스트 애니메이션을 작업해보았습니다. 간단하게 만들어서 간단하게 구현도 할 수 있고, 남들이 만들어 놓은 소스를 갖다 써서 구현할 수도 있습니다. 점점 여러분의 실력이 향상되면, 플러그인을 직접 만들면 더 좋겠죠! 아무튼 이런 텍스트 효과를 응용해서 포폴 사이트에 접목한다면 한층 더 퀄리티 있는 포폴을 구현 할 수 있습니다. 오늘도 수고하셨습니다. 😊


예제 목록

댓글