#
19.gsap
#
목차
1. gsap 2. 설치 2.1. cdn 2.2. npm 2.3. css transform과 gsap 속성 비교 2.4. 기타속성
3. Tween 3.1. 예제
4. easing 4.1. 예제
5. gsap.timeline() 5.1. 예제
6. scrollTrigger 6.1. 주요속성 6.2. 주요메서드 6.3. 예제
7. 가로방향 스크롤 예제
#
1. gsap
웹 브라우저에서 애니메이션을 구현하기 위한 자바스크립트 라이브러리 기존 CSS나 순수 자바스크립트보다 탁월한 퍼포먼스를 발휘할 수 있도록 최적화 된 애니메이션 전용 라이브러리이며, HTML5와 Flash 두 개의 플랫폼에서 사용할 수 있다. GreenSock Engaging the Internet GASP은 애니메이션을 쉽게 적용할 수 있는 라이브러리로 J-Query 보다 20배 이상 빠른 성능을 갖고 있다고 소개하고 있다.
gsap.to() 메소드를 이용해 자연스러운 움직임을 만들 수 있다
다양한 종류의 애니메이션을 제공하며 필요에 따라 플러그인 형태로 추가하여 사용할수 있다. (일부유료)
기본 기능만을 사용할때는 gsap.js 혹은 gsap.min.js 을 사용한다.
문법은 camelCase 를 사용한다. ex) background-color (x), backgroundColor (O)
left 및 top과 같은 위치 속성을 애니메이션화할 때 이동하려는 요소에 CSS position 속성을 적용해야한다.
#
2. 설치
#
2.1. cdn
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
#
2.2. npm
npm install gsap
#
2.3. css transform과 gsap 속성 비교
#
2.4. 기타속성
#
3. Tween
#
3.1. 예제
애니메이션 작업을 수행한다. 타겟(애니메이션을 적용하려는 개체), 지속 시간 및 애니메이션을 적용하려는 속성을 입력하면 간단히 애니메이션을 완성해준다. 트윈을 생성하는 매서드는 3종류가 있다.
- gsap.to()
- gsap.from()
- gsap.fromTo()
css로 transform: translateX(200px) 과 같은 효과를 주며 duration은 미작성시 0.5초 이다
<head>
<style>
.box {
margin: 15px;
}
.box1 {
width: 100px;
height: 100px;
background: orange;
}
.box2 {
width: 200px;
height: 100px;
background: greenyellow;
}
.box3 {
width: 100px;
height: 200px;
background: tomato;
}
</style>
</head>
<body>
<div class="box box1"></div>
<div class="box box2"></div>
<div class="box box3"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/gsap.min.js"></script>
<script>
gsap.to('.box', {
x: 200,
});
</script>
</body>
기본구조는 01과 같다.스크립트만 수정한다.
gsap.to('.box1', { duration: 3, x: 200, opacity: 0.2, ease: 'steps(10)', delay: 2 });
gsap.to('.box2', { duration: 3, x: 200, rotate: 720, scale: 1.3 });
gsap.to('.box3', { duration: 3, x: 200, ease: 'elastic', backgroundColor: 'red', width: 300, fontSize: 60 });
from 으로 변경하면 에니메이션이 반전되어 실행된다.
gsap.from('.box1', { duration: 3, x: 200, opacity: 0.2, ease: 'steps(10)', delay: 2 });
gsap.from('.box2', { duration: 3, x: 200, rotate: 720, scale: 1.3 });
gsap.from('.box3', { duration: 3, x: 200, ease: 'elastic', backgroundColor: 'red', width: 300, fontSize: 60 });
"h2" 요소에 1초 동안 투명도를 30%로 애니메이션 적용하고 ".orange" 요소는 2초 동안 x축으로 300pixel 이동하고 ".grey"는 Y축으로 300px 이동. “.green” 요 소는 3초 동안 360도 회전하고 크기를 50%로 축소하는 애니메이션 을 적용한다
<h2 class="title">gsap.to() Basic Usage</h2>
<div class="box orange"></div>
<div class="box grey"></div>
<div class="box green"></div>
body {
margin: 10px;
}
.box {
width: 100px;
height: 100px;
}
.orange {
background: orange;
}
.grey {
background: grey;
}
.green {
background: green;
}
gsap.to('h2.title', { duration: 1, opacity: 0.3 });
gsap.to('.orange', { duration: 2, x: 300 });
gsap.to('.grey', { duration: 2, y: 300 });
gsap.to('.green', { duration: 3, rotation: 360, scale: 0.5 });
#
4. easing
#
4.1. 예제
<section id="title">
<div class="box box1">none</div>
<div class="box box2">power1</div>
<div class="box box3">power2</div>
<div class="box box4">power3</div>
<div class="box box5">power4</div>
<div class="box box6">back</div>
<div class="box box7">elastic</div>
<div class="box box8">bounce</div>
<div class="box box9">rough</div>
<div class="box box10">slow</div>
<div class="box box11">steps</div>
<div class="box box12">circ</div>
<div class="box box13">expo</div>
<div class="box box14">sine</div>
</section>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
##title {
width: 700px;
margin: 100px auto;
}
.box {
background-color: purple;
width: 50px;
height: 50px;
border-radius: 50%;
text-align: center;
line-height: 50px;
font-size: 12px;
}
gsap.to('.box1', { x: 600, ease: 'none', duration: 5 });
gsap.to('.box2', { x: 600, ease: 'power1', duration: 5 });
gsap.to('.box3', { x: 600, ease: 'power2', duration: 5 });
gsap.to('.box4', { x: 600, ease: 'power3', duration: 5 });
gsap.to('.box5', { x: 600, ease: 'power4', duration: 5 });
gsap.to('.box6', { x: 600, ease: 'back', duration: 5 });
gsap.to('.box7', { x: 600, ease: 'elastic', duration: 5 });
gsap.to('.box8', { x: 600, ease: 'bounce', duration: 5 });
gsap.to('.box9', { x: 600, ease: 'rough', duration: 5 });
gsap.to('.box10', { x: 600, ease: 'slow', duration: 5 });
gsap.to('.box11', { x: 600, ease: 'steps(5)', duration: 5 });
gsap.to('.box12', { x: 600, ease: 'circ', duration: 5 });
gsap.to('.box13', { x: 600, ease: 'expo', duration: 5 });
gsap.to('.box14', { x: 600, ease: 'sine', duration: 5 });
#
5. gsap.timeline()
타임라인 메서드는 애니메이션의 시간을 지정할수 있어 다양한 모션 구현시 편리하다.
#
5.1. 예제
<div class="one"></div>
<div class="two"></div>
<div class="three"></div>
<div class="four"></div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
}
div {
width: 100px;
height: 100px;
}
.one {
background: red;
}
.two {
background: green;
}
.three {
background: blue;
}
.four {
background: pink;
}
let tl = gsap.timeline();
//total duration:
tl.to('.one', { duration: 2, x: 500 })
//2초 동안 x축으로 500px
.to('.two', { duration: 3, x: 500 }, 1)
// 1초 후 3초동안 x축으로 500px(1은 absolute값)
.to('.three', { duration: 1, x: 500 }, '<')
//1초 후 1초동안 x축으로 500px(<은 이전 target의 timeline을 따라감)
.to('.four', { duration: 1, x: 500 }, '<0.5');
//1.5초후 1초동은 x축으로 500px(<0.5를 추가하면 이전 target보다 0.5초 뒤 실행
timeline 을 활용하여 다른 클래스의 요소들에 시차애니메이션을 적용한다.
변수 t1을 선언하고 기본값으로 1초를 delay로 할당한다.
<section id="title">
<div class="container">
<h1 class="animation1">JavaScript GSAP Library Animation</h1>
<p class="animation1">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempore, dolore.</p>
<a class="animation1" href="#">Button</a>
</div>
</section>
<section>
<div class="box box1"></div>
<div class="box box2"></div>
</section>
<section id="thumbnail">
<img src="http://placekitten.com/200/300" />
</section>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
}
#title {
width: 700px;
margin: 100px auto;
}
.container {
padding: 20px;
}
h1 {
margin-bottom: 20px;
}
p {
margin-bottom: 15px;
}
a {
display: block;
width: 100px;
padding: 10px;
background-color: blueviolet;
text-decoration: none;
color: white;
text-align: center;
border-radius: 10px;
}
#thumbnail {
width: 420px;
height: 300px;
position: absolute;
right: 0;
bottom: 0;
}
.box {
width: 100px;
height: 100px;
}
.box1 {
background-color: red;
}
.box2 {
background-color: blue;
}
let t1 = gsap.timeline({ defaults: { duration: 1 } });
//defaults라는 특수 속성은 모든 하위 트윈과 타임라인에서 값을 상속할 수 있다.
//애니메이션을 1초동안 진행하는 기본값 설정
t1.from('h1', { y: -50, opacity: 0 })
.from('p', { y: -50, opacity: 0 }, '-=0.5') // 타임라인 종료 0.5초 전 (오버랩)
.from('a', { y: -50, opacity: 0 }, '+=1') // 타임라인 종료 1초 후 (갭)
.from('img', { y: 200, opacity: 0 }, '3'); // 타임라인 시작으로부터 3초 후 (절대적)
.from('.box1', { x: 200, opacity: 0 }, "<") // 이전 트윈 타임라인 시작지점
.to('.box2', { rotate: 360, x:800, opacity: 1}, ">") // 이전 트윈 타임라인 종료지점
문서의 구조와 스타일은 이전과 동일
let t1 = gsap.timeline({ defaults: { duration: 1 } });
t1.from('h1', { y: -50, opacity: 0 })
.from('p', { y: -50, opacity: 0 }, '-=0.5') // 타임라인 종료 0.5초 전 (오버랩)
.from('a', { y: -50, opacity: 0 }, '-=0.5') // 타임라인 종료 0.5초 전 (오버랩)
.from('img', { y: 200, opacity: 0 }, '-=0.5')
.from('.box1', { x: 200, opacity: 0 }, '<') // 이전 트윈 타임라인 시작지점
.to('.box2', { rotate: 360, x: 800, opacity: 1 }, '>'); // 이전 트윈 타임라인 종료지점
document.getElementById('cta').addEventListener('click', (e) => {
e.preventDefault();
t1.reversed() ? t1.play() : t1.reverse();
//타임라인의 진행상태가 반전 되었을경우 시작하고 아닐경우 반전
});
애니메이션의 핸들링이 궁금하다면?
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.box {
width: 100px;
height: 100px;
}
.box1 {
background-color: red;
}
</style>
</head>
<body>
<div class="box box1"></div>
<div class="nav">
<button id="play">시작</button>
<button id="pause">정지</button>
<button id="resume">재개</button>
<button id="reverse">반전</button>
<button id="restart">재시작</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
<script>
const tween = gsap.to(".box1", { duration: 8, x: 400, width: 400, paused: true, });
document.querySelector("#play").onclick = function () { return tween.play(); };
document.querySelector("#pause").onclick = function () { return tween.pause(); };
document.querySelector("#resume").onclick = function () { return tween.resume(); };
document.querySelector("#reverse").onclick = function () { return tween.reverse(); };
document.querySelector("#restart").onclick = function () { return tween.restart(); }.
//resume 애니메이션 멈춘곳 부터 재생을 다시 시작
</script>
</body>
</html>
#
6. scrollTrigger
스크롤트리거는 gsap 의 플러그인 으로 추가설치 해야한다.
#
6.1. 주요속성
#
6.2. 주요메서드
#
6.3. 예제
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
<section></section>
<section>
<div class="box"></div>
</section>
<section></section>
.box {
width: 200px;
height: 200px;
background: pink;
}
section {
height: 100vh;
}
- 섹션별 배경색 추가
const bg = document.querySelectorAll('section');
const colors = {
r: 255,
g: 255,
b: 255,
};
bg.forEach((o, i) => {
o.style.backgroundColor = `rgb(${colors.r - i * 100},${colors.g - i * 50},${colors.b - i * 50})`;
});
- 스크롤트리거 작성
gsap.to('.box', {
scrollTrigger: '.box',
x: '50vw',
});
- html
<main class="cont">
<section class="sec1 item">
<h2 class="num">01</h2>
<div class="box"></div>
</section>
<!-- //.sec1 -->
<section class="sec2 item">
<h2 class="num">02</h2>
<div class="box"></div>
</section>
<!-- //.sec2 -->
<section class="sec3 item">
<h2 class="num">03</h2>
<div class="box"></div>
</section>
<!-- //.sec3 -->
<section class="sec4 item">
<h2 class="num">04</h2>
<div class="box"></div>
</section>
<!-- //.sec4 -->
<section class="sec5 item">
<h2 class="num">05</h2>
<div class="box"></div>
</section>
<!-- //.sec5 -->
<section class="sec6 item">
<h2 class="num">06</h2>
<div class="box"></div>
</section>
<!-- //.sec6 -->
<section class="sec7 item">
<h2 class="num">07</h2>
<div class="box box7"></div>
</section>
<!-- //.sec7 -->
<section class="sec8 item">
<h2 class="num">08</h2>
<div class="box"></div>
</section>
<!-- //.sec8 -->
</main>
- css
@import url(https://qwerewqwerew.github.io/source/style/reset.css);
.cont {
overflow: hidden;
}
.item {
width: clamp(50%, 100%, 100%);
height: 100vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 5vw;
}
.item:nth-child(2n) {
background-color: gray;
}
.num {
position: absolute;
right: 20px;
bottom: 20px;
font-size: 5vw;
line-height: 1;
}
.box {
width: 10vw;
height: 10vw;
background-color: #ff1515;
background-size: cover;
background-position: center;
}
.active.box {
filter: hue-rotate(100deg);
}
- js
const box1 = document.querySelector('.sec1 .box');
const box2 = document.querySelector('.sec2 .box');
const box3 = document.querySelector('.sec3 .box');
const box4 = document.querySelector('.sec4 .box');
const box5 = document.querySelector('.sec5 .box');
const box6 = document.querySelector('.sec6 .box');
const box7 = document.querySelector('.sec7 .box');
const box8 = document.querySelector('.sec8 .box');
const box9 = document.querySelector('.sec9 .box');
- jq
const box1 = $('.sec1 .box');
const box2 = $('.sec2 .box');
const box3 = $('.sec3 .box');
const box4 = $('.sec4 .box');
const box5 = $('.sec5 .box');
const box6 = $('.sec6 .box');
const box7 = $('.sec7 .box');
const box8 = $('.sec8 .box');
const box9 = $('.sec9 .box');
- gsap
// 01
gsap.to(box1, {
duration: 2,
x: 500,
borderRadius: 100,
rotation: 360,
});
// 02 : trigger
gsap.to(box2, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
backgroundColor: 'yellow',
scrollTrigger: {
trigger: box2,
},
});
// 03 : toggleActions
gsap.to(box3, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box3,
toggleActions: 'play none reverse none',
},
});
// 04 : start, end
gsap.to(box4, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box4,
start: 'top 50%',
end: 'bottom 20%',
toggleActions: 'play none reverse none',
markers: true,
},
});
// 05 : scrub
gsap.to(box5, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box5,
start: 'top 50%',
end: 'bottom 20%',
scrub: true, //true, 1, 2...
markers: false,
},
});
// 06 : pin
gsap.to(box6, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box6,
start: 'top 50%',
end: 'bottom 200px',
scrub: true,
pin: true,
markers: false,
},
});
// 07 : toggleClass
gsap.to(box7, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box7,
start: 'top center',
end: 'bottom 20%',
scrub: true,
toggleClass: 'active',
markers: false,
id: 'box7',
},
});
// 08 : callback
gsap.to(box8, {
duration: 2,
x: 500,
rotation: 360,
borderRadius: 100,
scrollTrigger: {
trigger: box8,
start: 'top center',
end: 'bottom 20%',
scrub: true,
markers: true,
// onEnter: () => {console.log("onEnter")},
// onLeave: () => {console.log("onLeave")},
// onEnterBack: () => {console.log("onEnterBack")},
// onLeaveBack: () => {console.log("onLeaveBack")},
onUpdate: (self) => {
console.log('onUpdate', self.progress.toFixed(3));
},
onToggle: (self) => {
console.log('onToggle', self.isActive);
myAni();
},
},
});
function myAni() {
const txt = '<span>1</span>';
box8.parent().append(txt);
}
- html 08번 예제와 유사하나 일부 요소가 추가된다.
<section class="sec2 item">
<h2 class="num">02</h2>
<div class="box i1"></div>
<div class="box i2"></div>
<div class="box i3"></div>
</section>
<!-- //.sec2 -->
<section class="sec3 item">
<h2 class="num">03</h2>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</section>
<!-- //.sec3 -->
<section class="sec4 item">
<h2 class="num">04</h2>
<div class="box"></div>
</section>
<!-- //.sec4 -->
<section class="sec5 item">
<h2 class="num">05</h2>
<div class="text t1">Lorem</div>
<div class="text t2">Lorem</div>
<div class="text t3">Lorem</div>
<div class="text t4">Lorem</div>
</section>
<!-- //.sec5 -->
<section class="sec6 item">
<h2 class="num">06</h2>
<div class="text">Lorem</div>
</section>
<!-- //.sec6 -->
<section class="sec7 item">
<h2 class="num">07</h2>
<div class="text t1">Lorem</div>
<div class="text t2">Ipsum</div>
<div class="text t3">Gsap</div>
<div class="text t4">HTML</div>
<div class="text t5">CSS</div>
<div class="text t6">Javascript</div>
<div class="text t7">Jquery</div>
</section>
<!-- //.sec7 -->
<section class="sec8 item">
<h2 class="num">08</h2>
<div class="text t1">Lorem</div>
<div class="text t2">Ipsum</div>
<div class="text t3">Gsap</div>
<div class="box"></div>
</section>
<!-- //.sec8 -->
<section class="sec9 item">
<h2 class="num">09</h2>
<div class="box"></div>
</section>
<!-- //.sec9 -->
- css 추가
.sec4 .box,
.sec9 .box {
background: url(http://qwerew.cafe24.com/images/1.jpg) no-repeat center center/100%;
}
.text {
font-size: 10rem;
}
.sec5.item {
flex-direction: column;
}
.sec7 .text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
width: 100%;
padding: 2rem;
text-align: center;
}
- js
//01 : 이미지 애니메이션 주기
const ani1 = gsap.timeline();
ani1.to('.sec1 .box', { rotation: 450, scale: 0, borderRadius: 200 }).to('.sec1 .box', { rotation: 360 * 5, scale: 1, borderRadius: 20 });
ScrollTrigger.create({
animation: ani1,
trigger: '.sec1',
start: 'top top',
end: '+=2000', //시작 부분부터 2000px까지 스크롤 한 후종료
scrub: true,
pin: true, //화면고정 //true | 요소이름
anticipatePin: 1,
markers: true,
id: 'sec1',
});
//02 : 순차 나타나기
const ani2 = gsap.timeline();
ani2.from('.sec2 .i1', { y: 200, autoAlpha: 0 }).from('.sec2 .i2', { y: 100, autoAlpha: 0 }).from('.sec2 .i3', { y: -100, autoAlpha: 0 });
ScrollTrigger.create({
animation: ani2,
trigger: '.sec2',
start: 'top top',
end: '+=2000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//03 : 랜덤
const ani3 = gsap.timeline();
ani3.from('.sec3 .box', {
y: -300,
scale: 0.5,
autoAlpha: 0,
ease: 'back.out(4)',
//stagger: 0.3, // 0.3초 간격으로 실행
stagger: {
amount: 3, //총 3초 동안 무작위
from: 'random',
},
});
ScrollTrigger.create({
animation: ani3,
trigger: '.sec3',
start: 'top top',
end: '+=2000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//04 : 이미지 축소하기
const ani4 = gsap.timeline();
ani4.from('.sec4 .box', {
autoAlpha: 0,
scale: 5,
width: '100vw',
height: '100vh',
});
ScrollTrigger.create({
animation: ani4,
trigger: '.sec4',
start: 'top top',
end: '+=3000',
scrub: true,
pin: true,
anticipatePin: 1, //1초전 //핀이 시작되기 직전에 애니메이션을 미리 시작해서 예상치 못한 끊김 방지
markers: true,
});
//05 : 텍스트 애니메이션
const ani5 = gsap.timeline();
//"-=1" 1초전 시작
ani5.to('.sec5 .t1', { xPercent: 300 }).to('.sec5 .t2', { xPercent: -300 }, '-=1').to('.sec5 .t3', { xPercent: 300 }).to('.sec5 .t4', { xPercent: -300 });
//'text' 그룹핑기능 함께 동시실행
//ani5.to('.sec5 .t1', { xPercent: 300 }).to('.sec5 .t2', { xPercent: -300 }, 'text').to('.sec5 .t3', { xPercent: 300 }, 'text').to('.sec5 .t4', { xPercent: -300 }, 'text');
ScrollTrigger.create({
animation: ani5,
trigger: '.sec5',
start: 'top top',
end: '+=3000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//06 : 텍스트 확대하기
const ani6 = gsap.timeline();
ani6.to('.sec6 .text', { scale: 60, duration: 2 }).to('.sec6 .text', { autoAlpha: 0 });
ScrollTrigger.create({
animation: ani6,
trigger: '.sec6',
start: 'top top',
end: '+=4000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//07 : 텍스트 제자리 애니메이션
const ani7 = gsap.timeline();
ani7.from('.sec7 .t1', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t2', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t3', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t4', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t5', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t6', { autoAlpha: 0, duration: 1, y: 50 }, '+=1').from('.sec7 .t7', { autoAlpha: 0, duration: 1, y: 50 }, '+=1');
ScrollTrigger.create({
animation: ani7,
trigger: '.sec7',
start: 'top top',
end: '+=6000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//08 : 텍스트 애니메이션
const ani8 = gsap.timeline();
ani8
.from('.sec8 .t1', { x: innerWidth * 1 })
.from('.sec8 .t2', { x: innerWidth * -1 })
.from('.sec8 .t3', { x: innerWidth * 1 })
.from('.sec8 .box', { x: innerWidth * 1, rotation: 360, scale: 5.5 });
ScrollTrigger.create({
animation: ani8,
trigger: '.sec8',
start: 'top top',
end: '+=4000',
scrub: true,
pin: true,
anticipatePin: 1,
markers: true,
});
//09 : 이미지 확대하기
const ani9 = gsap.timeline();
ani9.to('.sec9 .box', { scale: 13 }).to('.sec9 .box', { autoAlpha: 0 });
ScrollTrigger.create({
animation: ani9,
trigger: '.sec9',
start: 'top top',
end: '+=4000',
scrub: true,
pin: true,
markers: false,
anticipatePin: 1,
});
anticipatePin
: 핀이 시작되기 전 애니메이션을 미리 시작해서 예상치 못한 끊김 방지 (1은 1초전 시작)
- html 작성
<main class="cont">
<!-- section.item.sec$*5>h2.num+.box -->
<section class="item sec1">
<h2 class="num">1</h2>
<div class="box"></div>
</section>
<section class="item sec2">
<h2 class="num">2</h2>
<div class="box"></div>
</section>
<section class="item sec3">
<h2 class="num">3</h2>
<div class="box"></div>
</section>
<section class="item sec4">
<h2 class="num">4</h2>
<div class="box"></div>
</section>
<section class="item sec5">
<h2 class="num">5</h2>
<div class="box"></div>
</section>
</main>
- css 추가
.box {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background-color: #fff;
background-size: cover;
background-position: center;
}
.box::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.num {
position: absolute;
font-size: 10vw;
z-index: 10;
color: #fff;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.sec1 .box {
background-image: url(http://qwerew.cafe24.com/images/1.jpg);
}
.sec2 .box {
background-image: url(http://qwerew.cafe24.com/images/2.jpg);
}
.sec3 .box {
background-image: url(http://qwerew.cafe24.com/images/3.jpg);
}
.sec4 .box {
background-image: url(http://qwerew.cafe24.com/images/4.jpg);
}
.sec5 .box {
background-image: url(http://qwerew.cafe24.com/images/5.jpg);
}
- js
// 01. 한개 섹션 고정시키기
function fn1() {
const panel = document.querySelector('.sec5');
ScrollTrigger.create({
trigger: panel,
start: 'top top',
pin: true,
pinSpacing: false,
});
}
// 02. 여러개 섹션 고정시키기
function fn2() {
gsap.utils.toArray('.box').forEach((panel, i) => {
ScrollTrigger.create({
trigger: panel,
start: 'top top',
pin: true,
pinSpacing: false,
});
});
}
// 03. 스냅 고정
function fn3() {
let panels = gsap.utils.toArray('.box');
let tops = panels.map((panel) => ScrollTrigger.create({ trigger: panel, start: 'top top' }));
console.log(tops);
panels.forEach((panel, i) => {
ScrollTrigger.create({
trigger: panel,
start: () => (panel.offsetHeight < window.innerHeight ? 'top top' : 'bottom bottom'),
pin: true,
pinSpacing: false,
});
});
ScrollTrigger.create({
snap: {
snapTo: (progress, self) => {
let panelStarts = tops.map((st) => st.start),
snapScroll = gsap.utils.snap(panelStarts, self.scroll());
return gsap.utils.normalize(0, ScrollTrigger.maxScroll(window), snapScroll);
},
duration: 0.5,
},
});
}
fn3();
- html
<main class="cont">
<!-- section.item.sec$*9>h2.num+.box -->
<section class="item sec1">
<h2 class="num">1</h2>
<div class="img_wrap">
<div class="img"></div>
</div>
<div class="desc">Lorem ipsum dolor sit, amet</div>
</section>
<section class="item sec2">
<h2 class="num">2</h2>
<div class="img_wrap">
<div class="img"></div>
</div>
<div class="desc">Lorem ipsum dolor sit, amet</div>
</section>
<section class="item sec3">
<h2 class="num reveal">3</h2>
<div class="img_wrap">
<div class="img "></div>
</div>
<div class="desc reveal ltr" data-delay="0.8">Lorem ipsum dolor sit, amet</div>
<div class="desc reveal ttb" data-delay="0.3">Lorem ipsum dolor sit, amet</div>
</section>
<section class="item sec4">
<h2 class="num">4</h2>
<div class="img_wrap reveal">
<div class="img"></div>
</div>
<div class="desc reveal btt" data-delay="0.5">Lorem ipsum dolor sit, amet</div>
</section>
</main>
- css
section.item {
padding-top: 5vw;
margin: 10vw 0;
margin-right: 0;
text-align: left;
}
.img_wrap {
position: relative;
width: 100%;
overflow: hidden;
/* 1080/1920*100 */
padding-bottom: 50%;
}
.img {
position: absolute;
filter: grayscale(100%);
transition: all 1s;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
}
.img_wrap:hover .img {
filter: grayscale(0%);
transform: scale(1.3);
}
.num {
position: absolute;
font-size: 10vw;
z-index: 10;
color: #fff;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.desc {
font-size: 5vw;
font-weight: 900;
margin-left: -50%;
margin-top: 50%;
z-index: 100;
position: relative;
-webkit-text-fill-color: #0000802d;
-webkit-text-stroke: 1px #000080;
text-stroke: 1px 000080;
}
.desc:before {
content: 'Lorem ipsum dolor sit, amet';
position: absolute;
filter: drop-shadow(-15px -10px 10px rgba(28, 33, 106, 0.5)) drop-shadow(10px 10px 10px rgba(13, 16, 65, 0.8)) drop-shadow(20px 20px 40px rgba(9, 11, 41, 0.8));
}
.ttb {
margin-top: 0%;
}
.sec1 .img {
background-image: url(http://qwerew.cafe24.com/images/1.jpg);
}
.sec2 .img {
background-image: url(http://qwerew.cafe24.com/images/2.jpg);
}
.sec3 .img {
background-image: url(http://qwerew.cafe24.com/images/3.jpg);
}
.sec4 .img {
background-image: url(http://qwerew.cafe24.com/images/4.jpg);
}
- js
//01. 패럴렉스1개
const fn1 = () => {
gsap.to('.desc', {
yPercent: -100,
ease: 'none',
duration: 0.5,
scrollTrigger: {
trigger: '.desc',
start: 'top bottom',
end: 'bottom top',
markers: true,
scrub: true,
},
});
};
//02. 패럴렉스여러개
const fn = () => {
gsap.utils.toArray('.desc').forEach((item) => {
gsap.to(item, {
yPercent: -200,
ease: 'none',
duration: 0.5,
scrollTrigger: {
trigger: item,
start: 'top bottom',
end: 'bottom top',
markers: false,
scrub: 0.5,
},
});
});
};
fn();
//03. 나타나기
const hide = (item) => {
gsap.set(item, { autoAlpha: 0 });
};
const animate = (item) => {
let x = 0;
let y = 0;
let delay = item.dataset.delay;
if (item.classList.contains('ltr')) {
(x = -100), (y = 0);
} else if (item.classList.contains('btt')) {
(x = 0), (y = 100);
} else if (item.classList.contains('ttb')) {
(x = 0), (y = -100);
} else {
(x = 100), (y = 0);
}
gsap.fromTo(item, { autoAlpha: 0, x: x, y: y }, { autoAlpha: 1, x: 0, y: 0, delay: delay, duration: 1.25, overwrite: 'auto', ease: 'expo' });
};
gsap.utils.toArray('.reveal').forEach((item) => {
hide(item);
ScrollTrigger.create({
trigger: item,
start: 'top bottom',
end: 'bottom top',
markers: false,
onEnter: () => {
animate(item);
},
});
});
- html
<!-- nav>ul>(li>a)*9 -->
<nav>
<h1>스크롤효과</h1>
<ul>
<li><a href="#sec1">sec1</a></li>
<li><a href="#sec2">sec2</a></li>
<li><a href="#sec3">sec3</a></li>
<li><a href="#sec4">sec4</a></li>
<li><a href="#sec5">sec5</a></li>
<li><a href="#sec6">sec6</a></li>
<li><a href="#sec7">sec7</a></li>
<li><a href="#sec8">sec8</a></li>
<li><a href="#sec9">sec9</a></li>
</ul>
</nav>
<main class="cont">
<!-- section.item#sec$*9>h2.num+.box -->
<section class="item" id="sec1">
<h2 class="num">1</h2>
<div class="box"></div>
</section>
<section class="item" id="sec2">
<h2 class="num">2</h2>
<div class="box"></div>
</section>
<section class="item" id="sec3">
<h2 class="num">3</h2>
<div class="box"></div>
</section>
<section class="item" id="sec4">
<h2 class="num">4</h2>
<div class="box"></div>
</section>
<section class="item" id="sec5">
<h2 class="num">5</h2>
<div class="box"></div>
</section>
<section class="item" id="sec6">
<h2 class="num">6</h2>
<div class="box"></div>
</section>
<section class="item" id="sec7">
<h2 class="num">7</h2>
<div class="box"></div>
</section>
<section class="item" id="sec8">
<h2 class="num">8</h2>
<div class="box"></div>
</section>
<section class="item" id="sec9">
<h2 class="num">9</h2>
<div class="box"></div>
</section>
</main>
- css
nav {
position: absolute;
display: flex;
gap: 2rem;
max-width: 80%;
margin: auto;
z-index: 9999;
top: 0;
transition: all 1s;
left: 0;
background-color: gray;
}
nav.active {
position: fixed;
top: 0;
background-color: rgba(128, 128, 128, 0);
}
nav.active ul {
padding: 0rem 8rem;
border-radius: 50px;
background-color: rgba(19, 116, 226, 0.5);
}
nav ul {
display: flex;
gap: 2rem;
padding: 0rem 2rem;
border-radius: 0px;
transition: all 1s;
}
nav li {
width: calc(100% / 9);
height: 10rem;
line-height: 10rem;
}
nav li a {
font-size: 1.8rem;
color: white;
}
nav li a.active {
color: red;
}
- js
let links = gsap.utils.toArray('nav ul li a');
links.forEach((link) => {
let element = document.querySelector(link.getAttribute('href'));
let linkST = ScrollTrigger.create({
trigger: element,
start: 'top top',
});
ScrollTrigger.create({
trigger: element,
start: 'top center',
end: 'bottom center',
onToggle: (self) => setActive(link),
});
link.addEventListener('click', (e) => {
e.preventDefault();
gsap.to(window, { duration: 1, scrollTo: linkST.start, overwrite: 'auto' });
});
});
function setActive(link) {
links.forEach((el) => el.classList.remove('active'));
link.classList.add('active');
}
ScrollTrigger.create({
start: 'top -80',
end: 99999,
toggleClass: {
className: 'active',
targets: 'nav',
},
});
#
7. 가로방향 스크롤
gsap을 이용하여 횡스크롤을 제작해보자
cdn 추가
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
#
예제
- html
<main class="cont">
<h1>가로스크롤</h1>
<section class="item" id="sec1">
<h2 class="num">1</h2>
</section>
<section class="item" id="sec2">
<h2 class="num">2</h2>
</section>
<section class="item" id="sec3">
<h2 class="num">3</h2>
<div class="box"></div>
</section>
<section class="item" id="sec4">
<h2 class="num">4</h2>
<div class="box"></div>
</section>
<section class="item" id="sec5">
<h2 class="num">5</h2>
<div class="box"></div>
</section>
<section class="item" id="sec6">
<h2 class="num">6</h2>
<div class="box"></div>
</section>
<section class="item" id="sec7">
<h2 class="num">7</h2>
<div class="box"></div>
</section>
<section class="item" id="sec8">
<h2 class="num">8</h2>
<div class="box"></div>
</section>
<section class="item" id="sec9">
<h2 class="num">9</h2>
<div class="box"></div>
</section>
</main>
- css
main {
overscroll-behavior: none;
width: 900%;
height: 100vh;
display: flex;
flex-wrap: nowrap;
}
- js
let sections = gsap.utils.toArray('section');
const move = gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: 'none',
scrollTrigger: {
trigger: 'main',
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1),
end: '+=7000',
//end: document.querySelector('main').offsetWidth,
},
});
if (ScrollTrigger.isTouch > 0) {
move;
}
- html
<main class="cont">
<h1>가로스크롤</h1>
<section class="item" id="sec1">
<h2 class="num">1</h2>
<div class="box"></div>
</section>
<section class="item" id="sec2">
<h2 class="num">2</h2>
<div class="box"></div>
</section>
<div class="horizontal">
<section class="item" id="sec3">
<h2 class="num">3</h2>
<div class="box"></div>
</section>
<section class="item" id="sec4">
<h2 class="num">4</h2>
<div class="box"></div>
</section>
<section class="item" id="sec5">
<h2 class="num">5</h2>
<div class="box"></div>
</section>
</div>
<section class="item" id="sec6">
<h2 class="num">6</h2>
<div class="box"></div>
</section>
<section class="item" id="sec7">
<h2 class="num">7</h2>
<div class="box"></div>
</section>
</main>
- css
main {
overflow: hidden;
}
.horizontal {
display: flex;
flex-wrap: nowrap;
width: 500%;
}
.horizontal > section {
width: 100%;
}
- js
const horizontal = document.querySelector('.horizontal');
const sections = gsap.utils.toArray('.horizontal > section');
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: 'none',
scrollTrigger: {
trigger: horizontal,
start: 'top top',
end: () => '+=' + (horizontal.offsetWidth - innerWidth),
pin: true,
scrub: 1,
snap: {
snapTo: 1 / (sections.length - 1),
inertia: false,
duration: { min: 0.1, max: 0.1 },
},
invalidateOnRefresh: true,
anticipatePin: 1,
},
});
10: '+='는 시작 지점으로부터의 상대적 위치. horizontal.offsetWidth는 트리거 요소의 전체 너비이며, innerWidth는 브라우저 창의 내부 너비이다. (horizontal.offsetWidth - innerWidth)는 트리거 요소의 너비에서 뷰포트의 너비를 뺀 값
14: snap snapTo: 스크롤 중단점. 1 / (sections.length - 1) => 1/2 을 의미하며 각 섹션 사이의 상대적인 스냅 지점을 계산한다
15: inertia: 스크롤 중단시 미끄러짐 적용여부. false는 스크롤을 멈추면 즉시 스냅 포인트로 이동
16: duration: 스냅 애니메이션의 지속 시간. min과 max는 애니메이션의 최소 및 최대 지속 시간을 초 단위로 설정. 0.1로 설정되어 있어, 스냅 애니메이션의 지속 시간이 항상 0.1초로 설정함
17: invalidateOnRefresh: 이 속성은 ScrollTrigger가 화면 크기 변경, 디바이스 회전 등으로 인해 뷰포트가 리프레시될 때 스냅 포인트를 재계산할지 여부를 결정. true로 설정 스냅 포인트가 자동으로 업데이트되어 항상 정확한 위치에 스냅될 수 있도록 한다
- html
<main>
<div class="first">
<h1>Testing horizontal scrolling w/ three sections</h1>
<h2>First Container</h2>
</div>
<div class="horizontal">
<div class="description section">
<div>
SCROLL DOWN
<div class="scroll-down">
<div class="arrow"></div>
</div>
</div>
</div>
<section class="section">
ONE
<div class="box-1 box">box-1</div>
</section>
<section class="section">
TWO
<div class="box-2 box">box-2</div>
</section>
<section class="section">
THREE
<div class="box-3 box">box-3</div>
</section>
</div>
<div class="last">Last Container</div>
</main>
- css
html {
overflow-y: scroll;
overflow-x: hidden;
height: 100%;
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
.section {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: 10vw;
text-align: center;
color: rgb(66, 8, 8);
position: relative;
box-sizing: border-box;
padding: 10px;
}
.horizontal {
width: 400%;
height: 100vh;
display: flex;
flex-wrap: nowrap;
}
.first {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background: yellow;
}
.last {
display: flex;
height: 100vh;
background: yellow;
}
.box {
font-size: 5vw;
text-align: center;
line-height: 80px;
background-color: white;
border-radius: 8px;
color: #222;
font-weight: 700;
margin-left: -200px;
margin-top: -200px;
will-change: transform;
display: flex;
z-index: 2;
}
.box.active {
background-color: orange;
border: 2px solid white;
}
- js
let sections = gsap.utils.toArray('.section');
let scrollTween = gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: 'none',
scrollTrigger: {
trigger: '.horizontal',
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1),
end: () => '+=' + document.querySelector('.horizontal').offsetWidth,
},
});
gsap.set('.box-1, .box-2 ', { y: 100 });
gsap.to('.box-1', {
y: -130,
duration: 2,
ease: 'elastic',
scrollTrigger: {
trigger: '.box-1',
containerAnimation: scrollTween,
start: 'left center',
toggleActions: 'play none none reset',
id: '1',
},
});
gsap.to('.box-2', {
y: -120,
rotate: 750,
backgroundColor: '#1e90ff',
ease: 'none',
scrollTrigger: {
trigger: '.box-2',
containerAnimation: scrollTween,
start: 'center 80%',
end: 'center 20%',
scrub: true,
id: '2',
},
});
ScrollTrigger.create({
trigger: '.box-3',
containerAnimation: scrollTween,
toggleClass: 'active',
start: 'center 60%',
id: '3',
markers:true,
});
- 구조작성
<div class="wrapper">
<div class="factsContainer">
<h2>안녕하세요</h2>
<div class="factsContainer_sm">
<div class="fact">
<h3>신입 프론트엔드 김망고이다:</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>일머리 좋은 신입 프론트엔드입니다</h3>
</div>
<div class="fact">
<h3>좋아하는 음식</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>떡순이</h3>
</div>
<div class="fact">
<h3>자신있는 기술스택</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>자바스크립트🤙</h3>
</div>
<div class="fact">
<h3>앞으로 공부하려고 하는 기술스택</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>앵귤러, 노드JS, 타입스크립트</h3>
</div>
<div class="fact">
<h3>좋아하는 가수</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>뉴진스 민지</h3>
</div>
<div class="fact">
<h3>좋아하는 것</h3>
<img src="https://source.unsplash.com/random" alt="" />
<h3>강아지</h3>
</div>
</div>
</div>
<div class="socialContainer">
<h2>저를 안뽑으시면 내일도 제생각이 나실꺼에요</h2>
</div>
</div>
- css 작성
* {
margin: 0;
padding: 0;
}
.wrapper {
background: #1d373f;
overflow-x: hidden;
}
.factsContainer {
min-height: 100vh;
padding: 0em 2em;
text-align: center;
line-height: 10vh;
}
.factsContainer h2 {
font-size: 1.8em;
transform: scale(0);
padding: 2em 0em;
margin-bottom: 15vh;
color: #f0c368;
}
.factsContainer_sm,
.factsContainer_sm1 {
display: flex;
width: 300vw;
}
.fact {
display: flex;
flex-direction: column;
height: 40vh;
flex: 1;
justify-content: space-between;
padding: 1em;
align-items: center;
color: #f0c368;
}
.fact img {
width: 25vw;
height: 100vh;
margin: 1em;
}
.socialContainer {
width: 100vw;
height: 100vh;
color: white;
font-size: 5em;
}
- js
//애니메이션 해야할 대상이 많으므로 전체 타임라인에 부모요소를 추가한다
let scroll_tl = gsap.timeline({
scrollTrigger: {
trigger: '.factsContainer',
markers: true,
start: 'top top', //시작점 설정 윗방향기준 뷰포드 중앙에서 시작
end: '+=300', //300px 떨어진거리에서 끝
scrub: true,
},
});
let facts = document.querySelectorAll('.fact');
let factW = document.querySelector('.factsContainer_sm').clientWidth;
console.log(factW);
scroll_tl.to('.factsContainer h2', {
scale: 1.5,
duration: 1,
ease: 'slow',
});
scroll_tl.to(facts, {
xPercent: -85 * (facts.length - 1), //x이동거리
scrollTrigger: {
trigger: '.factsContainer_sm',
start: 'center center',
pin: true,
scrub: 1,
snap: 1 / (facts.length - 1),
// base vertical scrolling on how wide the container is so it feels more natural.
// end: () => `+=${smallFactsContainer.offsetWidth}`
end: () => `+=${factW}`,
},
});
gsap.config({ trialWarn: false });
📢 코드설명
let scroll_tl=애니메이션 해야할 대상이 많으므로 전체 타임라인에 부모요소를 추가한다.
start = 스크롤의 시작방향과 지점 설정
end: 끝점 설정