- 引入独立的ParticleSystem类管理粒子系统 - 添加土星动画作为默认加载动画 - 实现动态动画切换机制,避免资源冲突 - 优化粒子爆炸与散射效果调用方式 - 移除旧版粒子初始化与物理计算代码 - 更新手势交互与UI状态同步逻辑 - 修复动画模式下按钮显示时机问题
1126 lines
42 KiB
HTML
1126 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>HONESTY | INTELLIGENT ARCHIVE</title>
|
||
<style>
|
||
:root {
|
||
/* 默认变量 */
|
||
--bg-color: #000000;
|
||
--text-color: #FFFFFF;
|
||
--accent-color: #00FFFF;
|
||
--loader-bg: #000000;
|
||
--loader-text: #FFFFFF;
|
||
}
|
||
|
||
/* === 白天模式 (高对比度/清雅) === */
|
||
[data-theme="day"] {
|
||
--bg-color: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
|
||
--text-color: #1a1a2e; /* 极深蓝黑,保证可见度 */
|
||
--accent-color: #3567ff;
|
||
--loader-bg: #f0f2f5;
|
||
--loader-text: #2c3e50;
|
||
--title-gradient: linear-gradient(120deg, #1a1a2e 0%, #2b56ff 50%, #1947ff 100%);
|
||
--veil: radial-gradient(closest-side, rgba(255, 255, 255, 0.0), rgba(255, 255, 255, 0.0) 70%);
|
||
}
|
||
|
||
/* === 黑夜模式 (霓虹/深空) === */
|
||
[data-theme="night"] {
|
||
--bg-color: radial-gradient(circle at 50% 50%, #0f1729 0%, #000000 100%);
|
||
--text-color: #FFFFFF;
|
||
--accent-color: #00FFFF;
|
||
--loader-bg: #000000;
|
||
--loader-text: #FFFFFF;
|
||
--title-gradient: linear-gradient(120deg, #ffffff 0%, #7dfbff 35%, #ff7af3 70%, #ffd36e 100%);
|
||
--veil: radial-gradient(closest-side, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.0) 72%);
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
background: var(--bg-color);
|
||
color: var(--text-color);
|
||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||
transition: background 1s, color 1s;
|
||
user-select: none;
|
||
cursor: default;
|
||
}
|
||
|
||
/* === 1. 智能加载层 === */
|
||
#start-screen {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: var(--loader-bg);
|
||
z-index: 100;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 1s cubic-bezier(0.65, 0, 0.35, 1);
|
||
}
|
||
|
||
.logo-text {
|
||
font-family: 'Times New Roman', serif;
|
||
font-size: 32px;
|
||
letter-spacing: 8px;
|
||
color: var(--loader-text);
|
||
margin-bottom: 30px;
|
||
animation: breathe 3s infinite;
|
||
}
|
||
|
||
/* 加载容器样式 */
|
||
.loading-container {
|
||
text-align: center;
|
||
min-height: 80px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 16px;
|
||
letter-spacing: 2px;
|
||
color: var(--loader-text);
|
||
opacity: 0.9;
|
||
height: 24px;
|
||
margin-bottom: 15px;
|
||
font-weight: 300;
|
||
}
|
||
|
||
/* 加载动画点 */
|
||
.loading-dots {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background-color: var(--accent-color);
|
||
border-radius: 50%;
|
||
margin: 0 4px;
|
||
animation: dot-pulse 1.5s infinite ease-in-out;
|
||
}
|
||
|
||
.dot:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.dot:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
@keyframes dot-pulse {
|
||
0%, 60%, 100% {
|
||
transform: translateY(0);
|
||
opacity: 0.8;
|
||
}
|
||
30% {
|
||
transform: translateY(-8px);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.enter-container {
|
||
text-align: center;
|
||
}
|
||
|
||
.enter-btn:hover {
|
||
background: var(--accent-color);
|
||
color: var(--loader-bg);
|
||
}
|
||
|
||
/* 交互模式按钮 */
|
||
.interaction-mode-btn {
|
||
position: fixed;
|
||
bottom: 40px;
|
||
right: 40px;
|
||
padding: 12px 24px;
|
||
background: var(--accent-color);
|
||
color: var(--loader-bg);
|
||
border: none;
|
||
border-radius: 30px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
letter-spacing: 1px;
|
||
box-shadow: 0 4px 12px rgba(0, 255, 255, 0.3);
|
||
animation: pulse 2s infinite;
|
||
z-index: 30;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.interaction-mode-btn.visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.interaction-mode-btn:hover {
|
||
box-shadow: 0 6px 16px rgba(0, 255, 255, 0.5);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(0, 255, 255, 0.4);
|
||
}
|
||
70% {
|
||
box-shadow: 0 0 0 10px rgba(0, 255, 255, 0);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(0, 255, 255, 0);
|
||
}
|
||
}
|
||
|
||
/* 黑夜模式下的特殊样式 */
|
||
[data-theme="night"] .interaction-mode-btn {
|
||
background: var(--accent-color);
|
||
color: #000;
|
||
box-shadow: 0 4px 12px rgba(0, 255, 255, 0.4);
|
||
}
|
||
|
||
[data-theme="night"] .interaction-mode-btn:hover {
|
||
box-shadow: 0 6px 16px rgba(0, 255, 255, 0.6);
|
||
}
|
||
|
||
/* 白天模式下的特殊样式 */
|
||
[data-theme="day"] .interaction-mode-btn {
|
||
background: var(--accent-color);
|
||
color: #fff;
|
||
box-shadow: 0 4px 12px rgba(53, 103, 255, 0.4);
|
||
}
|
||
|
||
[data-theme="day"] .interaction-mode-btn:hover {
|
||
box-shadow: 0 6px 16px rgba(53, 103, 255, 0.6);
|
||
}
|
||
|
||
/* === 2. 叙事文本层 (DOM覆盖) === */
|
||
#narrative-layer {
|
||
position: absolute;
|
||
top: 28%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
width: 72%;
|
||
max-width: 1000px;
|
||
pointer-events: none;
|
||
z-index: 5;
|
||
padding: 16px 24px;
|
||
}
|
||
|
||
#narrative-layer::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -4% 10% 0 10%;
|
||
transform: translateZ(0);
|
||
background: var(--veil);
|
||
filter: blur(20px) saturate(1.1);
|
||
pointer-events: none;
|
||
z-index: -1;
|
||
}
|
||
|
||
.n-title {
|
||
font-size: clamp(42px, 8vw, 128px);
|
||
font-weight: 800;
|
||
letter-spacing: 0;
|
||
margin-bottom: 14px;
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
transition: all 0.8s cubic-bezier(0.2, 1, 0.3, 1);
|
||
background: var(--title-gradient);
|
||
background-size: 200% 200%;
|
||
animation: gradientShift 9s ease-in-out infinite;
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
text-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
[data-theme="day"] .n-title {
|
||
-webkit-text-stroke: 0.6px rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
[data-theme="night"] .n-title {
|
||
-webkit-text-stroke: 0.6px rgba(0, 0, 0, 0.20);
|
||
}
|
||
|
||
.n-sub {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
letter-spacing: 6px;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 0.8s 0.2s cubic-bezier(0.2, 1, 0.3, 1);
|
||
color: var(--text-color);
|
||
text-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
/* 激活状态类名 */
|
||
.show-text .n-title, .show-text .n-sub {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* === 3. UI HUD === */
|
||
#ui-layer {
|
||
position: fixed;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 20;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding: 40px;
|
||
opacity: 0;
|
||
transition: opacity 1.5s ease;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 10px;
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
color: var(--text-color);
|
||
opacity: 0.7;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.center-stage {
|
||
position: absolute;
|
||
top: 78%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
width: 100%;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.main-hint {
|
||
font-size: 20px;
|
||
font-weight: 300;
|
||
letter-spacing: 8px;
|
||
cursor: pointer;
|
||
padding: 20px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.main-hint:hover {
|
||
letter-spacing: 10px;
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.sub-hint {
|
||
font-size: 10px;
|
||
opacity: 0.6;
|
||
margin-top: 15px;
|
||
letter-spacing: 3px;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.exit-btn {
|
||
margin-top: 40px;
|
||
display: inline-block;
|
||
font-size: 10px;
|
||
letter-spacing: 2px;
|
||
padding: 10px 24px;
|
||
border: 1px solid var(--text-color);
|
||
border-radius: 30px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 0.5s;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.exit-btn.visible {
|
||
opacity: 0.8;
|
||
transform: translateY(0);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.exit-btn:hover {
|
||
background: var(--text-color);
|
||
color: var(--bg-color);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 已移除动画切换按钮样式,避免同时加载多个动画资源 */
|
||
|
||
@keyframes breathe {
|
||
0%, 100% {
|
||
opacity: 0.6
|
||
}
|
||
50% {
|
||
opacity: 1
|
||
}
|
||
}
|
||
|
||
@keyframes gradientShift {
|
||
0% {
|
||
background-position: 0% 50%;
|
||
}
|
||
50% {
|
||
background-position: 100% 50%;
|
||
}
|
||
100% {
|
||
background-position: 0% 50%;
|
||
}
|
||
}
|
||
|
||
#canvas-container {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
#aurora-layer {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#aurora-layer::before, #aurora-layer::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -20% -10%;
|
||
filter: blur(60px);
|
||
opacity: 0.25;
|
||
}
|
||
|
||
@keyframes auroraDrift {
|
||
0% {
|
||
transform: translate3d(-4%, -2%, 0) scale(1);
|
||
}
|
||
50% {
|
||
transform: translate3d(4%, 2%, 0) scale(1.05);
|
||
}
|
||
100% {
|
||
transform: translate3d(-4%, 0, 0) scale(1);
|
||
}
|
||
}
|
||
|
||
[data-theme="day"] #aurora-layer::before {
|
||
background: radial-gradient(60% 40% at 25% 30%, rgba(255, 180, 200, 0.35), transparent 60%);
|
||
animation: auroraDrift 22s ease-in-out infinite;
|
||
}
|
||
|
||
[data-theme="day"] #aurora-layer::after {
|
||
background: radial-gradient(60% 40% at 70% 60%, rgba(110, 195, 255, 0.35), transparent 60%);
|
||
animation: auroraDrift 28s ease-in-out infinite reverse;
|
||
}
|
||
|
||
[data-theme="night"] #aurora-layer::before {
|
||
background: radial-gradient(70% 45% at 20% 30%, rgba(0, 255, 255, 0.28), transparent 62%);
|
||
animation: auroraDrift 24s ease-in-out infinite;
|
||
}
|
||
|
||
[data-theme="night"] #aurora-layer::after {
|
||
background: radial-gradient(70% 45% at 75% 65%, rgba(255, 122, 243, 0.24), transparent 62%);
|
||
animation: auroraDrift 32s ease-in-out infinite reverse;
|
||
}
|
||
|
||
</style>
|
||
|
||
<!-- 依赖库 -->
|
||
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"
|
||
crossorigin="anonymous"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
|
||
<!-- 添加动画类库 -->
|
||
<script src="js/ParticleSystem.js"></script>
|
||
|
||
</head>
|
||
<body>
|
||
|
||
<!-- 1. 智能加载层 -->
|
||
<div id="start-screen">
|
||
<div class="logo-text">Honesty</div>
|
||
<div class="loading-container">
|
||
<div class="status-text" id="loader-msg">正在唤醒灵感...</div>
|
||
<div class="loading-dots">
|
||
<span class="dot"></span>
|
||
<span class="dot"></span>
|
||
<span class="dot"></span>
|
||
</div>
|
||
</div>
|
||
<div class="enter-container" id="enter-container" style="margin-top: 30px; text-align: center; opacity: 0; transition: opacity 1s;">
|
||
<div class="countdown-hint" id="countdown-hint" style="font-size: 14px; margin-bottom: 15px; color: var(--loader-text);">5秒后自动进入</div>
|
||
<button class="enter-btn" id="enter-btn" style="padding: 12px 24px; background: transparent; border: 1px solid var(--accent-color); color: var(--loader-text); border-radius: 30px; cursor: pointer; font-size: 14px; letter-spacing: 2px;">立即进入</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2. 叙事文本层 (DOM) -->
|
||
<div id="narrative-layer">
|
||
<div class="n-title" id="n-title"></div>
|
||
<div class="n-sub" id="n-sub"></div>
|
||
</div>
|
||
|
||
<!-- 3. UI HUD -->
|
||
<div id="ui-layer">
|
||
<div class="header">
|
||
<span id="sys-status">NEURAL LINK: STANDBY</span>
|
||
<span id="theme-display">THEME: AUTO</span>
|
||
</div>
|
||
|
||
<div class="center-stage">
|
||
<div class="main-hint" id="main-hint" onclick="enterArchive()"></div>
|
||
<div class="sub-hint" id="sub-hint" onclick="requestCameraAccess()"></div>
|
||
<div class="exit-btn" id="exit-btn" onclick="exitArchive()">[ 退出动画 ]</div>
|
||
</div>
|
||
|
||
<div class="header" style="align-self: flex-end;">
|
||
<span id="lang-display">中文</span>
|
||
</div>
|
||
</div>
|
||
|
||
<video id="input-video" style="display:none"></video>
|
||
<div id="canvas-container"></div>
|
||
<div id="aurora-layer"></div>
|
||
<script src="js/config.js?version=20251125"></script>
|
||
<script>
|
||
/**
|
||
* ============================================================================
|
||
* 1. 安全工具与状态
|
||
* ============================================================================
|
||
*/
|
||
const APP_STATE = {
|
||
isLoaded: false,
|
||
mode: 'LOCKED', // LOCKED, UNLOCKED
|
||
handCount: 0,
|
||
handL: new THREE.Vector3(9999, 9999, 0),
|
||
handR: new THREE.Vector3(9999, 9999, 0),
|
||
unlockProgress: 0,
|
||
exitCooldownUntil: 0
|
||
};
|
||
|
||
APP_STATE.namasteStableFrames = 0;
|
||
|
||
// 缓存频繁访问的DOM元素以提高性能
|
||
const DOM_CACHE = {
|
||
// 加载屏幕相关元素
|
||
startScreen: document.getElementById('start-screen'),
|
||
loaderMsg: document.getElementById('loader-msg'),
|
||
enterContainer: document.getElementById('enter-container'),
|
||
countdownHint: document.getElementById('countdown-hint'),
|
||
enterBtn: document.getElementById('enter-btn'),
|
||
|
||
// 叙事层相关元素
|
||
narrativeLayer: document.getElementById('narrative-layer'),
|
||
nTitle: document.getElementById('n-title'),
|
||
nSub: document.getElementById('n-sub'),
|
||
|
||
// UI层相关元素
|
||
uiLayer: document.getElementById('ui-layer'),
|
||
sysStatus: document.getElementById('sys-status'),
|
||
themeDisplay: document.getElementById('theme-display'),
|
||
langDisplay: document.getElementById('lang-display'),
|
||
mainHint: document.getElementById('main-hint'),
|
||
subHint: document.getElementById('sub-hint'),
|
||
exitBtn: document.getElementById('exit-btn'),
|
||
|
||
// 交互模式按钮
|
||
interactionModeBtn: null,
|
||
|
||
// 其他元素
|
||
canvasContainer: document.getElementById('canvas-container'),
|
||
inputVideo: document.getElementById('input-video')
|
||
};
|
||
|
||
// 安全DOM操作,防止报错
|
||
function safeUpdateText(element, text) {
|
||
if (element) element.innerText = text;
|
||
}
|
||
|
||
function safeClass(element, method, className) {
|
||
if (element) element.classList[method](className);
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 2. 环境引擎 (主题 & 语言)
|
||
* ============================================================================
|
||
*/
|
||
function getStoredTheme() {
|
||
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme';
|
||
const cacheJson = localStorage.getItem(cacheKey);
|
||
const saved = cacheJson ? JSON.parse(cacheJson) : null;
|
||
|
||
let theme = 'day';
|
||
const hour = new Date().getHours();
|
||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
|
||
// 缓存过期(1小时)或无缓存时重新判断
|
||
if (!saved || (Date.now() - (saved.time || 0) > 3600000)) {
|
||
if (hour >= 18 || hour < 6 || prefersDark) theme = 'night';
|
||
} else {
|
||
theme = saved.value;
|
||
}
|
||
return theme;
|
||
}
|
||
|
||
function getStoredLanguage() {
|
||
return localStorage.getItem('lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
||
}
|
||
|
||
const ENV = {
|
||
theme: getStoredTheme(),
|
||
lang: getStoredLanguage()
|
||
};
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 3. 内容字典
|
||
* ============================================================================
|
||
*/
|
||
const DICTIONARY = {
|
||
zh: {
|
||
load: [
|
||
"系统正在学会做梦...", "唤醒沉睡的灵感...", "接入神经元网络...", "解析意识微光...", "在0与1间种诗...", "等待灵感穿越视界...", "系统准备就绪."
|
||
],
|
||
hints: {
|
||
main: "双手合十 · 解锁档案",
|
||
sub: "单手·引力牵引|双手·力场排斥",
|
||
unlocking: "正在识别...",
|
||
countdownHint: "{second}秒后自动进入",
|
||
enterBtn: "立即进入",
|
||
interactionModeBtn: "交互模式",
|
||
cameraAccessInfo: "点击启用交互模式"
|
||
},
|
||
slides: [
|
||
{t: "初心", s: "在这喧嚣世界中,依然相信纯粹的力量"},
|
||
{t: "思考", s: "喜欢在安静的夜晚与自己对话"},
|
||
{t: "成长", s: "每一步脚印都刻着时光的温度"},
|
||
{t: "梦想", s: "用代码编织心中的理想国"},
|
||
{t: "独处", s: "享受一个人的深度时光"},
|
||
{t: "人格", s: "INFJ —— 在人群之外,静静观察世界"}
|
||
]
|
||
},
|
||
en: {
|
||
load: [
|
||
"The system is learning to dream...", "Awakening dormant inspiration...", "Connecting to neural networks...", "Decoding the glow of consciousness...", "Planting poetry between 0s and 1s...", "Waiting for inspiration to cross the horizon...", "System ready."
|
||
],
|
||
hints: {
|
||
main: "Click or Namaste",
|
||
sub: "One Hand Drag · Two Hands Repel",
|
||
unlocking: "Identifying...",
|
||
countdownHint: "Auto enter in {second} seconds",
|
||
enterBtn: "Enter Now",
|
||
interactionModeBtn: "Interaction Mode",
|
||
cameraAccessInfo: "Click to enable camera interaction"
|
||
},
|
||
slides: [
|
||
{t: "Honesty", s: "In a noisy world, still believe in the power of simplicity"},
|
||
{t: "Contemplation", s: "Enjoy late night conversations with myself"},
|
||
{t: "Growth", s: "Every step carries the warmth of time"},
|
||
{t: "Dreams", s: "Weaving an ideal realm with code"},
|
||
{t: "Solitude", s: "Cherishing deep moments of alone time"},
|
||
{t: "Personality", s: "INFJ // Observing the world from the sidelines"}
|
||
]
|
||
}
|
||
};
|
||
|
||
const CONTENT = DICTIONARY[ENV.lang];
|
||
|
||
// 设置进入按钮的文本
|
||
safeUpdateText(DOM_CACHE.countdownHint, CONTENT.hints.countdownHint.replace('{second}', '5'));
|
||
safeUpdateText(DOM_CACHE.enterBtn, CONTENT.hints.enterBtn);
|
||
|
||
// 摄像头权限状态管理
|
||
const CAMERA_STATE = {
|
||
enabled: false, // 摄像头是否已启用
|
||
permissionDenied: false, // 用户是否拒绝了权限
|
||
autoEnterTimeout: null, // 自动进入计时器
|
||
countdownInterval: null // 倒计时计时器
|
||
};
|
||
|
||
// 应用主题
|
||
document.documentElement.setAttribute('data-theme', ENV.theme);
|
||
safeUpdateText(DOM_CACHE.themeDisplay, `THEME: ${ENV.lang.toUpperCase() === 'en' ? ENV.theme.toUpperCase() : ENV.theme === 'day' ? '白天' : '黑夜'}`);
|
||
safeUpdateText(DOM_CACHE.langDisplay, ENV.lang.toUpperCase() === 'ZH' ? '中文' : 'English');
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 4. THREE.JS 渲染引擎
|
||
* ============================================================================
|
||
*/
|
||
const CONFIG = {
|
||
particleCount: 22000,
|
||
camZ: 600,
|
||
// 白天用实心(Normal),黑夜用发光(Additive)
|
||
blending: ENV.theme === 'day' ? THREE.NormalBlending : THREE.AdditiveBlending,
|
||
colors: ENV.theme === 'day'
|
||
? {base: new THREE.Color(0x2c3e50), active: new THREE.Color(0x0055ff)} // 白天:深蓝灰 -> 亮蓝
|
||
: {base: new THREE.Color(0xFFFFFF), active: new THREE.Color(0x00FFFF)}, // 黑夜:白 -> 青
|
||
bloom: ENV.theme === 'day' ? 0.0 : 1.5 // 白天无辉光,确保清晰
|
||
};
|
||
|
||
const scene = new THREE.Scene();
|
||
// 雾效与背景色同步
|
||
const fogColor = ENV.theme === 'day' ? 0xe0eafc : 0x000000;
|
||
scene.fog = new THREE.FogExp2(fogColor, 0.0015);
|
||
|
||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 4000);
|
||
camera.position.z = CONFIG.camZ;
|
||
|
||
const renderer = new THREE.WebGLRenderer({powerPreference: "high-performance", antialias: true, alpha: true});
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
renderer.setClearColor(0x000000, 0);
|
||
DOM_CACHE.canvasContainer.appendChild(renderer.domElement);
|
||
|
||
const composer = new THREE.EffectComposer(renderer);
|
||
composer.addPass(new THREE.RenderPass(scene, camera));
|
||
|
||
if (CONFIG.bloom > 0) {
|
||
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
|
||
bloomPass.strength = CONFIG.bloom;
|
||
bloomPass.radius = 0.5;
|
||
bloomPass.threshold = 0.1;
|
||
composer.addPass(bloomPass);
|
||
}
|
||
|
||
// 初始化粒子系统
|
||
const particleSystem = new ParticleSystem({
|
||
particleCount: CONFIG.particleCount,
|
||
theme: ENV.theme,
|
||
callbacks: {
|
||
onInit: (system) => {
|
||
scene.add(system);
|
||
},
|
||
onUpdate: (system) => {
|
||
// 粒子系统更新后的回调
|
||
},
|
||
onAddObject: (object) => {
|
||
scene.add(object);
|
||
},
|
||
onRemoveObject: (object) => {
|
||
scene.remove(object);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 根据条件动态加载动画
|
||
function loadAnimation(animationType) {
|
||
// 移除当前所有动画
|
||
while (particleSystem.animations.length > 0) {
|
||
particleSystem.removeAnimation(particleSystem.animations[0]);
|
||
}
|
||
|
||
// 根据类型动态加载动画
|
||
switch (animationType) {
|
||
case 'saturn':
|
||
// 土星动画作为默认动画
|
||
const saturnAnimation = new SaturnAnimation(particleSystem);
|
||
particleSystem.addAnimation(saturnAnimation);
|
||
break;
|
||
case 'particles':
|
||
default:
|
||
// 基本粒子动画不需要额外对象
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 手势光标
|
||
const cursorMat = new THREE.MeshBasicMaterial({
|
||
color: ENV.theme === 'day' ? 0x0044cc : 0x00FF00,
|
||
side: THREE.DoubleSide, transparent: true, opacity: 0.6
|
||
});
|
||
const cursorL = new THREE.Mesh(new THREE.RingGeometry(8, 10, 32), cursorMat);
|
||
const cursorR = new THREE.Mesh(new THREE.RingGeometry(8, 10, 32), cursorMat);
|
||
scene.add(cursorL);
|
||
scene.add(cursorR);
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 5. 物理引擎循环
|
||
* ============================================================================
|
||
*/
|
||
const clock = new THREE.Clock();
|
||
let targetMix = 0;
|
||
|
||
function animate() {
|
||
const time = clock.getElapsedTime();
|
||
const delta = Math.min(clock.getDelta(), 0.1);
|
||
|
||
// 更新粒子系统
|
||
const targetMix = particleSystem.update(time, APP_STATE.mode, APP_STATE.handCount, APP_STATE.handL, APP_STATE.handR);
|
||
|
||
cursorL.position.set(APP_STATE.handL.x, APP_STATE.handL.y, 0);
|
||
cursorR.position.set(APP_STATE.handR.x, APP_STATE.handR.y, 0);
|
||
cursorL.visible = (APP_STATE.handCount >= 1);
|
||
cursorR.visible = (APP_STATE.handCount === 2);
|
||
|
||
composer.render();
|
||
requestAnimationFrame(animate);
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 6. 逻辑控制 (动画/退出)
|
||
* ============================================================================
|
||
*/
|
||
let narrativeTimer = null;
|
||
|
||
// 进入动画模式
|
||
function enterAnimationMode() {
|
||
APP_STATE.isLoaded = true;
|
||
|
||
// 隐藏加载屏幕
|
||
const loader = DOM_CACHE.startScreen;
|
||
loader.style.opacity = 0;
|
||
DOM_CACHE.uiLayer.style.opacity = 1;
|
||
setTimeout(() => {
|
||
if (loader && loader.parentNode) {
|
||
loader.style.display = 'none';
|
||
}
|
||
}, 1000);
|
||
|
||
// 默认使用土星动画
|
||
setTimeout(() => {
|
||
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
|
||
loadAnimation('saturn');
|
||
}
|
||
}, 1500);
|
||
|
||
// 如果摄像头已启用,更新UI以反映交互模式
|
||
if (CAMERA_STATE.enabled) {
|
||
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.sub);
|
||
} else {
|
||
// 显示交互模式按钮(如果摄像头未启用)
|
||
setTimeout(() => {
|
||
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
|
||
showInteractionModeButton();
|
||
}
|
||
}, 1500);
|
||
}
|
||
}
|
||
|
||
// 显示交互模式按钮
|
||
function showInteractionModeButton() {
|
||
// 创建交互模式按钮
|
||
const btn = document.createElement('button');
|
||
btn.className = 'interaction-mode-btn';
|
||
btn.id = 'interaction-mode-btn';
|
||
btn.textContent = CONTENT.hints.interactionModeBtn;
|
||
document.body.appendChild(btn);
|
||
|
||
// 缓存按钮引用
|
||
DOM_CACHE.interactionModeBtn = btn;
|
||
|
||
// 显示按钮
|
||
setTimeout(() => {
|
||
btn.classList.add('visible');
|
||
}, 100);
|
||
|
||
// 添加点击事件
|
||
btn.addEventListener('click', () => {
|
||
requestCameraAccess();
|
||
});
|
||
}
|
||
|
||
// 请求摄像头权限
|
||
function requestCameraAccess() {
|
||
if (CAMERA_STATE.enabled) {
|
||
return;
|
||
}
|
||
// 隐藏交互模式按钮
|
||
if (DOM_CACHE.interactionModeBtn) {
|
||
DOM_CACHE.interactionModeBtn.classList.remove('visible');
|
||
}
|
||
|
||
// 更新提示文本
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.cameraAccessInfo);
|
||
|
||
// 尝试启动摄像头
|
||
cameraUtils.start().then(() => {
|
||
// 成功启动,更新状态
|
||
CAMERA_STATE.enabled = true;
|
||
CAMERA_STATE.permissionDenied = false;
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.sub);
|
||
setTimeout(() => {
|
||
if (DOM_CACHE.interactionModeBtn && DOM_CACHE.interactionModeBtn.parentNode) {
|
||
DOM_CACHE.interactionModeBtn.parentNode.removeChild(DOM_CACHE.interactionModeBtn);
|
||
DOM_CACHE.interactionModeBtn = null;
|
||
}
|
||
}, 500);
|
||
}).catch(err => {
|
||
// 权限被拒绝
|
||
console.log("Camera access error:", err);
|
||
CAMERA_STATE.permissionDenied = true;
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.cameraAccessInfo);
|
||
DOM_CACHE.interactionModeBtn.classList.add('visible');
|
||
});
|
||
}
|
||
|
||
window.enterArchive = function () {
|
||
if (APP_STATE.mode === 'UNLOCKED') return;
|
||
APP_STATE.mode = 'UNLOCKED';
|
||
|
||
// UI
|
||
safeClass(DOM_CACHE.mainHint, 'add', 'hidden');
|
||
DOM_CACHE.mainHint.style.display = 'none';
|
||
DOM_CACHE.subHint.style.display = 'none';
|
||
safeClass(DOM_CACHE.exitBtn, 'add', 'visible');
|
||
|
||
// 粒子爆炸效果
|
||
particleSystem.explode(300);
|
||
|
||
startNarrative();
|
||
}
|
||
|
||
window.exitArchive = function () {
|
||
APP_STATE.mode = 'LOCKED';
|
||
APP_STATE.exitCooldownUntil = Date.now() + 2000;
|
||
APP_STATE.namasteStableFrames = 0; // 清空手势识别进度
|
||
|
||
DOM_CACHE.mainHint.style.display = 'block';
|
||
DOM_CACHE.subHint.style.display = 'block';
|
||
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||
if (CAMERA_STATE.enabled) {
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.sub);
|
||
} else {
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.cameraAccessInfo);
|
||
}
|
||
|
||
|
||
safeClass(DOM_CACHE.exitBtn, 'remove', 'visible');
|
||
safeClass(DOM_CACHE.narrativeLayer, 'remove', 'show-text');
|
||
|
||
APP_STATE.unlockProgress = 0;
|
||
clearTimeout(narrativeTimer);
|
||
particleSystem.explode(50);
|
||
|
||
// 退出后重新显示交互模式按钮(如果之前已启用摄像头)
|
||
if (CAMERA_STATE.enabled) {
|
||
showInteractionModeButton();
|
||
}
|
||
}
|
||
|
||
function startNarrative() {
|
||
let idx = 0;
|
||
const nTitle = DOM_CACHE.nTitle;
|
||
const nSub = DOM_CACHE.nSub;
|
||
const layer = DOM_CACHE.narrativeLayer;
|
||
|
||
// 必须检查元素是否存在
|
||
if (!nTitle || !nSub || !layer) return;
|
||
|
||
const next = () => {
|
||
if (APP_STATE.mode !== 'UNLOCKED') return;
|
||
|
||
const slide = CONTENT.slides[idx % CONTENT.slides.length];
|
||
|
||
nTitle.innerText = slide.t;
|
||
nSub.innerText = slide.s;
|
||
layer.classList.add('show-text');
|
||
|
||
// 粒子散开做背景
|
||
particleSystem.scatter();
|
||
|
||
narrativeTimer = setTimeout(() => {
|
||
layer.classList.remove('show-text');
|
||
particleSystem.explode(10);
|
||
narrativeTimer = setTimeout(next, 1000);
|
||
}, 3000);
|
||
|
||
idx++;
|
||
};
|
||
next();
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 7. MediaPipe 手势
|
||
* ============================================================================
|
||
*/
|
||
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
|
||
hands.setOptions({maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.85, minTrackingConfidence: 0.85});
|
||
|
||
hands.onResults(results => {
|
||
// Loader logic
|
||
if (!APP_STATE.isLoaded) {
|
||
APP_STATE.isLoaded = true;
|
||
CAMERA_STATE.enabled = true;
|
||
}
|
||
|
||
// 只有在摄像头启用时才处理手势
|
||
if (CAMERA_STATE.enabled) {
|
||
const landmarks = results.multiHandLandmarks;
|
||
const sysStatus = DOM_CACHE.sysStatus;
|
||
|
||
if (landmarks && landmarks.length > 0) {
|
||
APP_STATE.handCount = landmarks.length;
|
||
safeUpdateText(sysStatus, `LINKED (${APP_STATE.handCount})`);
|
||
if (sysStatus) sysStatus.style.color = ENV.theme === 'day' ? '#0044cc' : '#00ff00';
|
||
|
||
// 坐标处理 (镜像)
|
||
const process = (lm) => ({
|
||
x: ((1.0 - lm.x) * 2 - 1) * 800,
|
||
y: -(lm.y * 2 - 1 - 0.2) * 600
|
||
});
|
||
|
||
const p1 = process(landmarks[0][9]);
|
||
APP_STATE.handL.x += (p1.x - APP_STATE.handL.x) * 0.25;
|
||
APP_STATE.handL.y += (p1.y - APP_STATE.handL.y) * 0.25;
|
||
|
||
if (landmarks.length > 1) {
|
||
const p2 = process(landmarks[1][9]);
|
||
APP_STATE.handR.x += (p2.x - APP_STATE.handR.x) * 0.25;
|
||
APP_STATE.handR.y += (p2.y - APP_STATE.handR.y) * 0.25;
|
||
}
|
||
|
||
// 合十检测
|
||
if (landmarks.length === 2 && APP_STATE.mode === 'LOCKED' && Date.now() > APP_STATE.exitCooldownUntil) {
|
||
const w1 = landmarks[0][0], w2 = landmarks[1][0];
|
||
const i1 = landmarks[0][8], i2 = landmarks[1][8];
|
||
const m1 = landmarks[0][12], m2 = landmarks[1][12];
|
||
const dW = Math.hypot(w1.x - w2.x, w1.y - w2.y);
|
||
const dI = Math.hypot(i1.x - i2.x, i1.y - i2.y);
|
||
const dM = Math.hypot(m1.x - m2.x, m1.y - m2.y);
|
||
const okW = dW < 0.28;
|
||
const okI = dI < 0.20;
|
||
const okM = dM < 0.20;
|
||
const ok = ((okW ? 1 : 0) + (okI ? 1 : 0) + (okM ? 1 : 0)) >= 2;
|
||
if (ok) {
|
||
APP_STATE.namasteStableFrames++;
|
||
} else {
|
||
APP_STATE.namasteStableFrames = Math.max(0, APP_STATE.namasteStableFrames - 1);
|
||
}
|
||
const targetFrames = 20;
|
||
const pct = Math.min(100, Math.round(APP_STATE.namasteStableFrames / targetFrames * 100));
|
||
if (pct > 0) safeUpdateText(DOM_CACHE.mainHint, `${CONTENT.hints.unlocking} ${pct}%`);
|
||
if (APP_STATE.namasteStableFrames >= targetFrames) enterArchive();
|
||
if (!ok && APP_STATE.namasteStableFrames === 0) safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||
}
|
||
} else {
|
||
APP_STATE.handCount = 0;
|
||
safeUpdateText(sysStatus, 'SEARCHING...');
|
||
if (sysStatus) sysStatus.style.color = 'inherit';
|
||
APP_STATE.unlockProgress = 0;
|
||
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||
}
|
||
}
|
||
});
|
||
|
||
const videoElement = DOM_CACHE.inputVideo;
|
||
const cameraUtils = new Camera(videoElement, {
|
||
onFrame: async () => {
|
||
try {
|
||
await hands.send({image: videoElement});
|
||
} catch (err) {
|
||
// 捕获并隐藏技术性错误信息
|
||
if (err.name !== 'NotAllowedError' && err.message.indexOf('Permission dismissed') === -1) {
|
||
console.log("Camera processing error:", err);
|
||
}
|
||
}
|
||
},
|
||
width: 640, height: 480
|
||
});
|
||
|
||
// 检查摄像头权限状态(非阻塞方式)
|
||
function checkCameraPermissionAndEnable() {
|
||
// 使用setTimeout确保不阻塞主流程
|
||
setTimeout(async () => {
|
||
try {
|
||
// 检查是否支持 permissions API
|
||
if (navigator.permissions) {
|
||
const permission = await navigator.permissions.query({name: 'camera'});
|
||
if (permission.state === 'granted') {
|
||
// 如果已授权,则启用摄像头
|
||
try {
|
||
await cameraUtils.start();
|
||
CAMERA_STATE.enabled = true;
|
||
CAMERA_STATE.permissionDenied = false;
|
||
console.log("Camera auto-enabled due to existing permission");
|
||
} catch (err) {
|
||
console.log("Failed to auto-start camera:", err);
|
||
CAMERA_STATE.enabled = false;
|
||
CAMERA_STATE.permissionDenied = true;
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.log("Unable to check camera permission:", err);
|
||
}
|
||
}, 0); // 尽快执行,但仍是异步的
|
||
}
|
||
|
||
// 页面加载完成后检查摄像头权限状态
|
||
window.addEventListener('load', () => {
|
||
console.log("Page loaded, checking camera permission status");
|
||
checkCameraPermissionAndEnable();
|
||
|
||
// 无论摄像头权限如何,3.5秒后确保页面加载状态完成
|
||
setTimeout(() => {
|
||
if (!APP_STATE.isLoaded) {
|
||
APP_STATE.isLoaded = true;
|
||
console.log("Forced page load completion");
|
||
}
|
||
}, 3500);
|
||
});
|
||
|
||
window.addEventListener('resize', () => {
|
||
camera.aspect = window.innerWidth / window.innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
composer.setSize(window.innerWidth, window.innerHeight);
|
||
});
|
||
|
||
// 循环播放加载文案
|
||
let loadIdx = 0;
|
||
const loadTimer = setInterval(() => {
|
||
if (APP_STATE.isLoaded) {
|
||
clearInterval(loadTimer);
|
||
return;
|
||
}
|
||
safeUpdateText(DOM_CACHE.loaderMsg, CONTENT.load[loadIdx % CONTENT.load.length]);
|
||
loadIdx++;
|
||
}, 800);
|
||
|
||
// 资源加载完成后显示进入按钮
|
||
setTimeout(() => {
|
||
// 显示进入选项
|
||
DOM_CACHE.enterContainer.style.opacity = '1';
|
||
|
||
// 开始5秒倒计时
|
||
let countdown = 5;
|
||
safeUpdateText(DOM_CACHE.countdownHint, CONTENT.hints.countdownHint.replace('{second}', countdown + ''));
|
||
|
||
CAMERA_STATE.countdownInterval = setInterval(() => {
|
||
countdown--;
|
||
if (countdown > 0) {
|
||
safeUpdateText(DOM_CACHE.countdownHint, CONTENT.hints.countdownHint.replace('{second}', countdown + ''));
|
||
} else {
|
||
clearInterval(CAMERA_STATE.countdownInterval);
|
||
enterAnimationMode();
|
||
}
|
||
}, 1000);
|
||
}, 3000);
|
||
|
||
// 进入按钮事件处理
|
||
DOM_CACHE.enterBtn.addEventListener('click', () => {
|
||
clearInterval(CAMERA_STATE.countdownInterval);
|
||
enterAnimationMode();
|
||
});
|
||
|
||
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||
safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.sub);
|
||
animate();
|
||
|
||
</script>
|
||
</body>
|
||
</html> |