源码获取
私信回复水平切换获取完整源码
演示效果

html 文件
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>JS 实现的多个图片水平切换效果</title> <link rel="stylesheet" href="./style.css"></head><body> <div class="gallery"> <div class="loading-overlay" id="loadingOverlay"> <div class="spinner"></div> <p>加载中...</p> </div> <ul class="cards"></ul> <svg class="nav-btn prev-btn" width="56.898" height="91" viewBox="0 0 56.898 91" aria-label="上一张"> <path d="M45.5,0,91,56.9,48.452,24.068,0,56.9Z" transform="translate(0 91) rotate(-90)" fill="rgba(255, 255, 255, 0.2)"></path> </svg> <svg class="nav-btn next-btn" width="56.898" height="91" viewBox="0 0 56.898 91" aria-label="下一张"> <path d="M45.5,0,91,56.9,48.452,24.068,0,56.9Z" transform="translate(56.898) rotate(90)" fill="rgba(255, 255, 255, 0.2)"></path> </svg> </div> <script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/ScrollTrigger.min.js"></script> <script src="./script.js"></script></body></html>css 文件
@import url("https://fonts.googleapis.com/css2?family=Roboto&family=Zen+Dots&display=swap");* { box-sizing: border-box;}body { background: #222; min-height: 100vh; padding: 0; margin: 0; color: #fff; font-family: "Zen Dots", sans-serif; text-shadow: #000 1px 1px 5px, #000 -1px -1px 5px;}.gallery { position: absolute; width: 100%; height: 100vh; overflow: hidden;}.cards { position: absolute; width: 800px; height: 500px; top: 50%; left: 50%; transform: translate(-50%, -50%);}.cards li { list-style: none; padding: 0; margin: 0; width: 800px; height: 500px; text-align: center; line-height: 18rem; font-size: 2rem; border: 5px solid white; border-radius: 15px; position: absolute; top: 0; left: 0; background-size: cover; box-shadow: #000 1px 1px 5px, #000 -1px -1px 5px;}li:hover { color: #fff; transition: color 0.3s ease; box-shadow: rgba(255, 255, 255, 0.6) 0px 4px 15px, rgba(0, 0, 0, 0.3) 0px 4px 10px; z-index: 10; /* Brings the hovered card to the front */}.actions { position: absolute; bottom: 25px; left: 50%; transform: translateX(-50%);}button { display: inline-block; outline: none; border: none; padding: 8px 14px; background: #414141; background-image: -webkit-linear-gradient(top, #575757, #414141); background-image: -moz-linear-gradient(top, #575757, #414141); background-image: -ms-linear-gradient(top, #575757, #414141); background-image: -o-linear-gradient(top, #575757, #414141); background-image: linear-gradient(to bottom, #575757, #414141); text-shadow: #000 1px 1px 5px, #000 -1px -1px 5px; box-shadow: #000 1px 1px 5px, #000 -1px -1px 5px; color: #aaaaaa; text-decoration: none; margin: 0 auto 10px; border-radius: 4px; padding: 12px 25px; font-family: "Zen Dots", sans-serif; text-transform: uppercase; cursor: pointer; font-size: 10px; font-weight: 400; line-height: 18px;}button:hover { background: #57a818; background-image: -webkit-linear-gradient(top, #57a818, #4d9916); background-image: -moz-linear-gradient(top, #57a818, #4d9916); background-image: -ms-linear-gradient(top, #57a818, #4d9916); background-image: -o-linear-gradient(top, #57a818, #4d9916); background-image: linear-gradient(to bottom, #57a818, #4d9916); text-shadow: 0px 1px 0px #32610e; box-shadow: 0px 1px 0px fefefe; color: #ffffff; text-decoration: none;}.refresh-btn { position: fixed; top: 20px; right: 20px; padding: 10px 20px; background: #007bff; color: white; border: solid 2px black; border-radius: 5px; cursor: pointer; transition: background 0.3s ease; filter: drop-shadow(1px 2px 1px rgba(0, 0, 50, 0.7)) drop-shadow(2px 4px 3px rgba(0, 0, 0, 0.5));}.refresh-btn:hover { background: #0056b3;}.instruction { position: fixed; top: 80px; right: 20px; padding: 10px 20px; font-size: 0.8rem;}.nav-btn { position: absolute; top: 50%; transform: translateY(-50%); cursor: pointer; z-index: 100; transition: fill 0.3s ease; width: 40px; height: 60px;}.nav-btn:hover path { fill: rgba(255, 255, 255, 0.8);}.prev-btn { left: 20px;}.next-btn { right: 20px;}js 文件
gsap.registerPlugin(ScrollTrigger);const cardsContainer = document.querySelector(".cards");const loadingOverlay = document.getElementById("loadingOverlay");function preloadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(url); img.onerror = () => resolve(url); img.src = url; });}async function initGallery() { for (let i = 0; i <= 10; i++) { const li = document.createElement("li"); li.className = "card"; li.textContent = i; li.dataset.imageIndex = i; li.style.background = `url('https://picsum.photos/200/150?random=${i + 1}') center/cover no-repeat`; cardsContainer.appendChild(li); } loadingOverlay.classList.add("hidden"); setupGallery(); loadImagesSequentially();}async function loadImagesSequentially() { const cards = document.querySelectorAll(".card"); const loadInterval = 200; for (let i = 0; i < cards.length; i++) { await new Promise(resolve => setTimeout(resolve, loadInterval)); const card = cards[i]; const index = card.dataset.imageIndex; const img = new Image(); img.onload = () => { card.style.background = `url('https://picsum.photos/800/600?random=${parseInt(index) + 1}') center/cover no-repeat`; }; img.onerror = () => {}; img.src = `https://picsum.photos/800/600?random=${parseInt(index) + 1}`; }}function refreshCards() { const cards = document.querySelectorAll(".card"); cards.forEach((card, index) => { card.style.animation = "none"; card.style.background = `url('https://picsum.photos/800/600?random=${ index + 1 }&t=${Date.now()}') center/cover no-repeat`; card.offsetHeight; card.style.animation = null; });}function setupGallery() { let iteration = 0; const spacing = 0.1, snap = gsap.utils.snap(spacing), cards = gsap.utils.toArray(".cards li"), seamlessLoop = buildSeamlessLoop(cards, spacing), scrub = gsap.to(seamlessLoop, { totalTime: 0, duration: 0.5, ease: "power3", paused: true }), 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: ".gallery" }); 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)); } } document .querySelector(".next-btn") .addEventListener("click", () => scrubTo(scrub.vars.totalTime + spacing)); document .querySelector(".prev-btn") .addEventListener("click", () => scrubTo(scrub.vars.totalTime - spacing)); let autoplayInterval; const gallery = document.querySelector(".gallery"); function startAutoplay() { autoplayInterval = setInterval(() => { scrubTo(scrub.vars.totalTime + spacing); }, 3000); } function stopAutoplay() { if (autoplayInterval) { clearInterval(autoplayInterval); autoplayInterval = null; } } startAutoplay(); gallery.addEventListener("mouseenter", () => { stopAutoplay(); }); gallery.addEventListener("mouseleave", () => { startAutoplay(); });}function buildSeamlessLoop(items, spacing) { let overlap = Math.ceil(1 / spacing), startTime = items.length * spacing + 0.5, loopTime = (items.length + overlap) * spacing + 1, rawSequence = gsap.timeline({ paused: true }), seamlessLoop = gsap.timeline({ paused: true, repeat: -1, onRepeat() { this._time === this._dur && (this._tTime += this._dur - 0.01); } }), l = items.length + overlap * 2, time = 0, i, index, item; gsap.set(items, { xPercent: 300, 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: 300 }, { xPercent: -300, 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;}initGallery();gsap.registerPlugin(ScrollTrigger);const cardsContainer = document.querySelector(".cards");const loadingOverlay = document.getElementById("loadingOverlay");function preloadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(url); img.onerror = () => resolve(url); img.src = url; });}async function initGallery() { for (let i = 0; i <= 10; i++) { const li = document.createElement("li"); li.className = "card"; li.textContent = i; li.dataset.imageIndex = i; li.style.background = `url('https://picsum.photos/200/150?random=${i + 1}') center/cover no-repeat`; cardsContainer.appendChild(li); } loadingOverlay.classList.add("hidden"); setupGallery(); loadImagesSequentially();}async function loadImagesSequentially() { const cards = document.querySelectorAll(".card"); const loadInterval = 200; for (let i = 0; i < cards.length; i++) { await new Promise(resolve => setTimeout(resolve, loadInterval)); const card = cards[i]; const index = card.dataset.imageIndex; const img = new Image(); img.onload = () => { card.style.background = `url('https://picsum.photos/800/600?random=${parseInt(index) + 1}') center/cover no-repeat`; }; img.onerror = () => {}; img.src = `https://picsum.photos/800/600?random=${parseInt(index) + 1}`; }}function refreshCards() { const cards = document.querySelectorAll(".card"); cards.forEach((card, index) => { card.style.animation = "none"; card.style.background = `url('https://picsum.photos/800/600?random=${ index + 1 }&t=${Date.now()}') center/cover no-repeat`; card.offsetHeight; card.style.animation = null; });}function setupGallery() { let iteration = 0; const spacing = 0.1, snap = gsap.utils.snap(spacing), cards = gsap.utils.toArray(".cards li"), seamlessLoop = buildSeamlessLoop(cards, spacing), scrub = gsap.to(seamlessLoop, { totalTime: 0, duration: 0.5, ease: "power3", paused: true }), 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: ".gallery" }); 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)); } } document .querySelector(".next-btn") .addEventListener("click", () => scrubTo(scrub.vars.totalTime + spacing)); document .querySelector(".prev-btn") .addEventListener("click", () => scrubTo(scrub.vars.totalTime - spacing)); let autoplayInterval; const gallery = document.querySelector(".gallery"); function startAutoplay() { autoplayInterval = setInterval(() => { scrubTo(scrub.vars.totalTime + spacing); }, 3000); } function stopAutoplay() { if (autoplayInterval) { clearInterval(autoplayInterval); autoplayInterval = null; } } startAutoplay(); gallery.addEventListener("mouseenter", () => { stopAutoplay(); }); gallery.addEventListener("mouseleave", () => { startAutoplay(); });}function buildSeamlessLoop(items, spacing) { let overlap = Math.ceil(1 / spacing), startTime = items.length * spacing + 0.5, loopTime = (items.length + overlap) * spacing + 1, rawSequence = gsap.timeline({ paused: true }), seamlessLoop = gsap.timeline({ paused: true, repeat: -1, onRepeat() { this._time === this._dur && (this._tTime += this._dur - 0.01); } }), l = items.length + overlap * 2, time = 0, i, index, item; gsap.set(items, { xPercent: 300, 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: 300 }, { xPercent: -300, 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;}initGallery();
夜雨聆风