Files
home/me.html
hehh c21d276f40 feat(ui): 重构加载与叙事层,优化手势交互体验
- 重构智能加载层结构与样式,提升初始化体验
- 优化白天/黑夜主题配色与粒子渲染效果
- 增强手势识别逻辑与视觉反馈
- 改进UI层DOM结构与类名语义化
- 更新内容字典与加载文案
- 修复安全DOM操作与空指针问题
- 调整物理引擎参数,增强交互手感
- 优化粒子爆炸与散开动画效果
- 统一状态管理对象,提高代码可维护性
- 增加权限提示与超时处理机制
2025-12-04 16:02:55 +08:00

700 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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: #0044cc;
--loader-bg: #f0f2f5;
--loader-text: #2c3e50;
}
/* === 黑夜模式 (霓虹/深空) === */
[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;
}
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: 20px; animation: breathe 3s infinite;
}
.loader-ring {
width: 50px; height: 50px; border: 2px solid rgba(128,128,128,0.2);
border-top-color: var(--accent-color); border-radius: 50%;
animation: spin 1s linear infinite; margin-bottom: 20px;
}
.status-text {
font-size: 14px; letter-spacing: 2px; color: var(--loader-text);
opacity: 0.8; height: 20px;
}
.perm-hint {
margin-top: 30px; padding: 10px 20px; border: 1px solid var(--accent-color);
border-radius: 20px; font-size: 12px; color: var(--loader-text);
opacity: 0; transform: translateY(10px); transition: all 1s;
}
.perm-hint.show { opacity: 1; transform: translateY(0); }
/* === 2. 叙事文本层 (DOM覆盖) === */
#narrative-layer {
position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);
text-align: center; width: 90%; pointer-events: none; z-index: 5;
}
.n-title {
font-size: 8vw; font-weight: 900; letter-spacing: -2px; margin-bottom: 10px;
opacity: 0; transform: translateY(30px); transition: all 0.8s cubic-bezier(0.2, 1, 0.3, 1);
background: linear-gradient(45deg, var(--text-color), var(--accent-color));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.n-sub {
font-size: 18px; font-weight: 400; 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);
}
/* 激活状态类名 */
.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: 50%; 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 spin { to { transform: rotate(360deg); } }
</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>
</head>
<body>
<!-- 1. 智能加载层 -->
<div id="start-screen">
<div class="logo-text">HONESTY</div>
<div class="loader-ring"></div>
<div class="status-text" id="loader-msg">Initializing System...</div>
<div class="perm-hint" id="perm-guide">⚠ 请允许摄像头权限以开启手势交互</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"></div>
<div class="exit-btn" id="exit-btn" onclick="exitArchive()">[ EXIT ARCHIVE ]</div>
</div>
<div class="header" style="align-self: flex-end;">
<span id="lang-display">EN</span>
</div>
</div>
<video id="input-video" style="display:none"></video>
<div id="canvas-container"></div>
<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
};
// 安全DOM操作防止报错
function safeUpdateText(id, text) {
const el = document.getElementById(id);
if (el) el.innerText = text;
}
function safeClass(id, method, className) {
const el = document.getElementById(id);
if (el) el.classList[method](className);
}
/**
* ============================================================================
* 2. 环境引擎 (主题 & 语言)
* ============================================================================
*/
function getStoredTheme() {
const cacheKey = 'theme-v2';
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()
};
// 应用主题
document.documentElement.setAttribute('data-theme', ENV.theme);
safeUpdateText('theme-display', `THEME: ${ENV.theme.toUpperCase()}`);
safeUpdateText('lang-display', ENV.lang.toUpperCase());
/**
* ============================================================================
* 3. 内容字典
* ============================================================================
*/
const DICTIONARY = {
zh: {
load: [
"正在构建数字灵魂...",
"接入神经元网络...",
"校准引力波场...",
"等待视觉信号...",
"系统准备就绪."
],
hints: {
main: "点击 或 双手合十 开启",
sub: "单手·流体牵引 | 双手·力场排斥",
unlocking: "正在识别..."
},
slides: [
{ t: "HE HOUHUI", s: "JAVA & AI 架构师" },
{ t: "INFJ", s: "1% 的提倡者 // 理想主义" },
{ t: "深度 > 广度", s: "在垂直领域构建壁垒" },
{ t: "长期主义", s: "做时间的朋友" },
{ t: "代码哲学", s: "代码是逻辑的载体" },
{ t: "创造", s: "赋予技术以意义" }
]
},
en: {
load: [
"Constructing Digital Soul...",
"Connecting Neural Network...",
"Calibrating Gravity Field...",
"Waiting for Visual Sensor...",
"System Ready."
],
hints: {
main: "CLICK OR NAMASTE",
sub: "1 Hand Drag · 2 Hands Repel",
unlocking: "IDENTIFYING..."
},
slides: [
{ t: "HE HOUHUI", s: "JAVA & AI ARCHITECT" },
{ t: "INFJ", s: "THE ADVOCATE // 1% UNIVERSE" },
{ t: "DEPTH > BREADTH", s: "BUILDING FORTRESSES" },
{ t: "LONG-TERMISM", s: "FRIEND OF TIME" },
{ t: "CODE PHILOSOPHY", s: "VESSEL OF LOGIC" },
{ t: "CREATE", s: "EMPOWER MEANING" }
]
}
};
const CONTENT = DICTIONARY[ENV.lang];
// 循环播放加载文案
let loadIdx = 0;
const loadTimer = setInterval(() => {
if (APP_STATE.isLoaded) { clearInterval(loadTimer); return; }
safeUpdateText('loader-msg', CONTENT.load[loadIdx % CONTENT.load.length]);
loadIdx++;
}, 2000);
// 权限超时提示
setTimeout(() => {
if (!APP_STATE.isLoaded) safeClass('perm-hint', 'add', 'show');
}, 5000);
safeUpdateText('main-hint', CONTENT.hints.main);
safeUpdateText('sub-hint', CONTENT.hints.sub);
/**
* ============================================================================
* 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);
document.getElementById('canvas-container').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 geometry = new THREE.BufferGeometry();
const positions = new Float32Array(CONFIG.particleCount * 3);
const targets = new Float32Array(CONFIG.particleCount * 3);
const origin = new Float32Array(CONFIG.particleCount * 3);
const velocities = new Float32Array(CONFIG.particleCount * 3);
const sizes = new Float32Array(CONFIG.particleCount);
const simplex = new SimplexNoise();
for(let i=0; i<CONFIG.particleCount; i++) {
const i3 = i*3;
// 黄金螺旋球
const y = 1 - (i / (CONFIG.particleCount - 1)) * 2;
const radius = Math.sqrt(1 - y * y);
const theta = i * 2.39996;
const r = 280;
const x = Math.cos(theta) * radius * r;
const z = Math.sin(theta) * radius * r;
const py = y * r;
positions[i3] = x; origin[i3] = x; targets[i3] = x;
positions[i3+1] = py; origin[i3+1] = py; targets[i3+1] = py;
positions[i3+2] = z; origin[i3+2] = z; targets[i3+2] = z;
// 白天粒子略大,增强可见度
sizes[i] = (Math.random() * 2.5 + 0.5) * (ENV.theme==='day'?1.3:1.0);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
scale: { value: window.innerHeight / 2 },
baseColor: { value: CONFIG.colors.base },
activeColor: { value: CONFIG.colors.active },
mixVal: { value: 0.0 }
},
vertexShader: `
attribute float size;
void main() {
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_PointSize = size * ( 500.0 / -mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 baseColor;
uniform vec3 activeColor;
uniform float mixVal;
void main() {
float r = length(gl_PointCoord - vec2(0.5));
if (r > 0.5) discard;
vec3 finalColor = mix(baseColor, activeColor, mixVal);
gl_FragColor = vec4(finalColor, 1.0);
}
`,
blending: CONFIG.blending,
depthTest: false,
transparent: true
});
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
// 手势光标
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();
// 颜色插值
material.uniforms.mixVal.value += (targetMix - material.uniforms.mixVal.value) * 0.1;
// 1. 形状更新 (锁定模式:呼吸球)
if (APP_STATE.mode === 'LOCKED') {
targetMix = APP_STATE.handCount > 0 ? 0.6 : 0.0;
const ns = 0.002; const ts = time * 0.15;
for(let i=0; i<CONFIG.particleCount; i++) {
const i3 = i*3;
const ox = origin[i3]; const oy = origin[i3+1]; const oz = origin[i3+2];
const noise = simplex.noise3D(ox*ns + ts, oy*ns, oz*ns + ts);
let offX=0, offY=0;
if(APP_STATE.handCount===1) {
offX = APP_STATE.handL.x * 0.15;
offY = APP_STATE.handL.y * 0.15;
}
const scale = 1 + noise * 0.3;
targets[i3] = ox * scale + offX;
targets[i3+1] = oy * scale + offY;
targets[i3+2] = oz * scale;
}
} else {
targetMix = 1.0;
}
// 2. 物理迭代
for(let i=0; i<CONFIG.particleCount; i++) {
const i3 = i*3;
const px = positions[i3]; const py = positions[i3+1]; const pz = positions[i3+2];
// 归位力
const stiff = APP_STATE.mode === 'LOCKED' ? 0.03 : 0.05;
velocities[i3] += (targets[i3] - px) * stiff;
velocities[i3+1] += (targets[i3+1] - py) * stiff;
velocities[i3+2] += (targets[i3+2] - pz) * stiff;
// --- 交互物理 ---
if (APP_STATE.handCount === 1) {
// 单手黑洞 (增强吸附感)
const hx = APP_STATE.handL.x;
const hy = APP_STATE.handL.y;
const dx = hx - px;
const dy = hy - py;
const distSq = dx*dx + dy*dy;
if (distSq < 150000) {
const f = (150000 - distSq) / 150000;
velocities[i3] += dx * f * 0.05;
velocities[i3+1] += dy * f * 0.05;
velocities[i3+2] += Math.sin(time*10 + distSq*0.0001) * 8 * f; // 波纹效果
}
} else if (APP_STATE.handCount === 2) {
// 双手斥力
[APP_STATE.handL, APP_STATE.handR].forEach(h => {
const dx = px - h.x; const dy = py - h.y;
const distSq = dx*dx + dy*dy;
if (distSq < 80000) {
const f = (80000 - distSq) / 80000;
velocities[i3] -= dx * f * 0.3;
velocities[i3+1] -= dy * f * 0.3;
velocities[i3+2] += 15 * f;
}
});
}
velocities[i3] *= 0.90;
velocities[i3+1] *= 0.90;
velocities[i3+2] *= 0.90;
positions[i3] += velocities[i3];
positions[i3+1] += velocities[i3+1];
positions[i3+2] += velocities[i3+2];
}
geometry.attributes.position.needsUpdate = true;
if(APP_STATE.handCount===0) particleSystem.rotation.y += 0.002;
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;
window.enterArchive = function() {
if (APP_STATE.mode === 'UNLOCKED') return;
APP_STATE.mode = 'UNLOCKED';
// UI
safeClass('main-hint', 'add', 'hidden'); // 隐藏主提示 (CSS需支持或直接display)
document.getElementById('main-hint').style.display = 'none';
document.getElementById('sub-hint').style.display = 'none';
safeClass('exit-btn', 'add', 'visible');
// 粒子爆炸效果
explode(100);
startNarrative();
}
window.exitArchive = function() {
APP_STATE.mode = 'LOCKED';
document.getElementById('main-hint').style.display = 'block';
document.getElementById('sub-hint').style.display = 'block';
safeClass('exit-btn', 'remove', 'visible');
safeClass('narrative-layer', 'remove', 'show-text');
clearTimeout(narrativeTimer);
explode(50);
}
function explode(f) {
for(let i=0; i<CONFIG.particleCount; i++) {
velocities[i*3] += (Math.random()-0.5)*f;
velocities[i*3+1] += (Math.random()-0.5)*f;
velocities[i*3+2] += (Math.random()-0.5)*f;
}
}
function startNarrative() {
let idx = 0;
const nTitle = document.getElementById('n-title');
const nSub = document.getElementById('n-sub');
const layer = document.getElementById('narrative-layer');
// 必须检查元素是否存在
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');
// 粒子散开做背景
for(let i=0; i<CONFIG.particleCount; i++) {
const a = i * 0.1;
const r = 900 + Math.random()*200;
targets[i*3] = Math.cos(a)*r;
targets[i*3+1] = (Math.random()-0.5)*300;
targets[i*3+2] = Math.sin(a)*r;
}
narrativeTimer = setTimeout(() => {
layer.classList.remove('show-text');
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.7, minTrackingConfidence: 0.7 });
hands.onResults(results => {
// Loader logic
if (!APP_STATE.isLoaded) {
APP_STATE.isLoaded = true;
const loader = document.getElementById('start-screen');
loader.style.opacity = 0;
document.getElementById('ui-layer').style.opacity = 1;
setTimeout(() => loader.style.display = 'none', 1000);
}
const landmarks = results.multiHandLandmarks;
const sysStatus = document.getElementById('sys-status');
if (landmarks && landmarks.length > 0) {
APP_STATE.handCount = landmarks.length;
safeUpdateText('sys-status', `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') {
const w1=landmarks[0][0]; const w2=landmarks[1][0];
const t1=landmarks[0][12]; const t2=landmarks[1][12];
if (Math.hypot(w1.x-w2.x, w1.y-w2.y)<0.25 && Math.hypot(t1.x-t2.x, t1.y-t2.y)<0.2) {
APP_STATE.unlockProgress++;
safeUpdateText('main-hint', `${CONTENT.hints.unlocking} ${APP_STATE.unlockProgress}%`);
if(APP_STATE.unlockProgress > 50) enterArchive();
} else {
APP_STATE.unlockProgress = 0;
safeUpdateText('main-hint', CONTENT.hints.main);
}
}
} else {
APP_STATE.handCount = 0;
safeUpdateText('sys-status', 'SEARCHING...');
if(sysStatus) sysStatus.style.color = 'inherit';
}
});
const videoElement = document.getElementById('input-video');
const cameraUtils = new Camera(videoElement, {
onFrame: async () => { await hands.send({image: videoElement}); },
width: 640, height: 480
});
cameraUtils.start();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
material.uniforms.scale.value = window.innerHeight/2;
});
animate();
</script>
</body>
</html>