<!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>
index.html
style.css
index.js