const largeScreen = window.matchMedia("(min-width: 1537px)");
function setupResponsiveAnimation() {
// possible-slide 가로 이동
let x1 = largeScreen.matches ? "-40%" : "-51%";
gsap.to(".possible-slide", {
scrollTrigger: {
trigger: ".sc-possible",
start: "0% 0%",
end: "50% 0%",
scrub: true,
toggleActions: "play none none reverse",
// markers: true,
},
x: x1,
});
gsap.to(".sc-possible .possible-slide", {
scrollTrigger: {
trigger: ".sc-possible",
start: "0% 0%",
end: "50% 0%",
scrub: true,
invalidateOnRefresh: true, // 동적으로 변경되는 함수를 사용할 때 넣어줘야 한다.
markers: true,
},
x: function () {
return -$(".sc-possible .inner-slide").outerWidth() - 100; // inner-slide의 width만큼 이동, width가 변경될 때를 대비하여 함수를 사용한다.
},
});
수정 전에는 특정한 고정값만큼 이동하도록 하였다. 미디어 쿼리를 사용하긴 했지만 다양한 디바이스에 적용하기 어려울뿐더러 브라우저의 크기가 변경될 때를 대비할 수가 없다. 그리하여 sc-possible
내의 inner-slide
너비만큼 x축으로 이동하도록 수정하였다. 이렇게 하면 브라우저 크기 변경 시에도 동일한 너비만큼 이동하여 사용자 경험을 향상시킬 수 있다.
💡invalidateOnRefresh: true
브라우저 창 크기 변경과 같은 이벤트가 발생할 때, 애니메이션 관련 값들을 새롭게 계산하는 옵션이다. 이는 동적인 레이아웃 변경에 대응하여 애니메이션을 정확하게 유지하는 데 유용하다.
특히, 함수를 사용하여 애니메이션의 속성 값을 계산할 때, 이 옵션을 활성화해야 한다. 이렇게 하면 창 크기 조정과 같은 상황에서도 함수가 다시 실행되어 적절한 값을 계산할 수 있다.
// 초기 상태 설정
gsap.from(".sc-banner .box1", { x: -300 });
gsap.from(".sc-banner .box2", { x: -300 });
gsap.from(".sc-banner .box3", { x: 300 });
gsap.from(".sc-banner .box", { filter: "none" });
let bannerMotion = gsap.timeline({
scrollTrigger: {
trigger: ".sc-banner",
start: "0% 90%",
end: "100% 0%",
scrub: true,
// markers: true,
},
});
bannerMotion
.to(".sc-banner .box1", { x: 0 }, "motion")
.to(".sc-banner .box2", { x: 0 }, "motion")
.to(".sc-banner .box3", { x: 0 }, "motion")
.to(".sc-banner .banner-content", { opacity: 1 }, "motion2")
.to(".sc-banner .box", { filter: "blur(50px)" }, "motion2");
// sc-banner
gsap.set(".sc-banner .box1", { x: -300 });
gsap.set(".sc-banner .box2", { x: -300 });
gsap.set(".sc-banner .box3", { x: 300 });
const bannerMotion1 = gsap.timeline({
scrollTrigger: {
trigger: ".sc-banner",
start: "0% 98%",
end: "100% 85%",
scrub: true,
// markers: true,
},
});
bannerMotion1
.to(".sc-banner .box1", { x: 0 }, "a")
.to(".sc-banner .box2", { x: 0 }, "a")
.to(".sc-banner .box3", { x: 0 }, "a");
수정 전 해당 요소에 도달하기도 전에 애니메이션이 먼저 시작되는 문제가 있었다. 애니메이션 순서가 틀어져 아래 다른 섹션에도 영향을 미치는 문제가 있었는데, 이 문제는 from
대신 set
을 써서 해결할 수 있었다. from은 이전 상태로부터 현재 값으로 이동하는 애니메이션이기 때문에 이 경우에는 적합하지 않다.
💡gsap.from vs gsap.set
- from은 요소를 특정 상태에서 시작하여 현재 상태로 애니메이션하는 데 사용된다. 즉, from은 요소에 대해 지정된 시작 값으로부터 요소의 현재 값(또는 기본값)으로 이동하는 애니메이션을 생한다.
- set은 요소에 즉시 특정 스타일 또는 상태를 적용한다. set은 애니메이션이 없이 즉시 값을 적용하며, 애니메이션의 시작점이나 중간 상태를 설정하는 데 주로 사용된다.
// sc-intro
let introMotion = gsap.timeline({
scrollTrigger: {
trigger: ".intro-description",
start: "0% 0%",
end: "100% 100%",
scrub: true,
},
});
document
.querySelectorAll(".intro-description .description-text")
.forEach((elem, index, array) => {
introMotion.to(elem, { opacity: 1 });
if (index < array.length - 1) {
introMotion.to(elem, { opacity: 0 });
}
});
// sc-intro-bg
gsap.to(".sc-intro-bg", {
scrollTrigger: {
trigger: ".sc-intro",
start: "0% 0%",
end: "100% 100%",
scrub: true,
toggleActions: "play none none reverse",
},
opacity: 1,
});
let introMotion = gsap.timeline({
scrollTrigger: {
trigger: ".sc-intro",
start: "0% 0%",
end: "100% 100%",
scrub: true,
},
});
document
.querySelectorAll(".intro-description p")
.forEach((elem, index, array) => { // p 요소에 순차적으로 애니메이션을 적용하기 위해 반복문 사용
introMotion.to(elem, { opacity: 1 }); // 요소를 노출시킨다
if (index !== array.length - 1) { // 마지막 요소를 제외한 모든 요소
if (index === 0) { // 첫번째 요소일때만
introMotion.to(".sc-intro", { "--opacity": 1 }); // 배경을 dim처리 한다 그 이후에는 dim처리된 그대로 유지한다
}
introMotion.to(elem, { opacity: 0 }); // 다시 요소를 숨긴다
}
});
수정 전에는 <div class="sc-intro-bg"></div>
별도의 요소를 생성해서 dim처리를 해주었으나, 가상 선택자 ::before
를 사용하는 방식으로 변경하여 추가적인 html 마크업 없이 좀 더 가독성 좋고 효율적인 코드로 수정하였다.
💡 가상 선택자를 사용했을 때의 장점은?
- 추가적인 HTML 마크업 없이 콘텐츠 추가: ::before와 ::after 같은 가상 선택자를 사용하면, 추가적인 HTML 요소를 만들지 않고도 콘텐츠 앞이나 뒤에 텍스트나 이미지 등을 삽입할 수 있어 이는 HTML 문서를 깔끔하게 유지하는 데 도움이 된다.
- 유지 보수 용이: 스타일링 관련 변경사항을 CSS에서만 처리할 수 있으므로, HTML 구조를 변경하지 않고도 시각적 요소를 쉽게 수정할 수 있다.
- 콘텐츠와 스타일의 분리: 웹 접근성과 웹 표준을 준수하는 데 도움이 된다. 이는 콘텐츠(HTML)와 스타일링(CSS)을 분리하는 좋은 방법이다.
// sc-prove
let proveMotion = gsap.timeline({
scrollTrigger: {
trigger: ".sc-prove",
start: "0% 70%",
end: "100% 0%",
scrub: true,
},
});
proveMotion
.to(".box-before-prove", { width: "68%" }, "motion")
.to(".box-after-prove", { width: "68%" }, "motion")
.to(".prove-content p:first-child", { x: -200 }, "motion")
.to(".prove-content p:last-child", { x: 500 }, "motion");
// sc-global
let globalMotion = gsap.timeline({
scrollTrigger: {
trigger: ".sc-global",
start: "0% 70%",
end: "100% 0%",
scrub: true,
// markers: true,
},
});
globalMotion
.to(".box-before-global", { width: "68%" }, "motion")
.to(".box-after-global", { width: "68%" }, "motion")
.to(".global-content p:first-child", { x: -500 }, "motion")
.to(".global-content p:last-child", { x: 700 }, "motion");
// sc-value, sc-global
$(".side-animation").each(function () { // 두 섹션에 동일한 클래스명을 추가하여 반복문 실행
const sideAnimation = gsap.timeline({
scrollTrigger: {
trigger: $(this), // 해당 요소를 트리거로 지정
start: "0% 70%",
end: "100% 90%",
scrub: true,
// markers: true,
},
});
let before, after; // 변수 선언
if ($(this).attr("class").includes("value")) { // if문으로 분기 처리 - 해당 섹션의 클래스명에 "value"가 있을 경우
before = -172;
after = 134;
} else { // 그렇지 않을 경우
before = -114;
after = 120;
}
sideAnimation
.to($(this).find("[class*='box-before-']"), { xPercent: -100 }, "a") // 자기 자신의 너비만큼 100% 이동
.to($(this).find("[class*='box-after-']"), { xPercent: 100 }, "a") // 자기 자신의 너비만큼 100% 이동
.to($(this).find(".content p:first-child span"), { xPercent: before }, "a")
.to($(this).find(".content p:last-child span"), { xPercent: after }, "a");
});
수정 전에는 비슷한 구조의 코드가 여러번 반복되고 있었다. start, end 지점 및 모션이 동일한 코드이기 때문에 반복문을 사용하여 하나의 타임라인으로 수정하였다. 이렇게 수정함으로써 코드의 간결성과 가독성을 높였다.