Files
home/me.html
hehh e183f8bf63 feat(me): 实现昼夜主题切换与智能叙事系统
- 新增昼夜模式自动切换功能,根据时间和系统偏好设置主题
- 优化粒子系统,支持不同主题下的视觉效果差异
- 重构UI层结构,增强交互提示与视觉反馈
- 添加智能AI加载动画与科技感加载页
- 实现手势交互物理引擎,支持单手磁流体牵引与双手斥力场
- 增加多语言支持(中/英),动态内容切换
- 优化Three.js渲染配置,提升性能与视觉表现
- 添加叙事文本动画与过渡效果
- 改进着色器材质,支持颜色渐变插值
- 实现主题缓存机制,提高用户体验一致性
2025-12-04 14:26:51 +08:00

675 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;
}
/* 白天模式变量 */
[data-theme="day"] {
--bg-color: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%); /* 清新银蓝 */
--text-color: #1a1a2e; /* 深蓝黑文字 */
--accent-color: #0055ff; /* 科技蓝 */
--loader-bg: #f5f7fa;
}
/* 黑夜模式变量 */
[data-theme="night"] {
--bg-color: radial-gradient(circle at 50% 50%, #111122 0%, #050510 60%, #000000 100%);
--text-color: #FFFFFF;
--accent-color: #00FFFF;
--loader-bg: #000000;
}
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;
}
/* === UI 层 === */
#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; opacity: 0.6;
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; transition: all 0.3s;
padding: 20px; border: 1px solid transparent;
}
.main-hint:hover {
transform: scale(1.02);
letter-spacing: 10px;
color: var(--accent-color);
}
.sub-hint {
font-size: 10px; opacity: 0.6; margin-top: 10px; letter-spacing: 3px;
font-family: 'Courier New', monospace;
}
.exit-trigger {
margin-top: 40px; display: inline-block;
font-size: 10px; letter-spacing: 2px;
padding: 8px 16px; border: 1px solid rgba(128,128,128,0.3);
cursor: pointer; opacity: 0; transform: translateY(10px); transition: all 0.5s;
pointer-events: none; background: rgba(128,128,128,0.1); backdrop-filter: blur(5px);
border-radius: 20px;
}
.exit-trigger.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
.exit-trigger:hover { background: var(--accent-color); color: #FFF; border-color: var(--accent-color); }
/* === 叙事纯文本层 === */
#narrative-overlay {
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: 60px; font-weight: 800; 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: 14px; 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); }
/* === 智能AI加载页 === */
#ai-loader {
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.7, 0, 0.3, 1);
}
/* 科技感加载动画 */
.tech-spinner {
width: 60px; height: 60px; border: 2px solid var(--accent-color);
border-top: 2px solid transparent; border-radius: 50%;
animation: spin 1s linear infinite; margin-bottom: 30px;
}
.loader-quote {
font-family: 'Helvetica Neue', sans-serif; font-size: 14px;
max-width: 500px; text-align: center; line-height: 1.8; min-height: 50px;
opacity: 0; transition: opacity 0.5s; font-weight: 300; letter-spacing: 1px;
color: var(--text-color);
}
@keyframes spin { 0% {transform: rotate(0deg)} 100% {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>
<!-- AI 加载 -->
<div id="ai-loader">
<div class="tech-spinner"></div>
<div class="loader-quote" id="ai-quote"></div>
</div>
<!-- 叙事文本 -->
<div id="narrative-overlay">
<div class="n-title" id="n-title"></div>
<div class="n-sub" id="n-sub"></div>
</div>
<!-- UI -->
<div id="ui-layer">
<div class="header">
<span id="sys-status">SYSTEM IDLE</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-trigger" id="exit-btn" onclick="exitArchive()">[ 退出档案 / EXIT ]</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 src="js/config.js?version=20251125"></script>
<script>
/**
* ============================================================================
* 1. 主题与语言引擎
* ============================================================================
*/
// --- 主题逻辑 ---
function setStoredTheme(theme) {
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
const cacheData = { value: theme, time: new Date().getTime() };
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
}
function getStoredTheme() {
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
const timeout = window.SiteConfig?.cacheKeys?.theme?.ttlMs || 360000;
const cacheJson = localStorage.getItem(cacheKey);
const saved = cacheJson ? JSON.parse(cacheJson) : null;
let theme = 'day'; // 默认
if (saved == null || new Date().getTime() - timeout > saved.time) {
const hour = new Date().getHours();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const night = hour >= 18 || hour < 6 || prefersDark;
theme = night ? 'night' : 'day';
setStoredTheme(theme);
} else if (saved.value) {
theme = saved.value;
}
return theme;
}
function getStoredLanguage() {
return localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en');
}
// --- 初始化环境 ---
const ENV = {
theme: getStoredTheme(),
lang: getStoredLanguage()
};
// 应用主题到 DOM
document.documentElement.setAttribute('data-theme', ENV.theme);
document.getElementById('theme-display').innerText = `THEME: ${ENV.theme.toUpperCase()}`;
document.getElementById('lang-display').innerText = ENV.lang.toUpperCase();
/**
* ============================================================================
* 2. 内容库
* ============================================================================
*/
const DICTIONARY = {
zh: {
loading: ["逻辑是思维的脚手架。", "优雅不仅是外观,更是对复杂性的征服。", "万物互联,数据如水。", "深度思考是对抗熵增的唯一武器。", "代码即法律,设计即秩序。", "正在重构数字现实...", "正在接入神经元网络...", "INFJ: 洞察看不见的真实。"],
hints: {
main: "点击 或 双手合十 开启",
sub: "单手·磁流体牵引 | 双手·力场排斥",
unlocking: "识别中..."
},
slides: [
{ t: "HE HOUHUI", s: "JAVA & AI 工程师" },
{ t: "INFJ", s: "洞察者 · 理想主义" },
{ t: "深度 > 广度", s: "在垂直领域构建壁垒" },
{ t: "长期主义", s: "与时间做朋友" },
{ t: "代码哲学", s: "系统是思想的容器" },
{ t: "创造", s: "以技术赋予意义" }
]
},
en: {
loading: ["Logic is the scaffolding of the mind.", "Elegance is the conquest of complexity.", "Everything connects, data flows like water.", "Deep thinking is the weapon against entropy.", "Code is Law, Design is Order.", "Reconstructing digital reality...", "Connecting neural network...", "INFJ: Seeing the unseen."],
hints: {
main: "CLICK OR NAMASTE",
sub: "1 Hand Drag · 2 Hands Repel",
unlocking: "IDENTIFYING..."
},
slides: [
{ t: "HE HOUHUI", s: "JAVA & AI ENGINEER" },
{ 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 THOUGHT" },
{ t: "CREATE", s: "EMPOWERING MEANING" }
]
}
};
const CONTENT = DICTIONARY[ENV.lang];
// 加载动画文字循环
let qIdx = 0;
const qEl = document.getElementById('ai-quote');
setInterval(() => {
qEl.style.opacity = 0;
setTimeout(() => {
qEl.innerText = CONTENT.loading[qIdx % CONTENT.loading.length];
qEl.style.opacity = 1;
qIdx++;
}, 500);
}, 3000);
document.getElementById('main-hint').innerText = CONTENT.hints.main;
document.getElementById('sub-hint').innerText = CONTENT.hints.sub;
/**
* ============================================================================
* 3. THREE.JS 渲染配置 (Day/Night 核心差异)
* ============================================================================
*/
const CONFIG = {
particleCount: 22000,
camZ: 600,
// 关键:混合模式配置
blending: ENV.theme === 'night' ? THREE.AdditiveBlending : THREE.NormalBlending,
// 关键:颜色配置
colors: ENV.theme === 'night'
? { base: new THREE.Color(0xFFFFFF), active: new THREE.Color(0x00FFFF) } // Night: White/Cyan Glow
: { base: new THREE.Color(0x2c3e50), active: new THREE.Color(0x3498db) }, // Day: Dark Grey/Blue Solid
bloomStrength: ENV.theme === 'night' ? 1.5 : 0.0 // Day Mode: 0 Bloom to ensure clarity
};
const scene = new THREE.Scene();
// Day mode fog must match background color to hide distant particles
scene.fog = new THREE.FogExp2(ENV.theme==='night'?0x000000:0xe0eafc, 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); // Transparent for CSS bg
document.getElementById('canvas-container').appendChild(renderer.domElement);
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
if (CONFIG.bloomStrength > 0) {
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.strength = CONFIG.bloomStrength;
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.2:1.0);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// Shader 材质:支持颜色渐变插值
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 } // 0 = base, 1 = active
},
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;
// 简单的光照伪装
float alpha = 1.0;
// 颜色混合
vec3 finalColor = mix(baseColor, activeColor, mixVal);
gl_FragColor = vec4(finalColor, alpha);
}
`,
blending: CONFIG.blending,
depthTest: false,
transparent: true
});
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
// 手势光标 (仅 Night 模式发光Day 模式为实心圈)
const cursorMat = new THREE.MeshBasicMaterial({
color: ENV.theme==='day' ? 0x0055ff : 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);
/**
* ============================================================================
* 4. 物理引擎 (单手磁流体 / 双手斥力)
* ============================================================================
*/
const STATE = {
mode: 'LOCKED',
handL: new THREE.Vector3(9999,9999,0),
handR: new THREE.Vector3(9999,9999,0),
handCount: 0,
unlockProgress: 0,
targetMix: 0 // 颜色插值目标
};
const clock = new THREE.Clock();
function animate() {
const time = clock.getElapsedTime();
// 颜色动态更新
material.uniforms.mixVal.value += (STATE.targetMix - material.uniforms.mixVal.value) * 0.1;
// 1. 动态形状 (呼吸球)
if (STATE.mode === 'LOCKED') {
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(STATE.handCount===1) {
offX = STATE.handL.x * 0.15;
offY = STATE.handL.y * 0.15;
STATE.targetMix = 0.5; // 变色
} else if (STATE.handCount===0) {
STATE.targetMix = 0.0;
}
const scale = 1 + noise * 0.3;
targets[i3] = ox * scale + offX;
targets[i3+1] = oy * scale + offY;
targets[i3+2] = oz * scale;
}
} else {
STATE.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 = 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 (STATE.handCount === 1) {
// 单手:磁流体黑洞 (丝滑跟随)
const hx = STATE.handL.x; const hy = STATE.handL.y;
const dx = hx - px; const dy = hy - py;
const distSq = dx*dx + dy*dy;
if (distSq < 120000) {
const f = (120000 - distSq) / 120000;
// 吸引 + 旋转
velocities[i3] += dx * f * 0.06;
velocities[i3+1] += dy * f * 0.06;
velocities[i3+2] += 5 * f; // 立体扰动
}
} else if (STATE.handCount === 2) {
// 双手:斥力场
[STATE.handL, STATE.handR].forEach(h => {
const dx = px - h.x; const dy = py - h.y;
const distSq = dx*dx + dy*dy;
if (distSq < 70000) {
const f = (70000 - distSq) / 70000;
velocities[i3] -= dx * f * 0.3; // 强力推开
velocities[i3+1] -= dy * f * 0.3;
velocities[i3+2] += 10 * 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(STATE.handCount===0) particleSystem.rotation.y += 0.002;
// 光标
cursorL.position.set(STATE.handL.x, STATE.handL.y, 0);
cursorR.position.set(STATE.handR.x, STATE.handR.y, 0);
cursorL.visible = (STATE.handCount >= 1);
cursorR.visible = (STATE.handCount === 2);
composer.render();
requestAnimationFrame(animate);
}
/**
* ============================================================================
* 5. 逻辑控制 (叙事/进入/退出)
* ============================================================================
*/
let narrativeTimer = null;
window.enterArchive = function() {
if (STATE.mode === 'UNLOCKED') return;
STATE.mode = 'UNLOCKED';
// UI Update
document.getElementById('main-hint').style.display = 'none';
document.getElementById('sub-hint').style.display = 'none';
document.getElementById('exit-btn').classList.add('visible');
// 粒子散开形成背景环
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;
}
startNarrative();
}
window.exitArchive = function() {
STATE.mode = 'LOCKED';
// UI Reset
document.getElementById('main-hint').style.display = 'block';
document.getElementById('sub-hint').style.display = 'block';
document.getElementById('exit-btn').classList.remove('visible');
document.getElementById('narrative-overlay').classList.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 overlay = document.getElementById('narrative-overlay');
const next = () => {
if (STATE.mode !== 'UNLOCKED') return;
const slide = CONTENT.slides[idx % CONTENT.slides.length];
nTitle.innerText = slide.t;
nSub.innerText = slide.s;
overlay.classList.add('show-text');
// 3秒后文字消失粒子稍微聚拢一下产生呼吸感
narrativeTimer = setTimeout(() => {
overlay.classList.remove('show-text');
// Particle Pulse
explode(10);
narrativeTimer = setTimeout(next, 1000);
}, 3000);
idx++;
};
next();
}
/**
* ============================================================================
* 6. 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 => {
const loader = document.getElementById('ai-loader');
if (loader.style.opacity !== '0') {
document.querySelector('.loader-progress').innerText = "NEURAL LINK ESTABLISHED";
setTimeout(() => {
loader.style.opacity = 0;
document.getElementById('ui-layer').style.opacity = 1;
setTimeout(() => loader.style.display = 'none', 1000);
}, 1000);
}
const landmarks = results.multiHandLandmarks;
if (landmarks && landmarks.length > 0) {
STATE.handCount = landmarks.length;
document.getElementById('sys-status').innerText = `CONNECTED (${STATE.handCount})`;
// 坐标处理 (1.0 - x 实现镜像)
const process = (lm) => {
return {
x: ( (1.0 - lm.x) * 2 - 1 ) * 800,
y: -(lm.y * 2 - 1 - 0.2) * 600
};
};
const p1 = process(landmarks[0][9]);
STATE.handL.x += (p1.x - STATE.handL.x) * 0.2;
STATE.handL.y += (p1.y - STATE.handL.y) * 0.2;
if (landmarks.length > 1) {
const p2 = process(landmarks[1][9]);
STATE.handR.x += (p2.x - STATE.handR.x) * 0.2;
STATE.handR.y += (p2.y - STATE.handR.y) * 0.2;
}
// Unlock Logic
if (landmarks.length === 2 && 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.2 && Math.hypot(t1.x-t2.x, t1.y-t2.y) < 0.15) {
STATE.unlockProgress++;
document.getElementById('main-hint').innerText = `${CONTENT.hints.unlocking} ${STATE.unlockProgress}%`;
if (STATE.unlockProgress > 50) enterArchive();
} else {
STATE.unlockProgress = 0;
document.getElementById('main-hint').innerText = CONTENT.hints.main;
}
}
} else {
STATE.handCount = 0;
document.getElementById('sys-status').innerText = "SCANNING...";
}
});
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>