“ 지연되는 프로젝트에 인력을 더 투입하면 오히려 더 늦어진다. ”
- Frederick Philips Brooks
Mythical Man-Month 저자
안녕하세요 ヾ(≧▽≦*)o 오늘은 포트폴리오에 넣기 좋은 효과들로 만든 패럴렉스 효과 06번을 만들어 보도록 하겠습니다!
다양한 css 기능들이 있으니 한 번 적용해 보시면 멋진 포폴이 완성될 것이라 생각합니다 ! 🍕
🧇 완성화면입니다.
HTML
<main id="main">
<div class="parallax__wrap">
<section id="section1" class="parallax__item">
<span class="parallax__item__num">01</span>
<h2 class="parallax__item__title">section01</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style1">결과도 중요하지만, 과정을 더 중요하게 생각한다.</p>
</section>
<!-- section01 -->
<section id="section2" class="parallax__item">
<span class="parallax__item__num">02</span>
<h2 class="parallax__item__title">section02</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style2">우리 모두는 인생에서 만회할 기회라 할 수 있는 큰 변화를 경험한다</p>
</section>
<!-- section02 -->
<section id="section3" class="parallax__item">
<span class="parallax__item__num">03</span>
<h2 class="parallax__item__title">section03</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style3">다음에 벌어질지 모를 장벽을 걱정하여 미래를 향한 걸음을 멈춰선 안된다.</p>
</section>
<!-- section03 -->
<section id="section4" class="parallax__item">
<span class="parallax__item__num">04</span>
<h2 class="parallax__item__title">section04</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style4">나 스스로가 풍요로운 사람이 되려 노력해야 한다</p>
</section>
<!-- section04 -->
<section id="section5" class="parallax__item">
<span class="parallax__item__num">05</span>
<h2 class="parallax__item__title">section05</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style5">자신에 대한 평판은 신경쓰지마라</p>
</section>
<!-- section05 -->
<section id="section6" class="parallax__item">
<span class="parallax__item__num">06</span>
<h2 class="parallax__item__title">section06</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split style6">이상과 꿈을 버리지 마라</p>
</section>
<!-- section06 -->
<section id="section7" class="parallax__item">
<span class="parallax__item__num">07</span>
<h2 class="parallax__item__title">section07</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split">명사형이 아닌 동사형의 삶을 살아라</p>
</section>
<!-- section07 -->
<section id="section8" class="parallax__item">
<span class="parallax__item__num">08</span>
<h2 class="parallax__item__title">section08</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split">도달할 수 있는 최고의 삶과 마주하라</p>
</section>
<!-- section08 -->
<section id="section9" class="parallax__item">
<span class="parallax__item__num">09</span>
<h2 class="parallax__item__title">section09</h2>
<figure class="parallax__item__imgWrap">
<div class="parallax__item__img"></div>
</figure>
<p class="parallax__item__desc split">우리는 삶이 단 한 번 밖에 존재하지 않는단 사실을 망각하고 산다</p>
</section>
<!-- section09 -->
</main>
<!-- main -->
먼저 , main을 만들어줍니다.
이 main에는 내가 원하는 사진과 글귀를 설정해 줍니다.
CSS
<style>
/* .split span{
display: inline-block;
min-width: 1vw;
opacity: 0;
transform: translateY(100px);
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.split.show span{
opacity: 1;
transform: translateY(0);
}
.split span:nth-child(1) {transition-delay: 100ms;}
.split span:nth-child(2) {transition-delay: 150ms;}
.split span:nth-child(3) {transition-delay: 200ms;}
.split span:nth-child(4) {transition-delay: 250ms;}
.split span:nth-child(5) {transition-delay: 300ms;}
.split span:nth-child(6) {transition-delay: 350ms;}
.split span:nth-child(7) {transition-delay: 400ms;}
.split span:nth-child(8) {transition-delay: 450ms;}
.split span:nth-child(9) {transition-delay: 500ms;}
.split span:nth-child(10) {transition-delay: 550ms;}
.split span:nth-child(11) {transition-delay: 600ms;}
.split span:nth-child(12) {transition-delay: 650ms;}
.split span:nth-child(13) {transition-delay: 700ms;}
.split span:nth-child(14) {transition-delay: 750ms;}
.split span:nth-child(15) {transition-delay: 800ms;}
.split span:nth-child(16) {transition-delay: 850ms;}
.split span:nth-child(17) {transition-delay: 900ms;}
.split span:nth-child(18) {transition-delay: 950ms;}
.split span:nth-child(19) {transition-delay: 1000ms;}
.split span:nth-child(20) {transition-delay: 1050ms;}
.split span:nth-child(21) {transition-delay: 1100ms;}
.split span:nth-child(22) {transition-delay: 1150ms;}
.split span:nth-child(23) {transition-delay: 1200ms;}
.split span:nth-child(24) {transition-delay: 1250ms;}
.split span:nth-child(25) {transition-delay: 1300ms;}
.split span:nth-child(26) {transition-delay: 1350ms;}
.split span:nth-child(27) {transition-delay: 1400ms;}
.split span:nth-child(28) {transition-delay: 1450ms;}
.split span:nth-child(29) {transition-delay: 1500ms;}
.split span:nth-child(30) {transition-delay: 1550ms;}
.split span:nth-child(31) {transition-delay: 1600ms;}
.split span:nth-child(32) {transition-delay: 1650ms;}
.split span:nth-child(33) {transition-delay: 1700ms;}
.split span:nth-child(34) {transition-delay: 1750ms;}
.split span:nth-child(35) {transition-delay: 1800ms;}
.split span:nth-child(36) {transition-delay: 1850ms;}
.split span:nth-child(37) {transition-delay: 1900ms;}
.split span:nth-child(38) {transition-delay: 1950ms;}
.split span:nth-child(39) {transition-delay: 2000ms;}
.split span:nth-child(40) {transition-delay: 2050ms;} */
/* style1 */
/* .style1.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
transition: all 0.3s ease-in-out;
}
.style1.split span.show {
opacity: 1;
}
/* style2 */
/* .style2.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
transform: translateY(100px);
transition: all 0.3s ease-in-out;
}
.style2.split span.show {
opacity: 1;
transform: translateY(0);
} */
/*style3 */
/* .style3.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
transform: translateY(100px) translateX(-100px) rotate(360deg);
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
} */
/* .style3.split span.show {
opacity: 1;
transform: translateY(0) translateX(0) rotate(0);
} */
/*style4 */
/* .style4.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
}
.style4.split span.show {
opacity: 1;
animation: zoomInDown 0.5s 1;
} */
/*style5 */
/* .style5.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
}
.style5.split span.show {
opacity: 1;
animation: rubberBand 0.5s 1;
} */
/*style6 */
/* .style6.split span {
opacity: 0;
display: inline-block;
min-width: 1vw;
}
.style6.split span.show {
opacity: 1;
animation: bounceIn 0.5s 1;
}
@keyframes zoomInDown {
0% {
opacity: 0;
-webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
-webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
animation-timing-function: cubic-bezier(.55, .055, .675, .19)
}
60% {
opacity: 1;
-webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
-webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
animation-timing-function: cubic-bezier(.175, .885, .32, 1)
}
}
@keyframes rubberBand {
0% {
-webkit-transform: scaleX(1);
transform: scaleX(1)
}
30% {
-webkit-transform: scale3d(1.25, .75, 1);
transform: scale3d(1.25, .75, 1)
}
40% {
-webkit-transform: scale3d(.75, 1.25, 1);
transform: scale3d(.75, 1.25, 1)
}
50% {
-webkit-transform: scale3d(1.15, .85, 1);
transform: scale3d(1.15, .85, 1)
}
65% {
-webkit-transform: scale3d(.95, 1.05, 1);
transform: scale3d(.95, 1.05, 1)
}
75% {
-webkit-transform: scale3d(1.05, .95, 1);
transform: scale3d(1.05, .95, 1)
}
to {
-webkit-transform: scaleX(1);
transform: scaleX(1)
}
}
@keyframes bounceIn {
0%,
20%,
40%,
60%,
80%,
to {
-webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
animation-timing-function: cubic-bezier(.215, .61, .355, 1)
}
0% {
opacity: 0;
-webkit-transform: scale3d(.3, .3, .3);
transform: scale3d(.3, .3, .3)
}
20% {
-webkit-transform: scale3d(1.1, 1.1, 1.1);
transform: scale3d(1.1, 1.1, 1.1)
}
40% {
-webkit-transform: scale3d(.9, .9, .9);
transform: scale3d(.9, .9, .9)
}
60% {
opacity: 1;
-webkit-transform: scale3d(1.03, 1.03, 1.03);
transform: scale3d(1.03, 1.03, 1.03)
}
80% {
-webkit-transform: scale3d(.97, .97, .97);
transform: scale3d(.97, .97, .97)
}
to {
opacity: 1;
-webkit-transform: scaleX(1);
transform: scaleX(1)
}
} */
/* gsap */
.split span {
display:inline-block;
min-width : 1vw;
opacity: 0;
transform: translate3d(10px , 150px, 30px) scale(2) rotate(200deg);
}
</style>
제가 만들고 싶은 효과는 사진이 일정한 위치에 스크롤 돼 있을 때 사진이 나오면서 글귀에 애니메이션 효과를 줘 글씨가 움직이는 효과를 줄 것입니다.
여기서 @ketframs를 사용한 이유는 , 글씨에 애니메이션 효과를 부여해 움직이는 걸 구현하기 위해 ketframs를 사용하였습니다.
.split span:nth-child(1) {transition-delay: 100ms;}
또한 이 코드는 글씨가 한 글자씩 움직이도록 구현해주기 위해선 글씨마다 nth-child를 지정해줘야기 때문입니다.
이 설정을 해두면 , 글씨가 한 글자씩 각각 움직이도록 설정해 두었습니다.
(하지만 ,, 글씨 양이 많다면 ,, 그건 정말 노가다 그 잡채입니다 !!!!!!!!!!)
편리하게 작업하기 위해 애초에 css에서 style을 여러개 만들어 주도록 합니다.
style마다 움직이는 효과가 다르고 모양이 다르게 작업해 주기 위해 다양한 style을 만들어 줍니다.
JAVASCRIPT
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
<script>
//텍스트 분리하기
// let text = document.querySelector("#section1 .parallax__item__desc");
// let splitText = text.innerText;
// let splitWrap = splitText.split('').join('</span><span>');
// text.innerHTML = splitWrap = "<span>" + splitWrap + "</span>";
//모든 텍스트 분리하기
document.querySelectorAll(".split").forEach(text => {
let splitText = text.innerText;
let splitWrap = splitText.split('').join("</span><span aria-hidden='true'>");
splitWrap = "<span aria-hidden='true'>" + splitWrap + "</span>";
text.innerHTML = splitWrap;
text.setAttribute("aria-label", splitText);
})
//스크롤 이펙트
function scroll(){
let scrollTop = window.pageYOffset || window.scrollY;
//css 클래스 추가(노가다로 함)
// document.querySelectorAll(".parallax__item").forEach(item => {
// if(scrollTop > item.offsetTop){
// item.querySelector(".split").classList.add("show");
// }
// });
//span에 show를 추가
// document.querySelectorAll(".parallax__item").forEach(item => {
// if(scrollTop >= item.offsetTop){
// item.querySelectorAll(".split span").forEach((span, i) => {
// setTimeout(() => {
// span.classList.add("show");
// }, 100*i)
// });
// }
// })
//gsap를 이용해 작업
const items = document.querySelectorAll(".parallax__item");
items.forEach((item, itemIndex) => {
const spans = item.querySelectorAll(".split span");
if (scrollTop >= item.offsetTop){
gsap.to(spans, {
duration:0.1,
opacity:1,
x:0,
y:0,
z:0,
scale:1,
stagger:0.1,
rotation:0
})
}
})
requestAnimationFrame(scroll);
}
scroll();
</script>
마지막으로 , html , css 움직이게 만들어줄 스크립트를 작성해 줍니다.
gsap를 이용해 줄 것이기 때문에 script로 gsap를 걸어줍니다.
그 다음 텍스트 한 글자마다 효과를 주었기 때문에 모든 텍스트를 분리하는 스크립트를 짜주도록 합니다.
//모든 텍스트 분리하기
document.querySelectorAll(".split").forEach(text => {
let splitText = text.innerText;
let splitWrap = splitText.split('').join("</span><span aria-hidden='true'>");
splitWrap = "<span aria-hidden='true'>" + splitWrap + "</span>";
text.innerHTML = splitWrap;
text.setAttribute("aria-label", splitText);
})
🍔 document.querySelectorAll(".split")은 클래스가 "split"인 모든 요소를 선택합니다.
🍔 forEach 메서드를 사용하여 선택된 각 요소에 대해 반복합니다.
🍔 text.innerText는 현재 요소의 텍스트를 가져옵니다.
🍔 splitText.split('')는 텍스트를 문자 단위로 분리한 배열을 생성합니다
🍔 .join("</span><span aria-hidden='true'>")은 배열의 각 요소를 "</span><span aria-hidden='true'>" 문자열로 연결하여 하나의 문자열로 만듭니다.
🍔 이렇게 하면 각 문자 주위에 <span> 태그가 추가되고, aria-hidden 속성이 설정됩니다."<span aria-hidden='true'>" + 🍔 splitWrap + "</span>"는 앞뒤로 <span> 태그를 추가하여 최종적인 HTML 문자열을 만듭니다.
🍔 text.innerHTML = splitWrap;은 처리된 HTML 문자열을 요소의 내부 HTML로 설정합니다.
🍔 text.setAttribute("aria-label", splitText);은 요소에 "aria-label" 속성을 추가하고, 그 값으로 원래의 텍스트를 설정합니다.
스크롤 값을 계산하는 방법 , 스크롤 값을 화면에 띄우는 법
function scroll(){
let scrollTop = window.pageYOffset || window.scrollY;
document.querySelectorAll(".parallax__item").forEach(item => {
if(scrollTop > item.offsetTop){
item.querySelector(".split").classList.add("show");
}
});
document.querySelectorAll(".parallax__item").forEach(item => {
if(scrollTop >= item.offsetTop){
item.querySelectorAll(".split span").forEach((span, i) => {
setTimeout(() => {
span.classList.add("show");
}, 100*i)
});
}
})
🍔scroll 함수는 페이지의 스크롤 위치를 가져옵니다.
🍔window.pageYOffset 또는 window.scrollY를 사용하여 스크롤 위치를 확인합니다.
🍔첫 번째 forEach 루프는 "parallax__item" 클래스를 가진 요소들을 선택하고, 각 요소의 위치를 확인합니다.
🍔만약 현재 스크롤 위치가 요소의 위치보다 아래에 있으면 해당 요소의 자식 요소 중 클래스가 "split"인 요소에 "show" 클래스를 추가합니다.
🍔 이를 통해 특정 스크롤 위치에 도달했을 때 해당 요소가 나타나게 할 수 있습니다.
🍔 두 번째 forEach 루프는 "parallax__item" 클래스를 가진 요소들을 선택하고, 각 요소의 위치를 확인합니다.
🍔 현재 스크롤 위치가 요소의 위치와 같거나 그 이상인 경우에 실행됩니다.
🍔 해당 요소의 자식 요소 중 클래스가 "split"이고 "span"인 요소들을 선택하고, 각각에 대해 일정한 시간 간격으로 순차적으로 "show" 클래스를 추가합니다.
🍔이를 통해 스크롤 위치에 따라 텍스트가 순차적으로 나타나게 할 수 있습니다.
🍔 setTimeout 함수를 사용하여 시간 간격을 설정하고, 100 * i는 각 요소마다 100밀리초씩 증가하는 시간 간격을 의미합니다.
마지막으로 gsap를 이용해 작업해 보도록 하겠습니다.
//gsap를 이용해 작업
const items = document.querySelectorAll(".parallax__item");
items.forEach((item, itemIndex) => {
const spans = item.querySelectorAll(".split span");
if (scrollTop >= item.offsetTop){
gsap.to(spans, {
duration:0.1,
opacity:1,
x:0,
y:0,
z:0,
scale:1,
stagger:0.1,
rotation:0
})
}
})
requestAnimationFrame(scroll);
}
scroll();
🍔 items 변수는 "parallax__item" 클래스를 가진 모든 요소들을 선택합니다.
🍔 forEach 루프를 사용하여 각 요소에 대해 다음 작업을 수행합니다
🍔 spans 변수는 현재 요소 내부에서 클래스가 "split"인 "span" 요소들을 선택합니다.
🍔 현재 스크롤 위치가 해당 요소의 위치보다 크거나 같은 경우에만 아래의 애니메이션을 실행합니다.
🍔 gsap.to를 사용하여 선택된 spans 요소들에 대한 애니메이션을 정의합니다.
🍔 해당 애니메이션은 0.1초 동안 실행되며, 투명도(opacity), X축 이동(x), Y축 이동(y), Z축 이동(z), 크기(scale), 애니메이션 지연(stagger), 회전(rotation) 등의 속성을 지정합니다.
🍔 이를 통해 요소들이 순차적으로 나타나고 애니메이션되는 효과를 줄 수 있습니다.
🍔 requestAnimationFrame(scroll)은 스크롤 이벤트 발생 시 scroll 함수를 호출하여 애니메이션을 계속해서 업데이트합니다.
🍔마지막으로 scroll() 함수를 호출하여 초기에도 애니메이션을 실행합니다.
이렇게 하면 패럴렉스 효과를 완성할 수 있습니다 ~