generated from dellevin/template
添加星空相册
This commit is contained in:
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "local",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["http-server", ".", "-p", "3000", "-c-1"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__Claude_Preview__preview_start"
|
||||
]
|
||||
}
|
||||
}
|
||||
19
index.html
19
index.html
@@ -28,6 +28,8 @@
|
||||
<!-- <script defer src="https://umami.iletter.top/anyjs" data-website-id="ae6cd64c-5900-49c9-9c22-95cedc24a508"></script> -->
|
||||
<!-- ECharts 库 -->
|
||||
<script src="./static/js/echarts/echarts.min.js"></script>
|
||||
<!-- Three.js 用于星空预览 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<!-- 多语言 -->
|
||||
<script src="./static/js/language/zh.js"></script>
|
||||
<script src="./static/js/language/en.js"></script>
|
||||
@@ -37,6 +39,7 @@
|
||||
@import url("./static/css/memos.css");
|
||||
@import url("./static/css/game_dialog.css");
|
||||
@import url("./static/css/guestbook.css");
|
||||
@import url("./static/css/galaxy.css");
|
||||
</style>
|
||||
<title data-i18n="page_title">白荼 - BAITU</title>
|
||||
</head>
|
||||
@@ -171,10 +174,23 @@
|
||||
<div class="full-modal-content">
|
||||
<div class="guestbook-header">
|
||||
<h2 data-i18n="photo_album_title">旅途剪影</h2>
|
||||
<div class="header-controls">
|
||||
<div class="mode-switch-container">
|
||||
<button id="mode-switch" class="mode-switch-btn" title="切换模式">
|
||||
<span class="mode-label active" data-mode="galaxy">星空模式</span>
|
||||
<span class="mode-label" data-mode="waterfall">瀑布模式</span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="photo-album-close" class="guestbook-close" title="关闭">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guestbook-body photo-album-body">
|
||||
<!-- 照片网格将在这里动态加载 -->
|
||||
<!-- 照片网格将在这里动态加载(瀑布模式) -->
|
||||
</div>
|
||||
<!-- 星空模式画布 -->
|
||||
<div id="galaxy-canvas-wrapper" class="galaxy-canvas-wrapper">
|
||||
<canvas id="galaxy-canvas"></canvas>
|
||||
<div class="galaxy-tip">滚轮缩放 | 拖拽旋转 | 点击星星查看照片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -508,6 +524,7 @@
|
||||
<script src="./static/js/guestbook.js"></script>
|
||||
<script src="./static/js/blog_posts.js"></script>
|
||||
<script src="./static/js/photo_album.js"></script>
|
||||
<script src="./static/js/galaxy.js"></script>
|
||||
<script src="./static/js/tabchange.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
618
static/css/galaxy.css
Normal file
618
static/css/galaxy.css
Normal file
@@ -0,0 +1,618 @@
|
||||
/* ==================== 星空预览样式 ==================== */
|
||||
|
||||
/* 星空模式切换按钮(在弹窗header中) - 透明玻璃效果 + 黑色字体 */
|
||||
.galaxy-toggle-btn {
|
||||
display: none; /* 隐藏旧按钮 */
|
||||
}
|
||||
|
||||
/* 模式切换容器 */
|
||||
.mode-switch-container {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 模式切换按钮 */
|
||||
.mode-switch-btn {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mode-label.active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.mode-label:hover:not(.active) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 瀑布模式容器 */
|
||||
.waterfall-wrapper {
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, #f5f5f5 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.waterfall-wrapper.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.waterfall-container {
|
||||
column-count: 3;
|
||||
column-gap: 16px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 星空模式header样式 */
|
||||
.galaxy-mode-header {
|
||||
background: rgba(10, 10, 46, 0.95) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header h2 {
|
||||
color: #fff !important;
|
||||
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header .guestbook-close {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header .guestbook-close:hover {
|
||||
background: rgba(255, 0, 0, 0.5) !important;
|
||||
border-color: rgba(255, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header .mode-switch-btn {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header .mode-label {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.galaxy-mode-header .mode-label.active {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.waterfall-item {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.waterfall-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.waterfall-item img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.waterfall-item-info {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 900px) {
|
||||
.waterfall-container {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.waterfall-container {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 星空画布容器(在弹窗内) */
|
||||
.galaxy-canvas-wrapper {
|
||||
display: none;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: radial-gradient(ellipse at center, #1a1a3e 0%, #0a0a2e 50%, #050520 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.galaxy-canvas-wrapper.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 星空背景动画 - 繁杂无序 */
|
||||
.galaxy-canvas-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
/* 第一大区域 - 各种随机位置和大小 */
|
||||
radial-gradient(1px 1px at 5px 15px, #fff, transparent),
|
||||
radial-gradient(2px 2px at 45px 8px, rgba(255,255,255,0.85), transparent),
|
||||
radial-gradient(1px 1px at 82px 42px, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(3px 3px at 120px 25px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 155px 58px, rgba(255,255,255,0.9), transparent),
|
||||
radial-gradient(2px 2px at 192px 12px, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 228px 48px, #fff, transparent),
|
||||
radial-gradient(2px 2px at 265px 32px, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(1px 1px at 302px 65px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 338px 8px, #fff, transparent),
|
||||
radial-gradient(2px 2px at 375px 42px, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(1px 1px at 412px 18px, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(2px 2px at 448px 55px, rgba(255,255,255,0.3), transparent),
|
||||
radial-gradient(1px 1px at 485px 38px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 522px 72px, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(3px 3px at 558px 28px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 595px 52px, #fff, transparent),
|
||||
/* 第二大区域 - 更随机的分布 */
|
||||
radial-gradient(2px 2px at 22px 78px, rgba(255,255,255,0.45), transparent),
|
||||
radial-gradient(1px 1px at 68px 92px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 108px 85px, rgba(255,255,255,0.55), transparent),
|
||||
radial-gradient(2px 2px at 145px 98px, rgba(255,255,255,0.35), transparent),
|
||||
radial-gradient(1px 1px at 182px 88px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 218px 75px, rgba(255,255,255,0.65), transparent),
|
||||
radial-gradient(2px 2px at 255px 95px, rgba(255,255,255,0.45), transparent),
|
||||
radial-gradient(1px 1px at 292px 82px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 328px 98px, rgba(255,255,255,0.75), transparent),
|
||||
radial-gradient(2px 2px at 365px 78px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 402px 92px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 438px 85px, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(2px 2px at 475px 95px, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(1px 1px at 512px 78px, #fff, transparent),
|
||||
radial-gradient(1px 1px at 548px 88px, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(1px 1px at 585px 82px, rgba(255,255,255,0.3), transparent),
|
||||
/* 第三大区域 - 散落的小星星 */
|
||||
radial-gradient(1px 1px at 15px 108px, rgba(255,255,255,0.25), transparent),
|
||||
radial-gradient(1px 1px at 52px 118px, rgba(255,255,255,0.15), transparent),
|
||||
radial-gradient(1px 1px at 88px 112px, rgba(255,255,255,0.35), transparent),
|
||||
radial-gradient(1px 1px at 125px 105px, rgba(255,255,255,0.2), transparent),
|
||||
radial-gradient(1px 1px at 162px 120px, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 198px 108px, rgba(255,255,255,0.3), transparent),
|
||||
radial-gradient(1px 1px at 235px 115px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 272px 102px, rgba(255,255,255,0.2), transparent),
|
||||
radial-gradient(1px 1px at 308px 120px, rgba(255,255,255,0.35), transparent),
|
||||
radial-gradient(1px 1px at 345px 108px, rgba(255,255,255,0.45), transparent),
|
||||
radial-gradient(1px 1px at 382px 115px, rgba(255,255,255,0.15), transparent),
|
||||
radial-gradient(1px 1px at 418px 102px, rgba(255,255,255,0.25), transparent),
|
||||
radial-gradient(1px 1px at 455px 112px, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 492px 105px, rgba(255,255,255,0.3), transparent),
|
||||
radial-gradient(1px 1px at 528px 118px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 565px 108px, rgba(255,255,255,0.2), transparent),
|
||||
radial-gradient(1px 1px at 602px 115px, rgba(255,255,255,0.35), transparent),
|
||||
/* 额外的亮点 */
|
||||
radial-gradient(2px 2px at 35px 55px, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(3px 3px at 175px 35px, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(2px 2px at 315px 68px, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(2px 2px at 455px 25px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 535px 42px, rgba(255,255,255,0.9), transparent),
|
||||
radial-gradient(2px 2px at 75px 68px, rgba(255,255,255,0.65), transparent),
|
||||
radial-gradient(1px 1px at 205px 82px, rgba(255,255,255,0.75), transparent),
|
||||
radial-gradient(3px 3px at 385px 48px, rgba(255,255,255,0.55), transparent),
|
||||
radial-gradient(1px 1px at 505px 72px, rgba(255,255,255,0.85), transparent),
|
||||
radial-gradient(2px 2px at 625px 15px, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 652px 58px, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(2px 2px at 688px 32px, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(1px 1px at 725px 45px, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(2px 2px at 762px 62px, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 798px 18px, rgba(255,255,255,0.65), transparent),
|
||||
radial-gradient(1px 1px at 835px 75px, rgba(255,255,255,0.45), transparent),
|
||||
radial-gradient(2px 2px at 872px 42px, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(1px 1px at 908px 28px, rgba(255,255,255,0.55), transparent),
|
||||
radial-gradient(2px 2px at 945px 65px, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(1px 1px at 982px 38px, rgba(255,255,255,0.4), transparent);
|
||||
background-repeat: repeat;
|
||||
background-size: 1000px 130px;
|
||||
animation: twinkle 3s ease-in-out infinite alternate, drift 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0% { opacity: 0.4; }
|
||||
33% { opacity: 0.7; }
|
||||
66% { opacity: 0.5; }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
@keyframes drift {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(-10px, -5px); }
|
||||
}
|
||||
|
||||
.galaxy-canvas-wrapper canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.galaxy-tip {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.galaxy-canvas-wrapper.active .galaxy-tip {
|
||||
opacity: 1;
|
||||
animation: fadeInOut 3s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0%, 100% { opacity: 0; }
|
||||
20%, 80% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 星空模式独立按钮(旧版本,保留备用) */
|
||||
.galaxy-mode-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.galaxy-mode-btn:hover {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.galaxy-mode-btn svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.galaxy-mode-btn:hover svg {
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
/* 星空预览容器 */
|
||||
.galaxy-container {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #0a0a2e 0%, #16162e 50%, #0d0d3d 100%);
|
||||
z-index: 10000;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.galaxy-container.active {
|
||||
display: flex;
|
||||
animation: galaxyFadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes galaxyFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 星空头部 */
|
||||
.galaxy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.galaxy-header h2 {
|
||||
color: #FFD700;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
.galaxy-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.galaxy-tip {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.galaxy-close-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.galaxy-close-btn:hover {
|
||||
background: rgba(255, 0, 0, 0.5);
|
||||
border-color: rgba(255, 0, 0, 0.6);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 星空画布 */
|
||||
#galaxy-canvas {
|
||||
flex: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#galaxy-canvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 星星点击提示 */
|
||||
.star-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
pointer-events: none;
|
||||
z-index: 10002;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
max-width: 320px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.star-tooltip.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.star-tooltip img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.star-tooltip p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 星星放大弹窗 */
|
||||
.star-lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
z-index: 10003;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.star-lightbox.active {
|
||||
display: flex;
|
||||
animation: lightboxFadeIn 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes lightboxFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.star-lightbox-container {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.star-lightbox img {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
animation: imageZoomIn 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes imageZoomIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.star-lightbox-close {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: -10px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.star-lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: rotate(90deg);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.star-lightbox-info {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.star-lightbox-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.star-lightbox-nav:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.star-lightbox-nav.prev {
|
||||
left: -70px;
|
||||
}
|
||||
|
||||
.star-lightbox-nav.next {
|
||||
right: -70px;
|
||||
}
|
||||
|
||||
.star-lightbox-counter {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.galaxy-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #FFD700;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.galaxy-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.galaxy-header h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.galaxy-tip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.star-lightbox img {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
@@ -23,20 +23,7 @@
|
||||
"2799e89f4632184d151941741b59257f.jpg",
|
||||
"4c5a2a39e6ae9e01bf2ea9860ae8e41f.jpg",
|
||||
"3e0ad66c001c9894c26eac4b98601c73.jpg",
|
||||
"20955b34b4cbc661f3ec7ff2eda0b815.jpg",
|
||||
"1 (7).jpg",
|
||||
"1 (11).jpg",
|
||||
"1 (9).jpg",
|
||||
"1 (6).jpg",
|
||||
"1 (5).jpg",
|
||||
"1 (4).jpg",
|
||||
"1 (3).jpg",
|
||||
"1 (2).jpg",
|
||||
"1 (1).jpg",
|
||||
"1 (8).jpg",
|
||||
"1 (13).jpg",
|
||||
"1 (10).jpg",
|
||||
"1 (12).jpg"
|
||||
"20955b34b4cbc661f3ec7ff2eda0b815.jpg"
|
||||
],
|
||||
"total": 37,
|
||||
"lastUpdated": "2026-05-18 02:36:56"
|
||||
|
||||
948
static/js/galaxy.js
Normal file
948
static/js/galaxy.js
Normal file
@@ -0,0 +1,948 @@
|
||||
// ==================== 星空预览功能 ====================
|
||||
(function() {
|
||||
// DOM 元素
|
||||
const modeSwitch = document.getElementById('mode-switch');
|
||||
const photoAlbumModal = document.getElementById('photo-album-full-modal');
|
||||
const photoAlbumBody = document.querySelector('.photo-album-body');
|
||||
const galaxyCanvasWrapper = document.getElementById('galaxy-canvas-wrapper');
|
||||
const galaxyCanvas = document.getElementById('galaxy-canvas');
|
||||
const guestbookHeader = document.querySelector('#photo-album-full-modal .guestbook-header');
|
||||
|
||||
// Three.js 变量
|
||||
let scene, camera, renderer;
|
||||
let stars = [];
|
||||
let galaxyGroups = []; // 多个银河系组
|
||||
let mouse = { x: 0, y: 0, isDragging: false };
|
||||
let targetRotation = { x: 0, y: 0 };
|
||||
let currentRotation = { x: 0, y: 0 };
|
||||
let cameraDistance = 1000;
|
||||
let cameraTargetDistance = 1000;
|
||||
let photoData = [];
|
||||
let raycaster, mousePosition;
|
||||
let hoveredStar = null;
|
||||
let tooltip = null;
|
||||
let isGalaxyMode = false;
|
||||
let isWaterfallMode = false;
|
||||
let lastMode = 'galaxy'; // 保存用户最后选择的模式
|
||||
let animationId = null;
|
||||
let currentLightboxIndex = -1;
|
||||
let galaxyRotations = []; // 多个银河系的自旋角度
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
if (!modeSwitch || !photoAlbumModal) return;
|
||||
|
||||
// 绑定事件
|
||||
modeSwitch.addEventListener('click', toggleMode);
|
||||
|
||||
// 绑定模式标签点击事件
|
||||
document.querySelectorAll('.mode-label').forEach(label => {
|
||||
label.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const mode = label.dataset.mode;
|
||||
switchMode(mode);
|
||||
});
|
||||
});
|
||||
|
||||
// 创建提示元素
|
||||
createTooltip();
|
||||
|
||||
// 页面加载完成后,检查弹窗状态并初始化模式
|
||||
checkAndInitMode();
|
||||
}
|
||||
|
||||
// 检查并初始化模式
|
||||
function checkAndInitMode() {
|
||||
// 如果弹窗已经打开,初始化星空模式
|
||||
if (photoAlbumModal.classList.contains('active')) {
|
||||
initDefaultMode();
|
||||
} else {
|
||||
// 否则等待弹窗打开
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
if (photoAlbumModal.classList.contains('active')) {
|
||||
initDefaultMode();
|
||||
observer.disconnect(); // 只执行一次
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(photoAlbumModal, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认模式
|
||||
async function initDefaultMode() {
|
||||
if (isGalaxyMode || isWaterfallMode) return; // 已经激活了模式
|
||||
|
||||
// 切换到用户最后选择的模式(默认星空模式)
|
||||
await switchMode(lastMode);
|
||||
}
|
||||
|
||||
// 创建提示元素
|
||||
function createTooltip() {
|
||||
tooltip = document.createElement('div');
|
||||
tooltip.className = 'star-tooltip';
|
||||
tooltip.innerHTML = `
|
||||
<img src="" alt="star-preview">
|
||||
<p></p>
|
||||
`;
|
||||
document.body.appendChild(tooltip);
|
||||
}
|
||||
|
||||
// 切换模式
|
||||
function toggleMode() {
|
||||
if (isGalaxyMode) {
|
||||
// 当前是星空模式,切换到瀑布模式
|
||||
switchMode('waterfall');
|
||||
} else if (isWaterfallMode) {
|
||||
// 当前是瀑布模式,切换到星空模式
|
||||
switchMode('galaxy');
|
||||
} else {
|
||||
// 默认打开星空模式
|
||||
switchMode('galaxy');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到指定模式
|
||||
async function switchMode(mode) {
|
||||
// 保存用户选择的模式
|
||||
lastMode = mode;
|
||||
|
||||
// 关闭所有模式
|
||||
closeAllModes();
|
||||
|
||||
// 加载照片数据
|
||||
await loadPhotoData();
|
||||
|
||||
// 更新UI状态
|
||||
document.querySelectorAll('.mode-label').forEach(label => {
|
||||
label.classList.remove('active');
|
||||
if (label.dataset.mode === mode) {
|
||||
label.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 切换header样式
|
||||
if (guestbookHeader) {
|
||||
if (mode === 'galaxy') {
|
||||
guestbookHeader.classList.add('galaxy-mode-header');
|
||||
} else {
|
||||
guestbookHeader.classList.remove('galaxy-mode-header');
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'galaxy') {
|
||||
// 打开星空模式
|
||||
photoAlbumBody.style.display = 'none';
|
||||
galaxyCanvasWrapper.classList.add('active');
|
||||
|
||||
// 初始化Three.js场景
|
||||
initScene();
|
||||
|
||||
// 创建星空
|
||||
createGalaxy();
|
||||
|
||||
// 开始动画
|
||||
animate();
|
||||
|
||||
isGalaxyMode = true;
|
||||
} else if (mode === 'waterfall') {
|
||||
// 瀑布模式 - 使用原来的photo-album-body
|
||||
// photo-album-body已经默认显示,不需要额外操作
|
||||
isWaterfallMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭所有模式
|
||||
function closeAllModes() {
|
||||
// 关闭星空模式
|
||||
if (isGalaxyMode) {
|
||||
galaxyCanvasWrapper.classList.remove('active');
|
||||
cleanupScene();
|
||||
isGalaxyMode = false;
|
||||
}
|
||||
|
||||
// 关闭瀑布模式
|
||||
if (isWaterfallMode) {
|
||||
isWaterfallMode = false;
|
||||
}
|
||||
|
||||
// 恢复照片显示
|
||||
photoAlbumBody.style.display = '';
|
||||
|
||||
// 隐藏提示
|
||||
if (tooltip) {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载照片数据
|
||||
async function loadPhotoData() {
|
||||
try {
|
||||
const response = await fetch('./static/img/photos/photos.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load photos.json');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const photoFiles = data.photos || [];
|
||||
|
||||
photoData = photoFiles.map((filename, index) => ({
|
||||
id: index + 1,
|
||||
src: `./static/img/photos/${encodeURIComponent(filename)}`,
|
||||
filename: filename
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载照片失败:', error);
|
||||
photoData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Three.js场景
|
||||
function initScene() {
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
|
||||
// 创建多个银河系组
|
||||
galaxyGroups = [];
|
||||
galaxyRotations = [];
|
||||
|
||||
// 创建9个银河系(合理距离,多种角度)
|
||||
const galaxyPositions = [
|
||||
// 中心主银河系
|
||||
{ x: 0, y: 0, z: 0, scale: 1.0, rotX: 0, rotZ: 0 },
|
||||
// 前面银河系(z正方向)
|
||||
{ x: 500, y: 200, z: 600, scale: 0.7, rotX: 0.1, rotZ: -0.05 },
|
||||
{ x: -400, y: 350, z: 500, scale: 0.65, rotX: -0.08, rotZ: 0.12 },
|
||||
{ x: 300, y: -250, z: 700, scale: 0.6, rotX: 0.15, rotZ: -0.1 },
|
||||
// 近距离(600-900单位)
|
||||
{ x: 700, y: 200, z: -500, scale: 0.75, rotX: 0.1, rotZ: -0.05 },
|
||||
{ x: -600, y: -350, z: -400, scale: 0.7, rotX: -0.08, rotZ: 0.12 },
|
||||
// 中距离(1000-1500单位)
|
||||
{ x: 1200, y: 500, z: -1000, scale: 0.6, rotX: 0.15, rotZ: -0.1 },
|
||||
{ x: -1100, y: 400, z: -900, scale: 0.55, rotX: -0.12, rotZ: 0.08 },
|
||||
{ x: 400, y: -900, z: -800, scale: 0.65, rotX: 0.05, rotZ: 0.15 },
|
||||
];
|
||||
|
||||
galaxyPositions.forEach((pos, index) => {
|
||||
const galaxyGroup = new THREE.Group();
|
||||
galaxyGroup.position.set(pos.x, pos.y, pos.z);
|
||||
galaxyGroup.scale.set(pos.scale, pos.scale, pos.scale);
|
||||
|
||||
// 设置不同的角度
|
||||
galaxyGroup.rotation.x = pos.rotX;
|
||||
galaxyGroup.rotation.z = pos.rotZ;
|
||||
|
||||
scene.add(galaxyGroup);
|
||||
galaxyGroups.push(galaxyGroup);
|
||||
galaxyRotations.push(0);
|
||||
});
|
||||
|
||||
// 获取容器尺寸
|
||||
const containerRect = galaxyCanvasWrapper.getBoundingClientRect();
|
||||
|
||||
// 创建相机
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
containerRect.width / containerRect.height,
|
||||
1,
|
||||
5000 // 调整远裁剪面
|
||||
);
|
||||
camera.position.z = cameraDistance;
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
canvas: galaxyCanvas,
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
renderer.setSize(containerRect.width, containerRect.height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
// 添加环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
scene.add(ambientLight);
|
||||
|
||||
// 添加点光源
|
||||
const pointLight = new THREE.PointLight(0xFFD700, 1, 5000);
|
||||
pointLight.position.set(0, 0, 1000);
|
||||
scene.add(pointLight);
|
||||
|
||||
// 创建射线检测器
|
||||
raycaster = new THREE.Raycaster();
|
||||
mousePosition = new THREE.Vector2();
|
||||
|
||||
// 绑定事件
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
// 创建星空
|
||||
function createGalaxy() {
|
||||
// 为每个银河系创建星星
|
||||
galaxyGroups.forEach((galaxyGroup, index) => {
|
||||
if (index === 0) {
|
||||
// 主银河系 - 有照片星星
|
||||
if (photoData.length > 0) {
|
||||
photoData.forEach((photo, i) => {
|
||||
createPhotoStar(photo, i, galaxyGroup);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加装饰性星星
|
||||
addDecorativeStars(galaxyGroup, 1, index);
|
||||
} else {
|
||||
// 其他银河系 - 只有装饰性星星,使用不同的配色
|
||||
addDecorativeStars(galaxyGroup, 0.7, index); // 稍微小一些,传入银河系索引
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建照片星星
|
||||
function createPhotoStar(photo, index, galaxyGroup) {
|
||||
// 创建圆形纹理
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const texture = textureLoader.load(photo.src);
|
||||
|
||||
// 创建正方形几何体
|
||||
const geometry = new THREE.PlaneGeometry(30, 30);
|
||||
|
||||
// 创建材质 - 无滤镜
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.95,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// 创建网格
|
||||
const star = new THREE.Mesh(geometry, material);
|
||||
|
||||
// 随机位置(银河系分布)
|
||||
const radius = 200 + Math.random() * 600;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = (Math.random() - 0.5) * Math.PI * 0.3; // 扁平分布
|
||||
|
||||
star.position.x = radius * Math.cos(theta) * Math.cos(phi);
|
||||
star.position.y = radius * Math.sin(phi) * 0.3; // 更扁平
|
||||
star.position.z = radius * Math.sin(theta) * Math.cos(phi);
|
||||
|
||||
// 存储照片信息
|
||||
star.userData = {
|
||||
photoId: photo.id,
|
||||
photoSrc: photo.src,
|
||||
photoFilename: photo.filename
|
||||
};
|
||||
|
||||
// 面向相机
|
||||
star.lookAt(camera.position);
|
||||
|
||||
galaxyGroup.add(star);
|
||||
stars.push(star);
|
||||
}
|
||||
|
||||
// 创建默认星星
|
||||
function createDefaultStars() {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const geometry = new THREE.CircleGeometry(2, 32);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xFFD700,
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
});
|
||||
const star = new THREE.Mesh(geometry, material);
|
||||
|
||||
// 随机位置
|
||||
const radius = 100 + Math.random() * 800;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = (Math.random() - 0.5) * Math.PI;
|
||||
|
||||
star.position.x = radius * Math.cos(theta) * Math.cos(phi);
|
||||
star.position.y = radius * Math.sin(phi);
|
||||
star.position.z = radius * Math.sin(theta) * Math.cos(phi);
|
||||
|
||||
galaxyGroup.add(star);
|
||||
stars.push(star);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加装饰性星星(银河系效果)
|
||||
function addDecorativeStars(galaxyGroup, scale = 1, galaxyIndex = 0) {
|
||||
const starCount = Math.floor(4000 * scale); // 根据缩放调整星星数量
|
||||
const starGeometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
const colors = new Float32Array(starCount * 3);
|
||||
const sizes = new Float32Array(starCount);
|
||||
|
||||
// 银河系参数
|
||||
const bulgeRadius = 120; // 银核半径
|
||||
const diskRadius = 700; // 银盘半径
|
||||
const armCount = 4; // 旋臂数量
|
||||
const spiralAngle = 12; // 螺旋角(度)
|
||||
const spiralTightness = 0.005; // 螺旋紧密度
|
||||
|
||||
// 根据银河系索引选择配色方案
|
||||
const colorScheme = galaxyIndex === 0 ? 'warm' : getColorScheme(galaxyIndex);
|
||||
|
||||
// 获取银河系配色方案
|
||||
function getColorScheme(index) {
|
||||
const schemes = [
|
||||
'blue', // 蓝色系
|
||||
'purple', // 紫色系
|
||||
'cyan', // 青色系
|
||||
'pink', // 粉色系
|
||||
'red', // 红色系
|
||||
'green', // 绿色系
|
||||
'orange', // 橙色系
|
||||
'white', // 白色系
|
||||
];
|
||||
return schemes[(index - 1) % schemes.length];
|
||||
}
|
||||
|
||||
// 淡化颜色(向白色靠拢)
|
||||
function fadeColor(color, fadeAmount = 0.4) {
|
||||
return [
|
||||
color[0] + (1 - color[0]) * fadeAmount,
|
||||
color[1] + (1 - color[1]) * fadeAmount,
|
||||
color[2] + (1 - color[2]) * fadeAmount
|
||||
];
|
||||
}
|
||||
|
||||
// 根据配色方案获取颜色
|
||||
function getColor(scheme, brightness, distanceFromCenter) {
|
||||
let color;
|
||||
|
||||
switch (scheme) {
|
||||
case 'warm': // 主银河系 - 暖黄色(不淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [1, 0.9 + brightness * 0.1, 0.7 + brightness * 0.3];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [1, 0.8 + Math.random() * 0.15, 0.4 + Math.random() * 0.3];
|
||||
} else {
|
||||
color = [0.9 + Math.random() * 0.1, 0.85 + Math.random() * 0.1, 0.6 + Math.random() * 0.2];
|
||||
}
|
||||
return color; // 主银河系不淡化
|
||||
|
||||
case 'blue': // 蓝色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.3, 0.5 + brightness * 0.3, 0.9 + brightness * 0.1];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.2 + Math.random() * 0.1, 0.4 + Math.random() * 0.2, 0.8 + Math.random() * 0.15];
|
||||
} else {
|
||||
color = [0.1 + Math.random() * 0.15, 0.3 + Math.random() * 0.2, 0.7 + Math.random() * 0.2];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'purple': // 紫色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.7 + brightness * 0.3, 0.3, 0.9 + brightness * 0.1];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.6 + Math.random() * 0.2, 0.2 + Math.random() * 0.1, 0.8 + Math.random() * 0.15];
|
||||
} else {
|
||||
color = [0.5 + Math.random() * 0.2, 0.1 + Math.random() * 0.15, 0.7 + Math.random() * 0.2];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'cyan': // 青色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.2, 0.8 + brightness * 0.2, 0.9 + brightness * 0.1];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.1 + Math.random() * 0.1, 0.7 + Math.random() * 0.15, 0.8 + Math.random() * 0.15];
|
||||
} else {
|
||||
color = [0.05 + Math.random() * 0.1, 0.6 + Math.random() * 0.2, 0.7 + Math.random() * 0.2];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'pink': // 粉色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.9 + brightness * 0.1, 0.4 + brightness * 0.2, 0.6 + brightness * 0.2];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.8 + Math.random() * 0.15, 0.3 + Math.random() * 0.15, 0.5 + Math.random() * 0.2];
|
||||
} else {
|
||||
color = [0.7 + Math.random() * 0.2, 0.2 + Math.random() * 0.15, 0.4 + Math.random() * 0.2];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'red': // 红色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.9 + brightness * 0.1, 0.3, 0.3];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.8 + Math.random() * 0.15, 0.2 + Math.random() * 0.1, 0.2 + Math.random() * 0.1];
|
||||
} else {
|
||||
color = [0.7 + Math.random() * 0.2, 0.1 + Math.random() * 0.15, 0.1 + Math.random() * 0.15];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'green': // 绿色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.3, 0.8 + brightness * 0.2, 0.4];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.2 + Math.random() * 0.1, 0.7 + Math.random() * 0.15, 0.3 + Math.random() * 0.1];
|
||||
} else {
|
||||
color = [0.1 + Math.random() * 0.15, 0.6 + Math.random() * 0.2, 0.2 + Math.random() * 0.15];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'orange': // 橙色系(淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.9 + brightness * 0.1, 0.6 + brightness * 0.2, 0.2];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.8 + Math.random() * 0.15, 0.5 + Math.random() * 0.15, 0.15 + Math.random() * 0.1];
|
||||
} else {
|
||||
color = [0.7 + Math.random() * 0.2, 0.4 + Math.random() * 0.2, 0.1 + Math.random() * 0.15];
|
||||
}
|
||||
return fadeColor(color, 0.45);
|
||||
|
||||
case 'white': // 白色系(已经是淡色,稍微淡化)
|
||||
if (distanceFromCenter < 0.3) {
|
||||
color = [0.9 + brightness * 0.1, 0.9 + brightness * 0.1, 0.9 + brightness * 0.1];
|
||||
} else if (distanceFromCenter < 0.5) {
|
||||
color = [0.8 + Math.random() * 0.1, 0.8 + Math.random() * 0.1, 0.8 + Math.random() * 0.1];
|
||||
} else {
|
||||
color = [0.7 + Math.random() * 0.15, 0.7 + Math.random() * 0.15, 0.7 + Math.random() * 0.15];
|
||||
}
|
||||
return fadeColor(color, 0.3); // 白色系淡化程度低一些
|
||||
|
||||
default:
|
||||
color = [1, 1, 1];
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
let x, y, z, brightness;
|
||||
|
||||
// 根据概率分配
|
||||
const rand = Math.random();
|
||||
|
||||
if (rand < 0.15) {
|
||||
// ==================== 银核(15%)====================
|
||||
// 非常明亮的圆球区域,大量星星密集分布
|
||||
|
||||
// 使用球形分布,中心密度最高
|
||||
const r = Math.pow(Math.random(), 0.2) * bulgeRadius; // 高度集中在中心
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1); // 均匀的球形分布
|
||||
|
||||
x = r * Math.sin(phi) * Math.cos(theta);
|
||||
y = r * Math.cos(phi) * 0.5; // 稍微扁平一些
|
||||
z = r * Math.sin(phi) * Math.sin(theta);
|
||||
|
||||
// 银核非常亮,中心最亮
|
||||
brightness = 0.85 + Math.pow(1 - r / bulgeRadius, 2) * 0.15;
|
||||
sizes[i] = 3 + Math.random() * 4; // 大尺寸的明亮星星
|
||||
|
||||
} else if (rand < 0.85) {
|
||||
// ==================== 旋臂(70%)====================
|
||||
// 星星严格分布在旋臂上
|
||||
|
||||
// 选择一条旋臂
|
||||
const armIndex = Math.floor(Math.random() * armCount);
|
||||
const armBaseAngle = (armIndex / armCount) * Math.PI * 2;
|
||||
|
||||
// 从银核向外延伸的距离
|
||||
const distance = bulgeRadius + Math.pow(Math.random(), 0.6) * (diskRadius - bulgeRadius);
|
||||
|
||||
// 螺旋线角度
|
||||
const spiralTheta = armBaseAngle + spiralTightness * distance * Math.log(1 + distance / bulgeRadius);
|
||||
|
||||
// 旋臂宽度(更粗壮)
|
||||
const armWidth = 880 + distance * 0.12;
|
||||
|
||||
// 高斯分布让星星集中在旋臂中心
|
||||
const gaussianRandom = (Math.random() + Math.random() + Math.random()) / 3 - 0.5;
|
||||
const offset = gaussianRandom * armWidth * 0.5;
|
||||
|
||||
// 计算位置
|
||||
const angle = spiralTheta;
|
||||
x = distance * Math.cos(angle) + offset * Math.cos(angle + Math.PI / 2);
|
||||
z = distance * Math.sin(angle) + offset * Math.sin(angle + Math.PI / 2);
|
||||
|
||||
// 高度分布(扁平)
|
||||
y = (Math.random() - 0.5) * 15 * (distance / diskRadius);
|
||||
|
||||
// 旋臂较亮
|
||||
brightness = Math.max(0.6, 1 - distance / diskRadius * 0.3);
|
||||
sizes[i] = 1.5 + Math.random() * 2;
|
||||
|
||||
} else {
|
||||
// ==================== 背景星星(30%)====================
|
||||
// 随机分布,但避开旋臂区域(相对稀疏)
|
||||
|
||||
const distance = bulgeRadius + Math.random() * (diskRadius - bulgeRadius);
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
|
||||
x = distance * Math.cos(theta);
|
||||
z = distance * Math.sin(theta);
|
||||
y = (Math.random() - 0.5) * 50; // 扁平分布
|
||||
|
||||
// 背景较暗
|
||||
brightness = Math.max(0.2, 0.5 - distance / diskRadius * 0.3);
|
||||
sizes[i] = 0.5 + Math.random() * 1;
|
||||
}
|
||||
|
||||
positions[i * 3] = x;
|
||||
positions[i * 3 + 1] = y;
|
||||
positions[i * 3 + 2] = z;
|
||||
|
||||
// 设置颜色(根据配色方案)
|
||||
const distanceFromCenter = Math.sqrt(x * x + y * y + z * z) / bulgeRadius;
|
||||
const color = getColor(colorScheme, brightness, distanceFromCenter);
|
||||
colors[i * 3] = color[0];
|
||||
colors[i * 3 + 1] = color[1];
|
||||
colors[i * 3 + 2] = color[2];
|
||||
|
||||
// 背景星星用冷色调(除了主银河系)
|
||||
if (rand >= 0.70 && galaxyIndex !== 0) {
|
||||
colors[i * 3] *= 0.5;
|
||||
colors[i * 3 + 1] *= 0.5;
|
||||
colors[i * 3 + 2] *= 0.5;
|
||||
}
|
||||
|
||||
sizes[i] *= brightness;
|
||||
}
|
||||
|
||||
starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
starGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const starMaterial = new THREE.PointsMaterial({
|
||||
size: 2.5,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
sizeAttenuation: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false
|
||||
});
|
||||
|
||||
const starField = new THREE.Points(starGeometry, starMaterial);
|
||||
galaxyGroup.add(starField);
|
||||
|
||||
// 添加银核发光球体
|
||||
addGalacticCore(galaxyGroup, scale);
|
||||
|
||||
// 添加星云效果
|
||||
addNebulaEffect(galaxyGroup, scale);
|
||||
}
|
||||
|
||||
// 添加银核发光球体
|
||||
function addGalacticCore(galaxyGroup, scale = 1) {
|
||||
// 创建一个明亮的发光球体作为银核
|
||||
const coreGeometry = new THREE.SphereGeometry(80, 32, 32);
|
||||
const coreMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xFFF5E1, // 暖白色
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
|
||||
galaxyGroup.add(coreMesh);
|
||||
|
||||
// 添加第二层发光(更亮更小)
|
||||
const innerCoreGeometry = new THREE.SphereGeometry(40, 32, 32);
|
||||
const innerCoreMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xFFFBE6, // 更亮的白色
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const innerCoreMesh = new THREE.Mesh(innerCoreGeometry, innerCoreMaterial);
|
||||
galaxyGroup.add(innerCoreMesh);
|
||||
|
||||
// 添加核心光点(最亮)
|
||||
const coreLightGeometry = new THREE.SphereGeometry(15, 32, 32);
|
||||
const coreLightMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xFFFFF0, // 纯白偏黄
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const coreLightMesh = new THREE.Mesh(coreLightGeometry, coreLightMaterial);
|
||||
galaxyGroup.add(coreLightMesh);
|
||||
}
|
||||
|
||||
// 添加星云效果
|
||||
function addNebulaEffect(galaxyGroup, scale = 1) {
|
||||
const nebulaCount = Math.floor(80 * scale); // 根据缩放调整数量
|
||||
const nebulaGeometry = new THREE.BufferGeometry();
|
||||
const nebulaPositions = new Float32Array(nebulaCount * 3);
|
||||
const nebulaColors = new Float32Array(nebulaCount * 3);
|
||||
const nebulaSizes = new Float32Array(nebulaCount);
|
||||
|
||||
for (let i = 0; i < nebulaCount; i++) {
|
||||
// 主要分布在旋臂区域
|
||||
const armIndex = Math.floor(Math.random() * 4);
|
||||
const armAngle = (armIndex / 4) * Math.PI * 2;
|
||||
const r = 150 + Math.pow(Math.random(), 0.3) * 500;
|
||||
|
||||
const spiralTightness = 0.12;
|
||||
const theta = armAngle + spiralTightness * r;
|
||||
const offset = (Math.random() - 0.5) * 60;
|
||||
|
||||
nebulaPositions[i * 3] = r * Math.cos(theta) + offset * Math.cos(theta + Math.PI / 2);
|
||||
nebulaPositions[i * 3 + 1] = (Math.random() - 0.5) * 20;
|
||||
nebulaPositions[i * 3 + 2] = r * Math.sin(theta) + offset * Math.sin(theta + Math.PI / 2);
|
||||
|
||||
// 暖黄色星云(金黄色,与银河系协调)
|
||||
nebulaColors[i * 3] = 0.9 + Math.random() * 0.1;
|
||||
nebulaColors[i * 3 + 1] = 0.7 + Math.random() * 0.2;
|
||||
nebulaColors[i * 3 + 2] = 0.3 + Math.random() * 0.2;
|
||||
|
||||
nebulaSizes[i] = 15 + Math.random() * 25;
|
||||
}
|
||||
|
||||
nebulaGeometry.setAttribute('position', new THREE.BufferAttribute(nebulaPositions, 3));
|
||||
nebulaGeometry.setAttribute('color', new THREE.BufferAttribute(nebulaColors, 3));
|
||||
|
||||
const nebulaMaterial = new THREE.PointsMaterial({
|
||||
size: 25,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.35, // 提高透明度
|
||||
sizeAttenuation: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false
|
||||
});
|
||||
|
||||
const nebulaField = new THREE.Points(nebulaGeometry, nebulaMaterial);
|
||||
galaxyGroup.add(nebulaField);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindEvents() {
|
||||
// 鼠标按下
|
||||
galaxyCanvas.addEventListener('mousedown', onMouseDown);
|
||||
|
||||
// 鼠标移动
|
||||
galaxyCanvas.addEventListener('mousemove', onMouseMove);
|
||||
|
||||
// 鼠标释放
|
||||
galaxyCanvas.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
// 鼠标离开
|
||||
galaxyCanvas.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// 滚轮缩放
|
||||
galaxyCanvas.addEventListener('wheel', onMouseWheel);
|
||||
|
||||
// 点击星星
|
||||
galaxyCanvas.addEventListener('click', onStarClick);
|
||||
|
||||
// 窗口大小变化
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
}
|
||||
|
||||
// 鼠标按下
|
||||
function onMouseDown(e) {
|
||||
mouse.isDragging = true;
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
}
|
||||
|
||||
// 鼠标移动
|
||||
function onMouseMove(e) {
|
||||
if (mouse.isDragging) {
|
||||
// 计算旋转角度
|
||||
const deltaX = (e.clientX - mouse.x) * 0.005;
|
||||
const deltaY = (e.clientY - mouse.y) * 0.005;
|
||||
|
||||
targetRotation.y += deltaX;
|
||||
targetRotation.x += deltaY;
|
||||
|
||||
// 限制垂直旋转角度
|
||||
targetRotation.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, targetRotation.x));
|
||||
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
}
|
||||
|
||||
// 更新鼠标位置(用于射线检测)
|
||||
mousePosition.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
mousePosition.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
// 检测悬停的星星
|
||||
checkHoveredStar(e);
|
||||
}
|
||||
|
||||
// 鼠标释放
|
||||
function onMouseUp() {
|
||||
mouse.isDragging = false;
|
||||
}
|
||||
|
||||
// 鼠标离开
|
||||
function onMouseLeave() {
|
||||
mouse.isDragging = false;
|
||||
}
|
||||
|
||||
// 鼠标滚轮
|
||||
function onMouseWheel(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 调整相机距离
|
||||
cameraTargetDistance += e.deltaY * 2;
|
||||
cameraTargetDistance = Math.max(300, Math.min(3000, cameraTargetDistance));
|
||||
}
|
||||
|
||||
// 点击星星
|
||||
function onStarClick(e) {
|
||||
// 更新鼠标位置
|
||||
mousePosition.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
mousePosition.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
// 射线检测
|
||||
raycaster.setFromCamera(mousePosition, camera);
|
||||
const intersects = raycaster.intersectObjects(stars);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const clickedStar = intersects[0].object;
|
||||
|
||||
// 检查是否有照片数据
|
||||
if (clickedStar.userData && clickedStar.userData.photoSrc) {
|
||||
openStarLightbox(clickedStar.userData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测悬停的星星
|
||||
function checkHoveredStar(e) {
|
||||
raycaster.setFromCamera(mousePosition, camera);
|
||||
const intersects = raycaster.intersectObjects(stars);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const star = intersects[0].object;
|
||||
|
||||
if (star.userData && star.userData.photoSrc) {
|
||||
hoveredStar = star;
|
||||
galaxyCanvas.style.cursor = 'pointer';
|
||||
|
||||
// 显示提示
|
||||
if (tooltip) {
|
||||
tooltip.querySelector('img').src = star.userData.photoSrc;
|
||||
tooltip.querySelector('p').textContent = `点击放大查看 #${star.userData.photoId}`;
|
||||
tooltip.style.left = (e.clientX + 20) + 'px';
|
||||
tooltip.style.top = (e.clientY - 40) + 'px';
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hoveredStar = null;
|
||||
galaxyCanvas.style.cursor = 'grab';
|
||||
|
||||
// 隐藏提示
|
||||
if (tooltip) {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打开星星放大查看器(使用瀑布模式的lightbox)
|
||||
function openStarLightbox(starData) {
|
||||
// 找到照片在数组中的索引
|
||||
const photoIndex = photoData.findIndex(p => p.id === starData.photoId);
|
||||
if (photoIndex === -1) return;
|
||||
|
||||
// 隐藏tooltip
|
||||
if (tooltip) {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
|
||||
// 调用瀑布模式的openPhotoLightbox函数
|
||||
if (window.openPhotoLightbox) {
|
||||
window.openPhotoLightbox(photoIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小变化
|
||||
function onWindowResize() {
|
||||
if (camera && renderer && galaxyCanvasWrapper) {
|
||||
const containerRect = galaxyCanvasWrapper.getBoundingClientRect();
|
||||
camera.aspect = containerRect.width / containerRect.height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(containerRect.width, containerRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
// 动画循环
|
||||
function animate() {
|
||||
if (!galaxyCanvasWrapper.classList.contains('active')) return;
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
// 平滑旋转(用户拖拽)
|
||||
currentRotation.x += (targetRotation.x - currentRotation.x) * 0.05;
|
||||
currentRotation.y += (targetRotation.y - currentRotation.y) * 0.05;
|
||||
|
||||
// 银河系自旋(非常缓慢的旋转)
|
||||
galaxyGroups.forEach((galaxyGroup, index) => {
|
||||
galaxyRotations[index] += 0.0001 + index * 0.00005; // 不同的旋转速度
|
||||
galaxyGroup.rotation.y = galaxyRotations[index];
|
||||
});
|
||||
|
||||
// 更新相机位置
|
||||
const cameraX = cameraDistance * Math.sin(currentRotation.y) * Math.cos(currentRotation.x);
|
||||
const cameraY = cameraDistance * Math.sin(currentRotation.x);
|
||||
const cameraZ = cameraDistance * Math.cos(currentRotation.y) * Math.cos(currentRotation.x);
|
||||
|
||||
camera.position.set(cameraX, cameraY, cameraZ);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 平滑缩放
|
||||
cameraDistance += (cameraTargetDistance - cameraDistance) * 0.05;
|
||||
|
||||
// 让照片星星面向相机
|
||||
stars.forEach(star => {
|
||||
if (star.userData && star.userData.photoSrc) {
|
||||
star.lookAt(camera.position);
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染场景
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// 清理场景
|
||||
function cleanupScene() {
|
||||
// 停止动画
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
// 移除所有对象
|
||||
while (scene && scene.children.length > 0) {
|
||||
const object = scene.children[0];
|
||||
scene.remove(object);
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (object.material.map) object.material.map.dispose();
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 清空数组
|
||||
stars = [];
|
||||
galaxyGroups = [];
|
||||
galaxyRotations = [];
|
||||
|
||||
// 移除渲染器
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
renderer = null;
|
||||
}
|
||||
|
||||
scene = null;
|
||||
camera = null;
|
||||
}
|
||||
|
||||
// DOM 加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user