window.__STYLEGALLERY_ASSETS__={}; gsap.registerPlugin(ScrollTrigger, Draggable); let iteration = 0; gsap.set(".cards li", { xPercent: 400, opacity: 0, scale: 0 }); const spacing = 0.1, snapTime = gsap.utils.snap(spacing), cards = gsap.utils.toArray(".cards li"), animateFunc = (element) => { const tl = gsap.timeline(); tl.fromTo(element, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, zIndex: 100, duration: 0.5, yoyo: true, repeat: 1, ease: "power1.in", immediateRender: false }).fromTo(element, { xPercent: 400 }, { xPercent: -400, duration: 1, ease: "none", immediateRender: false }, 0); return tl; }, seamlessLoop = buildSeamlessLoop(cards, spacing, animateFunc), playhead = { offset: 0 }, wrapTime = gsap.utils.wrap(0, seamlessLoop.duration()), scrub = gsap.to(playhead, { // we reuse this tween to smoothly scrub the playhead on the seamlessLoop offset: 0, onUpdate() { seamlessLoop.time(wrapTime(playhead.offset)); }, duration: 0.5, ease: "power3", paused: true }), trigger = ScrollTrigger.create({ start: 0, onUpdate(self) { let scroll = self.scroll(); if (scroll > self.end - 1) { wrap(1, 2); } else if (scroll < 1 && self.direction < 0) { wrap(-1, self.end - 2); } else { scrub.vars.offset = (iteration + self.progress) * seamlessLoop.duration(); scrub.invalidate().restart(); } }, end: "+=3000", pin: ".gallery" }), progressToScroll = (progress) => gsap.utils.clamp(1, trigger.end - 1, gsap.utils.wrap(0, 1, progress) * trigger.end), wrap = (iterationDelta, scrollTo) => { iteration += iterationDelta; trigger.scroll(scrollTo); trigger.update(); }; ScrollTrigger.addEventListener("scrollEnd", () => scrollToOffset(scrub.vars.offset)); function scrollToOffset(offset) { let snappedTime = snapTime(offset), progress = (snappedTime - seamlessLoop.duration() * iteration) / seamlessLoop.duration(), scroll = progressToScroll(progress); if (progress >= 1 || progress < 0) { return wrap(Math.floor(progress), scroll); } trigger.scroll(scroll); } document.querySelector(".next").addEventListener("click", () => scrollToOffset(scrub.vars.offset + spacing)); document.querySelector(".prev").addEventListener("click", () => scrollToOffset(scrub.vars.offset - spacing)); Draggable.create(".drag-proxy", { type: "x", trigger: ".cards", onPress() { this.startOffset = scrub.vars.offset; }, onDrag() { scrub.vars.offset = this.startOffset + (this.startX - this.x) * 1e-3; scrub.invalidate().restart(); }, onDragEnd() { scrollToOffset(scrub.vars.offset); } }); function buildSeamlessLoop(items, spacing2, animateFunc2) { let overlap = Math.ceil(1 / spacing2), startTime = items.length * spacing2 + 0.5, loopTime = (items.length + overlap) * spacing2 + 1, rawSequence = gsap.timeline({ paused: true }), seamlessLoop2 = gsap.timeline({ // this merely scrubs the playhead of the rawSequence so that it appears to seamlessly loop paused: true, repeat: -1, // to accommodate infinite scrolling/looping onRepeat() { this._time === this._dur && (this._tTime += this._dur - 0.01); } }), l = items.length + overlap * 2, time, i, index; for (i = 0; i < l; i++) { index = i % items.length; time = i * spacing2; rawSequence.add(animateFunc2(items[index]), time); i <= items.length && seamlessLoop2.add("label" + i, time); } rawSequence.time(startTime); seamlessLoop2.to(rawSequence, { time: loopTime, duration: loopTime - startTime, ease: "none" }).fromTo(rawSequence, { time: overlap * spacing2 + 1 }, { time: startTime, duration: startTime - (overlap * spacing2 + 1), immediateRender: false, ease: "none" }); return seamlessLoop2; }