I cant speak english very well, sorry !!!!
Under is Translation!
I am currently developing a web application using Next.js 14 app router.
Initially, I created a simple horizontally scrolling content in a client-side component, as commented out in my code. However, I discovered the GSAP library while attempting to create a more complex, infinite-looping content where the central content is highlighted.
While integrating the GSAP library, I encountered an issue where the scrolling functionality does not work.
The type of content I want to create can be seen at the following URL: [https://wsss.tistory.com/1822]
Could anyone help me understand why the scrolling is not functioning and how I can achieve the desired infinite scrolling effect with GSAP?
Thank you in advance for your assistance.
"use client";
import styles from "@/app/(beforeLogin)/(contents)/_component/PhotocardList.module.css";
import Photocard from "@/app/(beforeLogin)/(contents)/_component/Photocard"
import { useEffect, useState, useRef } from "react";
import gsap from "gsap";
import {useGSAP} from "@gsap/react"
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger)
export default function PhotocardList(props) {
const { projectListData } = props;
const [projectList, setProjectList] = useState([]);
// const [isScrollableR, setIsScrollableR] = useState(false);
// const [isScrollableL, setIsScrollableL] = useState(false);
const scrollContainerRef = useRef(null);
useEffect(() => {
console.log("projectListData:", projectListData);
setProjectList(projectListData);
}, [projectListData])
useEffect(() => {
console.log("scrollContainerRef:", scrollContainerRef.current);
}, []);
useGSAP(() => {
console.log("데이터 만들어짐 :", projectList);
function buildSeamlessLoop(items, spacing) {
let overlap = Math.ceil(1 / spacing);
let startTime = items.length * spacing + 0.5;
let loopTime = (items.length + overlap) * spacing + 1;
let rawSequence = gsap.timeline({ paused: true });
let seamlessLoop = gsap.timeline({
paused: true,
repeat: -1,
onRepeat() {
this._time === this._dur && (this.tTime += this._dur - 0.01);
}
});
let l = items.length + overlap * 2;
let time = 0;
let i, index, item;
gsap.set(items, { xPercent: 500, opacity: 0, scale: 0 });
for (i = 0; i < l; i++) {
index = i % items.length;
item = items[index];
time = i * spacing;
rawSequence.fromTo(item, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, zIndex: 100, duration: 0.5, yoyo: true, repeat: 1, ease: "power1.in", immediateRender: false }, time)
.fromTo(item, { xPercent: 400 }, { xPercent: -400, duration: 1, ease: "none", immediateRender: false }, time);
i <= items.length && seamlessLoop.add("label" + i, time);
}
rawSequence.time(startTime);
seamlessLoop.to(rawSequence, {
time: loopTime,
duration: loopTime - startTime,
ease: "none"
}).fromTo(rawSequence, { time: overlap * spacing + 1 }, {
time: startTime,
duration: startTime - (overlap * spacing + 1),
immediateRender: false,
ease: "none"
});
return seamlessLoop;
}
if (!scrollContainerRef.current) return;
const container = scrollContainerRef.current;
const cards = container.querySelectorAll(`.${styles.photocard}`);
console.log("container:", container);
console.log("cards:", cards);
if (!cards.length) return;
let iteration = 0;
const spacing = 0.1;
const snap = gsap.utils.snap(spacing);
const seamlessLoop = buildSeamlessLoop(cards, spacing)
console.log("seamlessLoop:", seamlessLoop);
const scrub = gsap.to(seamlessLoop, {
totalTime: 0,
duration: 0.5,
ease: "power3",
paused: true
})
const trigger = ScrollTrigger.create({
start: 0,
onUpdate(self) {
if (self.progress === 1 && self.direction > 0 && !self.wrapping) {
wrapForward(self);
}
else if (self.progress < 1e-5 && self.direction < 0 && !self.wrapping) {
wrapBackward(self);
}
else {
scrub.vars.totalTime = snap((iteration + self.progress) * seamlessLoop.duration)
scrub.invalidate().restart();
self.wrapping = false;
}
},
end: "+=3000",
pin: `.${styles.photocard_box}`
})
console.log("ScrollTrigger:", trigger);
const handleWheel = (e) => {
e.preventDefault();
console.log("Wheel event:", e);
scrubTo(scrub.vars.totalTime + (e.deltaY * 0.01));
console.log("scrub :", scrub.vars);
}
container.addEventListener("wheel", handleWheel);
return (
() => {
trigger.kill();
scrub.kill();
container.removeEventListener("wheel", handleWheel);
}
)
function wrapForward(trigger) {
iteration++;
trigger.wrapping = true;
trigger.scroll(trigger.start + 1);
}
function wrapBackward(trigger) {
iteration--;
if (iteration < 0) {
iteration = 9;
seamlessLoop.totalTime(seamlessLoop.totalTime() + seamlessLoop.duration() * 10);
scrub.pause();
}
trigger.wrapping = true;
trigger.scroll(trigger.end - 1);
}
function scrubTo(totalTime) {
let progress = (totalTime - seamlessLoop.duration() * iteration) / seamlessLoop.duration();
if (progress > 1) {
wrapForward(trigger);
} else if (progress < 0) {
wrapBackward(trigger);
} else {
trigger.scroll(trigger.start + progress * (trigger.end - trigger.start));
}
}
}, [projectList])
// useEffect(() => {
// // 스크롤 속도 조절
// const scrollSpeed = 0.7;
// // 마우스 휠 이벤트
// const handleWheel = (e) => {
// if (scrollContainerRef.current) {
// scrollContainerRef.current.scrollLeft += e.deltaY * scrollSpeed;
// }
// }
// // 스크롤 가능 여부 확인
// const checkScrollable = () => {
// if (scrollContainerRef.current) {
// const { scrollWidth, clientWidth, scrollLeft } = scrollContainerRef.current;
// setIsScrollableR(scrollWidth > clientWidth && scrollLeft < scrollWidth - 5 - clientWidth);
// setIsScrollableL(scrollLeft > 5);
// }
// };
// // 스크롤 중앙 판별
// const handleScroll = () => {
// const container = scrollContainerRef.current;
// if (!container) return;
// const cards = container.querySelectorAll(`.${styles.photocard}`);
// const containerCenter = container.offsetWidth / 2;
// const containerScrollLeft = container.scrollLeft;
// cards.forEach( (card) => {
// const cardRect = card.getBoundingClientRect();
// const cardCenter = cardRect.left + cardRect.width / 2;
// const offset = Math.abs(containerCenter - (cardCenter - containerScrollLeft));
// const scale = Math.max(0.8, 1 - offset / 500);
// const opacity = Math.max(0.5, 1 - offset / 500);
// card.style.transform = `scale(${scale})`;
// card.style.opacity = opacity;
// })
// }
// // 스크롤 변화 적용
// const container = scrollContainerRef.current;
// if (container) {
// container.addEventListener("wheel", handleWheel);
// container.addEventListener("scroll", handleScroll);
// checkScrollable(); // 초기 체크
// window.addEventListener("resize", checkScrollable); // 창 크기 변경 시 체크
// container.addEventListener("scroll", checkScrollable); // 스크롤 시 체크
// }
// // 이벤트 변경 로직 종료 시 이벤트 핸들 회수
// return (() => {
// if (container) {
// container.removeEventListener("wheel", handleWheel);
// container.removeEventListener("scroll", handleScroll);
// container.removeEventListener("scroll", checkScrollable);
// }
// window.removeEventListener("resize", checkScrollable);
// })
// }, [projectList])
return (
<div className={[styles.photocard_box,].join(" ")}>
{/* <div className={[styles.photocard_group, "custom_scrollbar", isScrollableR && styles.gradient_r, isScrollableL && styles.gradient_l].join(" ")} ref={scrollContainerRef}> */}
<div className={[styles.photocard_group, "custom_scrollbar",].join(" ")} ref={scrollContainerRef}>
{projectList.map((project, idx) => {
return (
<Photocard key={["photocard", "map", idx].join("_")} projectData={project} />
)
})}
</div>
</div>
)
}
- chat with chatGPT on 4 days
- search with Docs and Example on other’s site
help me
이민섭 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.