轮播图edit icon

作者:
藤原
Fork(复制)
下载
嵌入
设置
BUG反馈
index.html
style.css
index.js
现在支持上传本地图片了!
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>宇宙系列幻灯片</title>
    <style>
        @import url("https://fonts.cdnfonts.com/css/thegoodmonolith");

        *,
        *::after,
        *::before {
            box-sizing: border-box;
        }

        :root {
            --color-text: #fff;
            --color-bg: #000;
            --thumb-width: 120px;
            --line-spacing: 10px;
            --line-base-height: 15px;
            --line-max-height: 50px;
        }

        body {
            margin: 0;
            color: var(--color-text);
            background-color: var(--color-bg);
            font-family: "TheGoodMonolith", sans-serif;
            overflow: hidden;
            height: 100vh;
        }

        .slides {
            width: 100%;
            height: 100vh;
            overflow: hidden;
            display: grid;
            grid-template-rows: 100%;
            grid-template-columns: 100%;
            place-items: center;
        }

        .slide {
            width: 100%;
            height: 100%;
            grid-area: 1 / 1 / -1 / -1;
            pointer-events: none;
            opacity: 0;
            overflow: hidden;
            position: relative;
            display: grid;
            place-items: center;
            will-change: transform, opacity;
        }

        .slide--current {
            pointer-events: auto;
            opacity: 1;
        }

        .slide__img {
            width: 100%;
            height: 100%;
            background-size: cover;
            background-position: left center;
            background-repeat: no-repeat;
            will-change: transform, opacity, filter;
        }

        .scroll-hint {
            position: fixed;
            top: 2rem;
            right: 2rem;
            color: #fff;
            z-index: 100;
            font-size: 1rem;
        }

        /* 底部UI容器 */
        .bottom-ui-container {
            position: fixed;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 720px;
            max-width: 100%;
            z-index: 100;
            padding-bottom: 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .slide-section {
            color: #fff;
            font-size: 1.8rem;
            font-weight: bold;
            width: 100%;
            text-align: center;
            opacity: 0.9;
            letter-spacing: 1px;
            margin-bottom: 36px;
        }

        .slide-counter {
            display: flex;
            align-items: center;
            width: 100%;
            justify-content: space-between;
            color: #fff;
            font-size: 0.825rem;
            margin-bottom: 24px;
        }

        .counter-display {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .counter-nav {
            width: 20px;
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.3s;
        }

        .counter-nav:hover {
            opacity: 1;
        }

        .counter-divider {
            opacity: 0.6;
            font-size: 0.8rem;
        }

        .slide-title-container {
            width: 100%;
            text-align: center;
            height: 30px;
            overflow: hidden;
            margin-bottom: 16px;
            position: relative;
        }

        .slide-title {
            position: absolute;
            width: 100%;
            color: #fff;
            font-size: 1.2rem;
            opacity: 0.8;
            transition: transform 0.5s ease, opacity 0.5s ease;
            left: 0;
        }

        .slide-title.exit-up {
            transform: translateY(-30px);
            opacity: 0;
        }

        .slide-title.enter-up {
            transform: translateY(30px);
            opacity: 0;
        }

        /* 拖动指示器样式 */
        .drag-indicator {
            width: 100%;
            height: 50px;
            pointer-events: none;
            margin-bottom: 8px;
            position: relative;
        }

        .lines-container {
            display: flex;
            height: 100%;
            width: 100%;
            position: relative;
            align-items: flex-end;
            justify-content: space-between;
        }

        .drag-line {
            width: 2px;
            background-color: rgba(255, 255, 255, 0.3);
            height: var(--line-base-height);
            transform-origin: bottom center;
            transition: height 0.6s cubic-bezier(0.25, 0.1, 0.25, 1),
                background-color 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
        }

        .thumbs-container {
            width: 100%;
            background: rgba(0, 0, 0, 0.5);
            overflow: hidden;
        }

        .slide-thumbs {
            display: flex;
            position: relative;
            background: transparent;
            padding: 0;
            z-index: 11;
            gap: 0;
        }

        .frost-bg {
            display: none;
        }

        .slide-thumb {
            width: var(--thumb-width);
            height: 80px;
            background-size: cover;
            background-position: left center;
            cursor: pointer;
            opacity: 0.5;
            transition: opacity 0.3s;
            border: none;
            outline: none;
            box-shadow: none;
            margin: 0;
            position: relative;
            z-index: 12;
        }

        .slide-thumb:hover {
            opacity: 0.8;
        }

        .slide-thumb.active {
            opacity: 1;
            transform: none;
            border: none;
            outline: none;
            box-shadow: none;
        }
    </style>
</head>
<body>
    <div class="scroll-hint">滚动或拖动</div>

    <!-- 底部UI容器,包含所有底部元素 -->
    <div class="bottom-ui-container">
        <div class="slide-section">宇宙系列</div>
        <div class="slide-counter">
            <div class="counter-nav prev-slide">⟪</div>
            <div class="counter-display">
                <span class="current-slide">01</span>
                <span class="counter-divider">//</span>
                <span class="total-slides">06</span>
            </div>
            <div class="counter-nav next-slide">⟫</div>
        </div>
        <div class="slide-title-container">
            <div class="slide-title">宇宙和谐</div>
        </div>
        <div class="drag-indicator"></div>
        <div class="thumbs-container">
            <div class="frost-bg"></div>
            <div class="slide-thumbs"></div>
        </div>
    </div>

    <div class="slides">
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/1d4dbaff-8087-4451-a727-9d3266b573dd?format=jpeg)"></div>
        </div>
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/67ef01f5-09c8-4117-9199-04ec5323d64f?format=jpeg)"></div>
        </div>
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/77f73423-0eb7-4eaa-a782-036457985290?format=jpeg)"></div>
        </div>
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/3dd498a9-169d-4b69-8e2e-df042123c124?format=jpeg)"></div>
        </div>
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/ca346107-04c8-4241-85e6-f26c8b64c85c?format=jpeg)"></div>
        </div>
        <div class="slide">
            <div class="slide__img" style="background-image: url(https://cdn.cosmos.so/7d2c5113-b2d3-4f9d-8215-f46fbb679f31?format=jpeg)"></div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script>
    <script>
        // 方向常量
        const NEXT = 1;
        const PREV = -1;

        // 幻灯片标题数组
        const slideTitles = [
            "宇宙和谐",
            "星际旅程",
            "以太视觉",
            "量子场域",
            "天体轨迹",
            "宇宙低语"
        ];

        // 全局变量跟踪当前悬停的缩略图
        let currentHoveredThumb = null;
        let mouseOverThumbnails = false;
        let lastHoveredThumbIndex = null;

        // 全局动画状态管理
        let isAnimating = false;
        let pendingNavigation = null;

        // 更新导航UI元素
        function updateNavigationUI(disabled) {
            const navButtons = document.querySelectorAll(".counter-nav");
            navButtons.forEach((btn) => {
                btn.style.opacity = disabled ? "0.3" : "";
                btn.style.pointerEvents = disabled ? "none" : "";
            });

            const thumbs = document.querySelectorAll(".slide-thumb");
            thumbs.forEach((thumb) => {
                thumb.style.pointerEvents = disabled ? "none" : "";
            });
        }

        // 更新幻灯片计数器
        function updateSlideCounter(index) {
            const currentSlideEl = document.querySelector(".current-slide");
            if (currentSlideEl) {
                currentSlideEl.textContent = String(index + 1).padStart(2, "0");
            }
        }

        // 更新幻灯片标题
        function updateSlideTitle(index) {
            const titleContainer = document.querySelector(".slide-title-container");
            const currentTitle = document.querySelector(".slide-title");
            if (!titleContainer || !currentTitle) return;

            const newTitle = document.createElement("div");
            newTitle.className = "slide-title enter-up";
            newTitle.textContent = slideTitles[index];

            titleContainer.appendChild(newTitle);
            currentTitle.classList.add("exit-up");

            void newTitle.offsetWidth;

            setTimeout(() => {
                newTitle.classList.remove("enter-up");
            }, 10);

            setTimeout(() => {
                currentTitle.remove();
            }, 500);
        }

        // 更新拖动线条
        function updateDragLines(activeIndex, forceUpdate = false) {
            const lines = document.querySelectorAll(".drag-line");
            if (!lines.length) return;

            lines.forEach((line) => {
                line.style.height = "var(--line-base-height)";
                line.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
            });

            if (activeIndex === null) return;

            const slideCount = document.querySelectorAll(".slide").length;
            const lineCount = lines.length;
            const thumbWidth = 720 / slideCount;
            const centerPosition = (activeIndex + 0.5) * thumbWidth;
            const lineWidth = 720 / lineCount;

            for (let i = 0; i < lineCount; i++) {
                const linePosition = (i + 0.5) * lineWidth;
                const distFromCenter = Math.abs(linePosition - centerPosition);
                const maxDistance = thumbWidth * 0.7;

                if (distFromCenter <= maxDistance) {
                    const normalizedDist = distFromCenter / maxDistance;
                    const waveHeight = Math.cos((normalizedDist * Math.PI) / 2);
                    const height = parseInt(
                        getComputedStyle(document.documentElement).getPropertyValue(
                            "--line-base-height"
                        )
                    ) + waveHeight * 35;
                    const opacity = 0.3 + waveHeight * 0.4;
                    const delay = normalizedDist * 100;

                    if (forceUpdate) {
                        lines[i].style.height = `${height}px`;
                        lines[i].style.backgroundColor = `rgba(255, 255, 255, ${opacity})`;
                    } else {
                        setTimeout(() => {
                            if (
                                currentHoveredThumb === activeIndex ||
                                (mouseOverThumbnails && lastHoveredThumbIndex === activeIndex)
                            ) {
                                lines[i].style.height = `${height}px`;
                                lines[i].style.backgroundColor = `rgba(255, 255, 255, ${opacity})`;
                            }
                        }, delay);
                    }
                }
            }
        }

        // 幻灯片类
        class Slideshow {
            DOM = {
                el: null,
                slides: null,
                slidesInner: null
            };
            current = 0;
            slidesTotal = 0;

            constructor(DOM_el) {
                this.DOM.el = DOM_el;
                this.DOM.slides = [...this.DOM.el.querySelectorAll(".slide")];
                this.DOM.slidesInner = this.DOM.slides.map((item) =>
                    item.querySelector(".slide__img")
                );
                this.DOM.slides[this.current].classList.add("slide--current");
                this.slidesTotal = this.DOM.slides.length;
            }

            next() {
                this.navigate(NEXT);
            }

            prev() {
                this.navigate(PREV);
            }

            goTo(index) {
                if (isAnimating) {
                    pendingNavigation = { type: "goto", index };
                    return false;
                }

                if (index === this.current) return false;

                isAnimating = true;
                updateNavigationUI(true);

                const previous = this.current;
                this.current = index;

                const thumbs = document.querySelectorAll(".slide-thumb");
                thumbs.forEach((thumb, i) => {
                    thumb.classList.toggle("active", i === index);
                });

                updateSlideCounter(index);
                updateSlideTitle(index);
                updateDragLines(index, true);

                const direction = index > previous ? 1 : -1;
                const currentSlide = this.DOM.slides[previous];
                const currentInner = this.DOM.slidesInner[previous];
                const upcomingSlide = this.DOM.slides[index];
                const upcomingInner = this.DOM.slidesInner[index];

                gsap
                    .timeline({
                        onStart: () => {
                            this.DOM.slides[index].classList.add("slide--current");
                            gsap.set(upcomingSlide, { zIndex: 99 });
                        },
                        onComplete: () => {
                            this.DOM.slides[previous].classList.remove("slide--current");
                            gsap.set(upcomingSlide, { zIndex: 1 });

                            isAnimating = false;
                            updateNavigationUI(false);

                            if (pendingNavigation) {
                                const { type, index, direction } = pendingNavigation;
                                pendingNavigation = null;

                                setTimeout(() => {
                                    if (type === "goto") {
                                        this.goTo(index);
                                    } else if (type === "navigate") {
                                        this.navigate(direction);
                                    }
                                }, 50);
                            }

                            if (mouseOverThumbnails && lastHoveredThumbIndex !== null) {
                                currentHoveredThumb = lastHoveredThumbIndex;
                                updateDragLines(lastHoveredThumbIndex, true);
                            }
                        }
                    })
                    .addLabel("start", 0)
                    .fromTo(
                        upcomingSlide,
                        {
                            autoAlpha: 1,
                            scale: 0.1,
                            yPercent: direction === 1 ? 100 : -100
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            scale: 0.4,
                            yPercent: 0
                        },
                        "start"
                    )
                    .fromTo(
                        upcomingInner,
                        {
                            filter: "contrast(100%) saturate(100%)",
                            transformOrigin: "100% 50%",
                            scaleY: 4
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            scaleY: 1
                        },
                        "start"
                    )
                    .fromTo(
                        currentInner,
                        {
                            filter: "contrast(100%) saturate(100%)"
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            filter: "contrast(120%) saturate(140%)"
                        },
                        "start"
                    )
                    .addLabel("middle", "start+=0.6")
                    .to(
                        upcomingSlide,
                        {
                            duration: 1,
                            ease: "power4.inOut",
                            scale: 1
                        },
                        "middle"
                    )
                    .to(
                        currentSlide,
                        {
                            duration: 1,
                            ease: "power4.inOut",
                            scale: 0.98,
                            autoAlpha: 0
                        },
                        "middle"
                    );
            }

            navigate(direction) {
                if (isAnimating) {
                    pendingNavigation = { type: "navigate", direction };
                    return false;
                }

                isAnimating = true;
                updateNavigationUI(true);

                const previous = this.current;
                this.current =
                    direction === 1
                        ? this.current < this.slidesTotal - 1
                            ? ++this.current
                            : 0
                        : this.current > 0
                        ? --this.current
                        : this.slidesTotal - 1;

                const thumbs = document.querySelectorAll(".slide-thumb");
                thumbs.forEach((thumb, index) => {
                    thumb.classList.toggle("active", index === this.current);
                });

                updateSlideCounter(this.current);
                updateSlideTitle(this.current);
                updateDragLines(this.current, true);

                const currentSlide = this.DOM.slides[previous];
                const currentInner = this.DOM.slidesInner[previous];
                const upcomingSlide = this.DOM.slides[this.current];
                const upcomingInner = this.DOM.slidesInner[this.current];

                gsap
                    .timeline({
                        onStart: () => {
                            this.DOM.slides[this.current].classList.add("slide--current");
                            gsap.set(upcomingSlide, { zIndex: 99 });
                        },
                        onComplete: () => {
                            this.DOM.slides[previous].classList.remove("slide--current");
                            gsap.set(upcomingSlide, { zIndex: 1 });

                            isAnimating = false;
                            updateNavigationUI(false);

                            if (pendingNavigation) {
                                const { type, index, direction } = pendingNavigation;
                                pendingNavigation = null;

                                setTimeout(() => {
                                    if (type === "goto") {
                                        this.goTo(index);
                                    } else if (type === "navigate") {
                                        this.navigate(direction);
                                    }
                                }, 50);
                            }

                            if (mouseOverThumbnails && lastHoveredThumbIndex !== null) {
                                currentHoveredThumb = lastHoveredThumbIndex;
                                updateDragLines(lastHoveredThumbIndex, true);
                            }
                        }
                    })
                    .addLabel("start", 0)
                    .fromTo(
                        upcomingSlide,
                        {
                            autoAlpha: 1,
                            scale: 0.1,
                            yPercent: direction === 1 ? 100 : -100
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            scale: 0.4,
                            yPercent: 0
                        },
                        "start"
                    )
                    .fromTo(
                        upcomingInner,
                        {
                            filter: "contrast(100%) saturate(100%)",
                            transformOrigin: "100% 50%",
                            scaleY: 4
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            scaleY: 1
                        },
                        "start"
                    )
                    .fromTo(
                        currentInner,
                        {
                            filter: "contrast(100%) saturate(100%)"
                        },
                        {
                            duration: 0.7,
                            ease: "expo",
                            filter: "contrast(120%) saturate(140%)"
                        },
                        "start"
                    )
                    .addLabel("middle", "start+=0.6")
                    .to(
                        upcomingSlide,
                        {
                            duration: 1,
                            ease: "power4.inOut",
                            scale: 1
                        },
                        "middle"
                    )
                    .to(
                        currentSlide,
                        {
                            duration: 1,
                            ease: "power4.inOut",
                            scale: 0.98,
                            autoAlpha: 0
                        },
                        "middle"
                    );
            }
        }

        // 初始化
        document.addEventListener("DOMContentLoaded", () => {
            const slides = document.querySelector(".slides");
            const slideshow = new Slideshow(slides);

            // 创建缩略图
            const thumbsContainer = document.querySelector(".slide-thumbs");
            const slideImgs = document.querySelectorAll(".slide__img");
            const slideCount = slideImgs.length;

            if (thumbsContainer) {
                thumbsContainer.innerHTML = "";
                slideImgs.forEach((img, index) => {
                    const bgImg = img.style.backgroundImage;
                    const thumb = document.createElement("div");
                    thumb.className = "slide-thumb";
                    thumb.style.backgroundImage = bgImg;
                    if (index === 0) {
                        thumb.classList.add("active");
                    }

                    thumb.addEventListener("click", () => {
                        lastHoveredThumbIndex = index;
                        slideshow.goTo(index);
                    });

                    thumb.addEventListener("mouseenter", () => {
                        currentHoveredThumb = index;
                        lastHoveredThumbIndex = index;
                        mouseOverThumbnails = true;

                        if (!isAnimating) {
                            updateDragLines(index, true);
                        }
                    });

                    thumb.addEventListener("mouseleave", () => {
                        if (currentHoveredThumb === index) {
                            currentHoveredThumb = null;
                        }
                    });

                    thumbsContainer.appendChild(thumb);
                });
            }

            // 创建拖动指示线
            const dragIndicator = document.querySelector(".drag-indicator");
            if (dragIndicator) {
                dragIndicator.innerHTML = "";

                const linesContainer = document.createElement("div");
                linesContainer.className = "lines-container";
                dragIndicator.appendChild(linesContainer);

                const totalLines = 60;
                for (let i = 0; i < totalLines; i++) {
                    const line = document.createElement("div");
                    line.className = "drag-line";
                    linesContainer.appendChild(line);
                }
            }

            // 设置总幻灯片数
            const totalSlidesEl = document.querySelector(".total-slides");
            if (totalSlidesEl) {
                totalSlidesEl.textContent = String(slideCount).padStart(2, "0");
            }

            // 添加导航处理
            const prevButton = document.querySelector(".prev-slide");
            const nextButton = document.querySelector(".next-slide");

            if (prevButton) {
                prevButton.addEventListener("click", () => slideshow.prev());
            }

            if (nextButton) {
                nextButton.addEventListener("click", () => slideshow.next());
            }

            // 初始化计数器和线条
            updateSlideCounter(0);
            updateDragLines(0, true);

            // 缩略图区域鼠标事件
            const thumbsArea = document.querySelector(".thumbs-container");
            if (thumbsArea) {
                thumbsArea.addEventListener("mouseenter", () => {
                    mouseOverThumbnails = true;
                });

                thumbsArea.addEventListener("mouseleave", () => {
                    mouseOverThumbnails = false;
                    currentHoveredThumb = null;
                    updateDragLines(null);
                });
            }

            // 初始化GSAP Observer
            try {
                if (typeof Observer !== "undefined") {
                    Observer.create({
                        type: "wheel,touch,pointer",
                        onDown: () => {
                            if (!isAnimating) slideshow.prev();
                        },
                        onUp: () => {
                            if (!isAnimating) slideshow.next();
                        },
                        wheelSpeed: -1,
                        tolerance: 10
                    });
                }
                else if (typeof gsap.Observer !== "undefined") {
                    gsap.Observer.create({
                        type: "wheel,touch,pointer",
                        onDown: () => {
                            if (!isAnimating) slideshow.prev();
                        },
                        onUp: () => {
                            if (!isAnimating) slideshow.next();
                        },
                        wheelSpeed: -1,
                        tolerance: 10
                    });
                }
                else {
                    console.warn("GSAP Observer插件未找到,使用备用方案");

                    document.addEventListener("wheel", (e) => {
                        if (isAnimating) return;

                        if (e.deltaY > 0) {
                            slideshow.next();
                        } else {
                            slideshow.prev();
                        }
                    });

                    let touchStartY = 0;

                    document.addEventListener("touchstart", (e) => {
                        touchStartY = e.touches[0].clientY;
                    });

                    document.addEventListener("touchend", (e) => {
                        if (isAnimating) return;

                        const touchEndY = e.changedTouches[0].clientY;
                        const diff = touchEndY - touchStartY;

                        if (Math.abs(diff) > 50) {
                            if (diff > 0) {
                                slideshow.prev();
                            } else {
                                slideshow.next();
                            }
                        }
                    });
                }
            } catch (error) {
                console.error("初始化Observer错误:", error);
            }

            // 键盘导航
            document.addEventListener("keydown", (e) => {
                if (isAnimating) return;

                if (e.key === "ArrowRight") slideshow.next();
                else if (e.key === "ArrowLeft") slideshow.prev();
            });
        });
    </script>
</body>
</html>
        
预览
控制台