이번 프로젝트에서는 아래와 같은 문단에서 각 텍스트 라인이 한 라인씩 순차적으로 아래에서 스르륵 올라오는 애니메이션을 구현하려고 한다. 중요 포인트는 한 라인씩 나타나게 하는 것인데, 다양한 화면 크기에서 문단이 한 라인씩 배치되려면 화면 크기에 따라 동적으로 텍스트의 줄바꿈이 일어나야 한다.
It comes in 4 preset cuts, Sans, Serif, Glare, and Text, each with unique personalities and quirks. Each weight counts 581 glyphs with plenty of alternate symbols to achieve the best-desired result for your next design.
그리고 애니메이션이 실행될 때, 각 라인이 해당 영역을 벗어나지 않도록 overflow: hidden
속성을 적용해야 하기 때문에 각 라인을 div
로 한번 더 감싸주어야 한다. 여기서는 animation-wrapper
라는 클래스를 사용하여 각 라인을 감싸주려고 한다.
코드를 살펴보자!
function dynamicWrapLines() {
const elementArray = document.querySelectorAll(".dynamic-paragraph");
elementArray.forEach((element) => {
const content = element.innerText; // 문단의 내용
const wordArray = content.split(" "); // 공백을 기준으로 단어를 잘라서 배열 생성
let line = "";
element.innerHTML = "";
wordArray.forEach((word) => {
const testLine = line + word + " "; // 가로너비 비교를 위한 테스트 라인 생성
const tempSpan = document.createElement("span"); // 임시 span 생성
tempSpan.style.position = "absolute";
tempSpan.style.left = "-9999px";
tempSpan.style.visibility = "hidden";
tempSpan.innerText = testLine;
document.body.appendChild(tempSpan); // body에 tempSpan 추가
// tempSpan의 가로너비와 해당 요소의 가로너비 비교
if (tempSpan.offsetWidth > element.offsetWidth) {
const newLine = document.createElement("span"); // tempSpan의 너비가 크다면 새로운 라인 생성
const animationWrapper = document.createElement("span"); // 애니메이션 적용을 위한 부모 wrapper 생성
animationWrapper.className = "animation-wrapper";
newLine.className = "line";
newLine.innerText = line; // line에는 현재 단어 이전까지의 텍스트가 담겨 있음
element.appendChild(animationWrapper); // 해당 요소에 부모 wrapper를 먼저 추가
animationWrapper.appendChild(newLine); // 최종적으로 부모 wrapper에 라인을 자식으로 추가
line = word + " "; // 라인에 현재 단어를 넣어서 초기화
} else {
line = testLine;
}
document.body.removeChild(tempSpan); // body에서 tempSpan 제거
});
const lastLine = document.createElement("span");
const lastWrapper = document.createElement("span"); // 애니메이션 적용을 위한 부모 wrapper 생성
lastWrapper.className = "animation-wrapper";
lastLine.className = "line";
lastLine.innerText = line;
element.appendChild(lastWrapper); // 해당 요소에 부모 wrapper를 먼저 추가
lastWrapper.appendChild(lastLine);
});
}
코드를 간단하게 설명하자면 먼저,
.dynamic-paragraph
클래스를 가진 모든 요소를 선택하고, 각 문단의 내용을 공백을 기준으로 분리하여 배열을 생성한다. 이 배열은 문단의 각 단어를 순회하며 라인을 구성하는 데 사용된다.
줄바꿈이 필요한 지점을 체크하기 위해 임시 span을 생성하고, 단어를 추가할 때마다 가로 너비를 비교한다. 임시 span의 너비가 해당 요소(문단)의 가로 너비보다 크면 현재 단어 이전까지의 텍스트를 newLine
에 담고 그 라인을 문단에 추가한다.
이제 이 함수의 호출 시점이 중요한데, 여기서 문제가 발생했다.
tempSpan.offsetWidth
가 새로고침할 때마다 다르게 측정되는 문제가 발생했다. 원인을 추측해보자면,
1) tempSpan
이 생성되고 측정되는 시점이 페이지의 로딩 상태에 따라 달라질 수 있다.
2) css 스타일이 tempSpan
에 적용되기 전에 크기를 측정하면, 잘못된 너비값을 얻을 수 있다.
3) tempSpan
을 추가하고 바로 크기를 측정하는 경우, 브라우저가 아직 레이아웃 계산을 완료하지 않았을 수 있다.
웹페이지 초기 진입 시에 tempSpan의 너비가 제대로 측정되지 않는 점, 해당 페이지에서 새로고침을 한 후에는 tempSpan의 너비가 올바르게 측정된다는 점으로 미루어보아 페이지 로딩 상태에 따른 문제일 것으로 추측, window.onload를 사용하여 웹페이지가 완전히 로드되었을 때 해당 함수를 실행하도록 수정하여 문제를 해결하였다.
window.addEventListener("load", function () {
dynamicWrapLines();
});
DOMContentLoaded VS window.onload
- DOMContentLoaded: HTML 문서가 완전히 로드되고 파싱됐을 때 발생한다. 이 시점에는 모든 HTML이 로드되고, DOM 트리가 구축되었지만, 스타일시트, 이미지, 프레임 등 다른 리소스의 로딩은 완료되지 않았을 수 있다.
- window.onload: 페이지의 모든 리소스(HTML, 스타일시트, 스크립트, 이미지, 프레임 등)가 로드되고, 페이지 로딩이 완전히 끝난 후에 발생한다.
opacity를 이용해 텍스트가 서서히 나타나는 애니메이션을 구현하고자 하는데, 텍스트가 랜덤하게 나타나게 하려면 어떻게 할 수 있을까?
gsap의 sttager
와 random
옵션을 사용하면 가능하다. 사용 방법은 아래와 같다.
const introMotion = gsap.timeline(); // gsap 타임라인 생성
introMotion.from(".sc-main .scatter-letters .scatter-item", {
opacity: 0, // opacity 0에서 시작한다.
stagger: {
from: "random", // 애니메이션의 순서를 무작위로 지정한다.
each: 0.05, // 각 요소 사이에 0.05초 지연 시간 적용
},
delay: 1, // 애니메이션의 지연 시간(1초 대기 후 시작)
duration: 3, // 애니메이션의 총 지속 시간
ease: "power2.out", // 부드러운 ease 효과 적용
})
여기서 sttager에 대해 좀 더 알아보자!
💡 sttager란?
stagger는 여러 요소에 대한 애니메이션을 순차적으로 지연시켜 실행하는 기능이다. stagger를 사용하면 동일한 애니메이션을 여러 요소에 적용하면서 각 요소마다 약간의 지연을 두어 순차적으로 애니메이션을 실행할 수 있다.
✅ stagger의 옵션
each: 각 요소 사이의 지연 시간
예를 들어,each: 0.1
은 한 요소가 애니메이션된 후 다음 요소가 0.1초 뒤에 애니메이션되도록 설정한다.
from: 애니메이션이 시작될 요소의 위치
예를 들어,from: "start"
는 첫 번째 요소에서 시작하여 순차적으로 애니메이션하고,from: "end"
는 마지막 요소에서 시작한다. 내가 사용한from: "random"
은 무작위 순서로 애니메이션한다.
amount: 전체 stagger 애니메이션의 총 지속 시간
이 옵션을 사용하면 개별 지연 시간(each) 대신 전체 애니메이션 시간을 기반으로 각 요소의 지연 시간이 계산된다.
다음으로 텍스트가 깨져서 흩어지는 듯한 애니메이션을 구현하고자 한다. gsap 라이브러리와 css로 해당 애니메이션을 구현할 수 있다.
원하는 시점에 해당 요소에 class를 추가하여 css와 결합한다. 이후 설정은 css에서 해주면 된다.
introMotion.add(() => {
$(".sc-main .scatter-letters .scatter-item").addClass("on"); // 클래스 추가
})
각각의 텍스트 조각이 특정한 위치로 이동하도록 transform: translate(X, Y)
값을 설정해주면 된다. 필요하다면 rotate()
값도 추가한다.
@keyframes scaleAndMoveAnimation { /* keyframes 애니메이션 선언 */
10% {
transform: translate(0, 0) rotate(0) scale(0.9);
}
15% {
transform: translate(0, 0) rotate(0) scale(1);
}
100% {
transform: var(--move-transform);
}
}
.sc-main .scatter-letters .scatter-item.on { /* 해당 요소에 애니메이션 설정 */
animation: scaleAndMoveAnimation cubic-bezier(0.165, 0.84, 0.44, 1) forwards,
floatAnimation cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite;
}
그런데 깨진 텍스트 조각이 많기 때문에 각 요소마다 애니메이션을 별도로 설정하는 것은 번거롭고 비효율적이다. 이 때 css 변수를 선언해서 변수에 각 요소의 이동 지점을 담아준다. 아래와 같이 하면 중복 코드를 줄일 수 있다.
.sc-main .scatter-letters .f1.on {
/* 이동할 지점의 위치값을 변수에 담아준다. */
--move-transform: translateX(-50vw) translateY(-40vh) rotate(10deg);
animation-duration: 3s, 4.68s; /* 두 개의 애니메이션에 대한 각각의 지속 시간 */
animation-delay: 0s, 3s; /* 두 개의 애니메이션에 대한 각각의 지연 시간*/
}
.sc-main .scatter-letters .f2.on {
--move-transform: translateX(-50vw) translateY(-20vh) rotate(45deg);
animation-duration: 3s, 5.34s;
animation-delay: 0s, 3s;
}
.sc-main .scatter-letters .f3.on {
--move-transform: translateX(-20vw) translateY(-50vh) rotate(-45deg);
animation-duration: 3s, 5.82s;
animation-delay: 0s, 3s;
}
.
.
.
생략
1-2와 이어서 이동한 지점에서 둥둥 떠다니는 듯한 애니메이션 효과를 주려면 어떻게 할까? 포인트는 기존의 위치에서가 아닌, 이동한 지점에서 해당 애니메이션을 적용해야 하는 것이다. 아래와 같이 할 수 있다.
@keyframes floatAnimation {
0% {
transform: var(--move-transform);
}
50% {
transform: var(--move-transform) translateY(10%);
}
100% {
transform: var(--move-transform);
}
}
이동한 지점의 좌표값을 담은 변수를 넣어주기만 하면 간단하게 구현할 수 있다.
여러개의 텍스트가 그 자리에서 롤링되는 애니메이션은 어떻게 구현할 수 있을까?
먼저 html 구조를 살펴보자.
<div class="first-line">
<div class="letter-p1">
<span class="default-letter sans">P</span>
<div class="rolling-letters">
<span class="sans">P</span>
<span class="glare">P</span>
<span class="serif">P</span>
<span class="text">P</span>
</div>
</div>
<div class="letter-p2">
<span class="default-letter serif">P</span>
<div class="rolling-letters">
<span class="serif">P</span>
<span class="text">P</span>
<span class="glare">P</span>
<span class="sans">P</span>
</div>
</div>
</div>
<div class="second-line">
<div class="letter-f">
<div class="default-letter sans">F</div>
<div class="rolling-letters">
<span class="sans">F</span>
<span class="text">F</span>
<span class="serif">F</span>
<span class="glare">F</span>
</div>
</div>
.
.
.
생략
한글자 한글자마다 div로 묶어주어야 한다. 그리고 자식 요소로 초기 위치를 잡기 위한 default-letter
와 롤링될 텍스트 rolling-letters
가 필요하다. rolling-letters
는 y축 기준으로 아래로 내려가면서 애니메이션이 진행될 예정이다.
.sc-main .main-title .first-line {
display: flex;
justify-content: center;
overflow: hidden; /* 원래의 자리에서 롤링되어야 하기 때문에 벗어나는 요소는 숨겨준다. */
}
.sc-main .main-title .default-letter {
display: inline-block;
visibility: hidden;
}
.sc-main .main-title .rolling-letters {
position: absolute; /* 롤링될 텍스트들을 절대 위치를 잡아준다. */
left: 0;
bottom: 0;
}
.sc-main .main-title .rolling-letters span {
display: flex;
}
rolling-letters
의 초기 위치가 중요하다 부모를 기준으로 bottom: 0
에 위치하도록 잡아준다. 그리고 gsap 라이브러리의 to()
로 위치를 이동시켜 준다. 아래로 이동해야 하기 때문에 y축 기준 100% 이동한다. y축 기준 아래로 100% 이동하면 현재 default-letter
의 아래 rolling-letters
가 위치해 있기 때문에 다시 위로 bottom: 100%
만큼 올려주어야 한다.
introMotion.to(
".sc-main .rolling-letters",
2, // 애니메이션 진행 시간
{
yPercent: 100,
bottom: "100%",
stagger: {
from: "random",
each: 0.1,
},
},
"a"
)
Swiper는 다양한 옵션과 유연한 커스터마이징이 가능한 라이브러리이다. Swiper와 css를 조합하면 내가 원하는 효과를 자유롭게 구현할 수 있다. 이 프로젝트에서는 direction
옵션을 사용하여 수직 방향으로 슬라이드되는 배경 슬라이드를 구현하였다.
const bgSwiper = new Swiper(".bg-swiper-container", {
direction: "vertical", // 슬라이드 방향을 수직으로 설정
slidesPerView: 1,
spaceBetween: 20,
loop: true,
speed: 500,
navigation: {
nextEl: ".random-btn",
},
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
});
Swiper에서 제공하는 클래스명을 사용하면 css를 통해 스타일을 쉽게 조정할 수 있다. .swiper-slide-prev
클래스를 커스터마이징하여 이전 슬라이드에 특별한 효과를 적용하였다.
.sc-poster .bg-swiper-wrapper .swiper-slide {
width: 100%;
height: 100%;
border-radius: 48px;
transition: 0.3s ease-in-out;
}
.sc-poster .bg-swiper-wrapper .swiper-slide-prev {
opacity: 0.75;
transform: scale(0.8);
}
Swiper의 creativeEffect
옵션을 활용하면 다양한 창의적인 애니메이션을 구현할 수 있다. 이번엔 creativeEffect
를 활용하여 슬라이더에 독특한 효과를 추가했다.
const posterSwiper = new Swiper(".poster-swiper-container", {
direction: "vertical",
slidesPerView: 1,
spaceBetween: 20,
loop: true,
speed: 500,
effect: "creative", // creative effect 적용
creativeEffect: {
prev: {
shadow: true, // 이전 슬라이드에 그림자 효과 적용
translate: [0, 0, -400], // z축으로 -400px 이동
},
next: {
translate: [0, "100%", 0], // 다음 슬라이드를 y축으로 100% 이동
},
},
navigation: {
nextEl: ".random-btn",
},
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
});
이전 슬라이드에는 그림자 효과와 z축 이동으로 공간감을 주고, 다음 슬라이드는 아래로 내려가도록 하여 마치 페이지를 넘기는 듯한 효과를 주었다.
📌 참고
Swiper 라이브러리의 공식 데모페이지에서 다양한 효과를 확인할 수 있으며, 원하는 효과를 선택하여 자유롭게 커스터마이징할 수 있다.