- 移除旧版加载屏与叙事层DOM结构 - 新增基于Three.js的粒子系统引擎 - 实现动态主题检测与切换功能 - 更新CSS变量系统以支持RGB颜色模式 - 优化UI层布局结构提升响应式体验 - 添加节庆主题自动识别逻辑 - 简化HTML结构并增强语义化程度 - 调整字体与色彩配置提高可读性 - 引入新的动画呼吸效果与脉冲效果 - 重构JavaScript模块提升代码组织性
732 lines
28 KiB
HTML
732 lines
28 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 {
|
||
--font-serif: 'Times New Roman', 'Songti SC', serif;
|
||
--font-sans: 'Helvetica Neue', 'Arial', sans-serif;
|
||
--ui-color: 255, 255, 255;
|
||
--accent-color: 100, 200, 255;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
background-color: #050505;
|
||
color: rgb(var(--ui-color));
|
||
font-family: var(--font-sans);
|
||
user-select: none;
|
||
}
|
||
|
||
/* 画布层 */
|
||
#canvas-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 视频输入(隐藏,用于Debug可开启) */
|
||
#input-video {
|
||
display: none;
|
||
position: fixed;
|
||
bottom: 0;
|
||
right: 0;
|
||
width: 200px;
|
||
z-index: 0;
|
||
opacity: 0.2;
|
||
transform: scaleX(-1); /* 镜像预览 */
|
||
}
|
||
|
||
/* UI 层 */
|
||
#ui-layer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 10;
|
||
pointer-events: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding: 40px;
|
||
box-sizing: border-box;
|
||
background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.4) 100%);
|
||
}
|
||
|
||
/* 顶部信息 */
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
text-transform: uppercase;
|
||
letter-spacing: 2px;
|
||
font-size: 10px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 中间叙事文字 */
|
||
.narrative-container {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
width: 80%;
|
||
max-width: 800px;
|
||
mix-blend-mode: overlay;
|
||
}
|
||
|
||
.n-title {
|
||
font-family: var(--font-serif);
|
||
font-size: clamp(32px, 5vw, 64px);
|
||
font-weight: 300;
|
||
margin-bottom: 20px;
|
||
letter-spacing: 8px;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 1.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||
text-shadow: 0 0 30px rgba(var(--accent-color), 0.5);
|
||
}
|
||
|
||
.n-sub {
|
||
font-size: 14px;
|
||
letter-spacing: 4px;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 1.5s 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||
}
|
||
|
||
.active .n-title, .active .n-sub {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 底部交互提示 */
|
||
.footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
font-size: 12px;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.interaction-hint {
|
||
text-align: center;
|
||
width: 100%;
|
||
opacity: 0.6;
|
||
animation: breathe 4s infinite ease-in-out;
|
||
}
|
||
|
||
.gesture-icon {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
background: rgb(var(--accent-color));
|
||
border-radius: 50%;
|
||
margin-right: 10px;
|
||
box-shadow: 0 0 10px rgb(var(--accent-color));
|
||
}
|
||
|
||
/* 加载屏 */
|
||
#loader {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: #000;
|
||
z-index: 100;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
transition: opacity 1s;
|
||
}
|
||
|
||
.loader-text {
|
||
font-family: var(--font-serif);
|
||
letter-spacing: 5px;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes breathe { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; } }
|
||
@keyframes pulse { 0% { opacity: 0.3; } 50% { opacity: 1; } 100% { opacity: 0.3; } }
|
||
|
||
</style>
|
||
|
||
<!-- Libraries -->
|
||
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<!-- Post Processing -->
|
||
<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/postprocessing/UnrealBloomPass.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>
|
||
<!-- MediaPipe -->
|
||
<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>
|
||
|
||
<div id="loader"><div class="loader-text">INITIALIZING NEURAL LINK...</div></div>
|
||
|
||
<div id="canvas-container"></div>
|
||
<video id="input-video" playsinline></video>
|
||
|
||
<div id="ui-layer">
|
||
<div class="header">
|
||
<span id="theme-name">THEME: DETECTING...</span>
|
||
<span id="system-status">WAITING FOR CAMERA</span>
|
||
</div>
|
||
|
||
<div class="narrative-container" id="narrative-box">
|
||
<div class="n-title" id="text-title">HONESTY</div>
|
||
<div class="n-sub" id="text-sub">INTELLIGENT ARCHIVE</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<div class="interaction-hint">
|
||
<span id="gesture-hint">双手合十 · 唤醒记忆</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/**
|
||
* ============================================================================
|
||
* 1. THEME ENGINE (节日/主题算法)
|
||
* ============================================================================
|
||
*/
|
||
const Themes = {
|
||
DEFAULT: { id: 'default', name: 'Saturn / 素墨', colors: [0x8899a6, 0x4a90e2], shape: 'SATURN' },
|
||
SPRING: { id: 'spring', name: 'Spring / 春笺', colors: [0xff0000, 0xffd700], shape: 'LANTERN' },
|
||
VALENTINE: { id: 'valentine', name: 'Love / 心语', colors: [0xff69b4, 0xff1493], shape: 'HEART' },
|
||
QINGMING: { id: 'qingming', name: 'Rain / 雨巷', colors: [0x2c3e50, 0xbdc3c7], shape: 'WILLOW' },
|
||
MIDAUTUMN: { id: 'midautumn', name: 'Moon / 月笺', colors: [0x0f1729, 0xffd700], shape: 'MOON' },
|
||
NATIONAL: { id: 'national', name: 'Glory / 山河', colors: [0xff0000, 0xffff00], shape: 'STAR' },
|
||
HALLOWEEN: { id: 'halloween', name: 'Night / 夜宴', colors: [0xff8c00, 0x800080], shape: 'PUMPKIN' },
|
||
GHOST: { id: 'ghost', name: 'Spirit / 幽思', colors: [0x008080, 0x90ee90], shape: 'LOTUS' },
|
||
CHRISTMAS: { id: 'christmas', name: 'Snow / 雪颂', colors: [0x0f5e36, 0xc41e3a], shape: 'TREE' }
|
||
};
|
||
|
||
const Narratives = {
|
||
default: { t: "HONESTY", s: "In the silence of the universe, I found myself." },
|
||
spring: { t: "RENEWAL", s: "Every beginning is a promise to the future." },
|
||
valentine: { t: "DEVOTION", s: "Love is the only gravity that transcends time." },
|
||
qingming: { t: "MEMORY", s: "Rain falls like ink, writing stories of the past." },
|
||
midautumn: { t: "REUNION", s: "Though miles apart, we share the same moonlight." },
|
||
national: { t: "PRIDE", s: "Stars shine brightest when they burn together." },
|
||
halloween: { t: "MASK", s: "We are all ghosts driving meat-coated skeletons." },
|
||
ghost: { t: "FLOAT", s: "Lights on the river, guiding souls back home." },
|
||
christmas: { t: "WISH", s: "Warmth is not a temperature, but a presence." }
|
||
};
|
||
|
||
class ThemeManager {
|
||
constructor() {
|
||
this.currentTheme = Themes.DEFAULT;
|
||
this.detectTheme();
|
||
}
|
||
|
||
detectTheme() {
|
||
const now = new Date();
|
||
const m = now.getMonth() + 1; // 1-12
|
||
const d = now.getDate();
|
||
|
||
// 简单节日映射 (为了演示,公历近似)
|
||
// 实际项目可引入 Lunar Calendar 库
|
||
const festivals = [
|
||
{ t: Themes.SPRING, m: 2, d: 10, range: 10 }, // 假设春节
|
||
{ t: Themes.VALENTINE, m: 2, d: 14, range: 7 },
|
||
{ t: Themes.QINGMING, m: 4, d: 4, range: 7 },
|
||
{ t: Themes.GHOST, m: 8, d: 18, range: 7 }, // 中元节近似
|
||
{ t: Themes.MIDAUTUMN, m: 9, d: 17, range: 7 }, // 中秋近似
|
||
{ t: Themes.NATIONAL, m: 10, d: 1, range: 7 },
|
||
{ t: Themes.HALLOWEEN, m: 10, d: 31, range: 7 },
|
||
{ t: Themes.CHRISTMAS, m: 12, d: 25, range: 10 }
|
||
];
|
||
|
||
let closest = null;
|
||
let minDist = Infinity;
|
||
|
||
festivals.forEach(fes => {
|
||
// 简单日期距离计算 (忽略跨年问题,简单demo)
|
||
if (Math.abs(fes.m - m) <= 1) {
|
||
const diff = Math.abs((fes.m * 30 + fes.d) - (m * 30 + d));
|
||
if (diff <= fes.range && diff < minDist) {
|
||
minDist = diff;
|
||
closest = fes.t;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 18:00 - 6:00 强制黑夜模式逻辑由渲染器处理,这里只管节日
|
||
this.currentTheme = closest || Themes.DEFAULT;
|
||
this.applyUI();
|
||
}
|
||
|
||
applyUI() {
|
||
document.getElementById('theme-name').innerText = `THEME: ${this.currentTheme.name}`;
|
||
const color = new THREE.Color(this.currentTheme.colors[1]);
|
||
document.documentElement.style.setProperty('--accent-color', `${color.r*255}, ${color.g*255}, ${color.b*255}`);
|
||
|
||
// Update Narrative
|
||
const txt = Narratives[this.currentTheme.id] || Narratives.default;
|
||
const titleEl = document.getElementById('text-title');
|
||
const subEl = document.getElementById('text-sub');
|
||
const box = document.getElementById('narrative-box');
|
||
|
||
box.classList.remove('active');
|
||
setTimeout(() => {
|
||
titleEl.innerText = txt.t;
|
||
subEl.innerText = txt.s;
|
||
box.classList.add('active');
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 2. PARTICLE SYSTEM (核心物理引擎)
|
||
* ============================================================================
|
||
*/
|
||
class ParticleEngine {
|
||
constructor(scene, count = 20000) {
|
||
this.scene = scene;
|
||
this.count = count;
|
||
this.particles = null;
|
||
this.geometry = null;
|
||
this.material = null;
|
||
|
||
// Physics Data
|
||
this.positions = new Float32Array(count * 3);
|
||
this.targets = new Float32Array(count * 3); // 目标形状位置
|
||
this.velocities = new Float32Array(count * 3);
|
||
this.colors = new Float32Array(count * 3);
|
||
|
||
this.state = {
|
||
mode: 'SCATTER', // SCATTER, AGGREGATE
|
||
timeScale: 1.0, // 子弹时间
|
||
mouse: new THREE.Vector3(0, 0, 0),
|
||
repel: false,
|
||
attract: false
|
||
};
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.geometry = new THREE.BufferGeometry();
|
||
|
||
// Initialize random positions
|
||
for(let i=0; i<this.count; i++) {
|
||
const i3 = i*3;
|
||
this.positions[i3] = (Math.random() - 0.5) * 50;
|
||
this.positions[i3+1] = (Math.random() - 0.5) * 50;
|
||
this.positions[i3+2] = (Math.random() - 0.5) * 20;
|
||
|
||
// Set initial colors based on default theme
|
||
this.setColor(i, Themes.DEFAULT.colors);
|
||
}
|
||
|
||
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
|
||
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
|
||
|
||
// Shader for romantic glow & performance
|
||
this.material = new THREE.PointsMaterial({
|
||
size: 0.15,
|
||
vertexColors: true,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
transparent: true,
|
||
opacity: 0.8
|
||
});
|
||
|
||
this.particles = new THREE.Points(this.geometry, this.material);
|
||
this.scene.add(this.particles);
|
||
|
||
// Initial Target Generation
|
||
this.setShape(Themes.DEFAULT.shape);
|
||
}
|
||
|
||
setColor(i, colorPair) {
|
||
const c1 = new THREE.Color(colorPair[0]);
|
||
const c2 = new THREE.Color(colorPair[1]);
|
||
// Random mix
|
||
const mix = Math.random();
|
||
this.colors[i*3] = c1.r * mix + c2.r * (1-mix);
|
||
this.colors[i*3+1] = c1.g * mix + c2.g * (1-mix);
|
||
this.colors[i*3+2] = c1.b * mix + c2.b * (1-mix);
|
||
}
|
||
|
||
updateColors(theme) {
|
||
// Smooth transition needs shader, here we just snap for demo simplicity
|
||
// or re-generate buffer
|
||
for(let i=0; i<this.count; i++) {
|
||
this.setColor(i, theme.colors);
|
||
}
|
||
this.geometry.attributes.color.needsUpdate = true;
|
||
}
|
||
|
||
// 数学之美:形状生成器
|
||
setShape(shapeType) {
|
||
const scale = 15;
|
||
for(let i=0; i<this.count; i++) {
|
||
const i3 = i*3;
|
||
let x, y, z;
|
||
const idx = i / this.count;
|
||
const theta = idx * Math.PI * 2 * 20; // spiral
|
||
const phi = Math.acos(2 * Math.random() - 1);
|
||
|
||
switch(shapeType) {
|
||
case 'HEART':
|
||
// 3D Heart parametric
|
||
const t = Math.PI * (Math.random() - 0.5) * 2; // -PI to PI
|
||
const u = Math.random() * 2 * Math.PI; // 0 to 2PI
|
||
// Simplified Heart
|
||
x = 16 * Math.pow(Math.sin(u), 3);
|
||
y = 13 * Math.cos(u) - 5 * Math.cos(2*u) - 2 * Math.cos(3*u) - Math.cos(4*u);
|
||
z = t * 5;
|
||
x *= 0.5; y *= 0.5;
|
||
break;
|
||
|
||
case 'SATURN':
|
||
if (Math.random() > 0.3) {
|
||
// Sphere
|
||
const r = 5 * Math.cbrt(Math.random());
|
||
x = r * Math.sin(phi) * Math.cos(theta);
|
||
y = r * Math.sin(phi) * Math.sin(theta);
|
||
z = r * Math.cos(phi);
|
||
} else {
|
||
// Ring
|
||
const r = 8 + Math.random() * 4;
|
||
const ang = Math.random() * Math.PI * 2;
|
||
x = r * Math.cos(ang);
|
||
y = (Math.random()-0.5) * 0.5;
|
||
z = r * Math.sin(ang);
|
||
}
|
||
break;
|
||
|
||
case 'LANTERN': // Ellipsoid
|
||
const lr = 6;
|
||
x = lr * Math.sin(phi) * Math.cos(theta);
|
||
y = (lr * 1.5) * Math.sin(phi) * Math.sin(theta); // taller
|
||
z = lr * Math.cos(phi);
|
||
break;
|
||
|
||
case 'TREE': // Cone + Spiral
|
||
const h = 20 * Math.random(); // 0 to 20
|
||
const radius = (20 - h) * 0.4;
|
||
const angle = Math.random() * Math.PI * 2;
|
||
y = h - 10;
|
||
x = radius * Math.cos(angle);
|
||
z = radius * Math.sin(angle);
|
||
break;
|
||
|
||
default: // Sphere fallback
|
||
const r = 10 * Math.cbrt(Math.random());
|
||
x = r * Math.sin(phi) * Math.cos(theta);
|
||
y = r * Math.sin(phi) * Math.sin(theta);
|
||
z = r * Math.cos(phi);
|
||
}
|
||
|
||
this.targets[i3] = x;
|
||
this.targets[i3+1] = y;
|
||
this.targets[i3+2] = z;
|
||
}
|
||
}
|
||
|
||
animate(delta) {
|
||
const positions = this.geometry.attributes.position.array;
|
||
|
||
// Physics Parameters
|
||
const springStrength = 0.05 * this.state.timeScale;
|
||
const friction = 0.90 + (0.05 * (1-this.state.timeScale)); // Higher friction when slow
|
||
const mouseForce = 50.0;
|
||
|
||
for(let i=0; i<this.count; i++) {
|
||
const i3 = i*3;
|
||
|
||
// 1. Attraction to Shape Target
|
||
if (this.state.mode === 'AGGREGATE') {
|
||
const ax = (this.targets[i3] - positions[i3]) * springStrength;
|
||
const ay = (this.targets[i3+1] - positions[i3+1]) * springStrength;
|
||
const az = (this.targets[i3+2] - positions[i3+2]) * springStrength;
|
||
|
||
this.velocities[i3] += ax;
|
||
this.velocities[i3+1] += ay;
|
||
this.velocities[i3+2] += az;
|
||
} else {
|
||
// Scatter mode: drifting
|
||
this.velocities[i3] += (Math.random()-0.5) * 0.01;
|
||
this.velocities[i3+1] += (Math.random()-0.5) * 0.01;
|
||
this.velocities[i3+2] += (Math.random()-0.5) * 0.01;
|
||
}
|
||
|
||
// 2. Interactive Force (Mouse/Hand)
|
||
if (this.state.repel || this.state.attract) {
|
||
const dx = positions[i3] - this.state.mouse.x;
|
||
const dy = positions[i3+1] - this.state.mouse.y;
|
||
const dz = positions[i3+2] - this.state.mouse.z; // Assumes 2D interaction plane mostly
|
||
|
||
const distSq = dx*dx + dy*dy;
|
||
const dist = Math.sqrt(distSq);
|
||
|
||
if (dist < 10) {
|
||
const f = (10 - dist) / 10; // 0 to 1
|
||
const dir = this.state.repel ? 1 : -1;
|
||
|
||
this.velocities[i3] += (dx / dist) * f * mouseForce * 0.01 * dir;
|
||
this.velocities[i3+1] += (dy / dist) * f * mouseForce * 0.01 * dir;
|
||
}
|
||
}
|
||
|
||
// 3. Update Position
|
||
positions[i3] += this.velocities[i3] * this.state.timeScale;
|
||
positions[i3+1] += this.velocities[i3+1] * this.state.timeScale;
|
||
positions[i3+2] += this.velocities[i3+2] * this.state.timeScale;
|
||
|
||
// 4. Apply Friction
|
||
this.velocities[i3] *= friction;
|
||
this.velocities[i3+1] *= friction;
|
||
this.velocities[i3+2] *= friction;
|
||
}
|
||
|
||
this.geometry.attributes.position.needsUpdate = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 3. GESTURE CONTROLLER (指令解析)
|
||
* ============================================================================
|
||
*/
|
||
class GestureController {
|
||
constructor(videoElement, onCommand) {
|
||
this.video = videoElement;
|
||
this.onCommand = onCommand; // Callback function
|
||
this.hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
|
||
|
||
this.hands.setOptions({
|
||
maxNumHands: 2,
|
||
modelComplexity: 1,
|
||
minDetectionConfidence: 0.7,
|
||
minTrackingConfidence: 0.7
|
||
});
|
||
|
||
this.hands.onResults(this.onResults.bind(this));
|
||
|
||
// Hand smoothness
|
||
this.handL = { x:0, y:0, z:0 };
|
||
this.handR = { x:0, y:0, z:0 };
|
||
}
|
||
|
||
start() {
|
||
const camera = new Camera(this.video, {
|
||
onFrame: async () => {
|
||
await this.hands.send({image: this.video});
|
||
},
|
||
width: 640,
|
||
height: 480
|
||
});
|
||
camera.start();
|
||
}
|
||
|
||
onResults(results) {
|
||
const landmarks = results.multiHandLandmarks;
|
||
const statusEl = document.getElementById('system-status');
|
||
|
||
if (!landmarks || landmarks.length === 0) {
|
||
statusEl.innerText = "NO HANDS DETECTED";
|
||
this.onCommand({ type: 'IDLE' });
|
||
return;
|
||
}
|
||
|
||
statusEl.innerText = `HANDS LINKED: ${landmarks.length}`;
|
||
|
||
// 1. Process Coordinates (Mirror & Norm to World)
|
||
// Assume z is 0 for interaction plane
|
||
const process = (lm) => ({
|
||
// Mirror X: (1 - x)
|
||
x: (1.0 - lm.x) * 30 - 15, // Map 0..1 to -15..15 World Units
|
||
y: -(lm.y * 20 - 10), // Map 0..1 to -10..10
|
||
z: 0
|
||
});
|
||
|
||
const l1 = landmarks[0];
|
||
const p1 = process(l1[9]); // Middle finger MCP as center
|
||
|
||
// Smoothing
|
||
this.handL.x += (p1.x - this.handL.x) * 0.2;
|
||
this.handL.y += (p1.y - this.handL.y) * 0.2;
|
||
|
||
let cmd = { type: 'MOVE', pos: this.handL, hands: 1 };
|
||
|
||
if (landmarks.length === 2) {
|
||
const l2 = landmarks[1];
|
||
const p2 = process(l2[9]);
|
||
|
||
this.handR.x += (p2.x - this.handR.x) * 0.2;
|
||
this.handR.y += (p2.y - this.handR.y) * 0.2;
|
||
|
||
// Calculate Distance
|
||
const dx = this.handL.x - this.handR.x;
|
||
const dy = this.handL.y - this.handR.y;
|
||
const dist = Math.sqrt(dx*dx + dy*dy);
|
||
|
||
// NAMASTE Detection (Hands very close)
|
||
if (dist < 3.0) {
|
||
cmd = { type: 'NAMASTE', pos: {x:(this.handL.x+this.handR.x)/2, y:(this.handL.y+this.handR.y)/2} };
|
||
}
|
||
// BULLET TIME (Hands far apart)
|
||
else if (dist > 15.0) {
|
||
cmd = { type: 'SPREAD', dist: dist };
|
||
}
|
||
else {
|
||
cmd = { type: 'DUAL_MOVE', p1: this.handL, p2: this.handR };
|
||
}
|
||
}
|
||
else {
|
||
// PINCH Detection (Thumb tip close to Index tip)
|
||
const thumb = l1[4];
|
||
const index = l1[8];
|
||
const dPinch = Math.hypot(thumb.x - index.x, thumb.y - index.y);
|
||
if (dPinch < 0.05) {
|
||
cmd.type = 'PINCH';
|
||
}
|
||
}
|
||
|
||
this.onCommand(cmd);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 4. MAIN APP (组装)
|
||
* ============================================================================
|
||
*/
|
||
class App {
|
||
constructor() {
|
||
this.container = document.getElementById('canvas-container');
|
||
this.scene = new THREE.Scene();
|
||
this.scene.fog = new THREE.FogExp2(0x000000, 0.02);
|
||
|
||
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||
this.camera.position.z = 25;
|
||
|
||
this.renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
|
||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
this.container.appendChild(this.renderer.domElement);
|
||
|
||
// Post Processing (Bloom)
|
||
this.composer = new THREE.EffectComposer(this.renderer);
|
||
this.composer.addPass(new THREE.RenderPass(this.scene, this.camera));
|
||
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
|
||
bloomPass.threshold = 0;
|
||
bloomPass.strength = 1.2; // Romantic Glow
|
||
bloomPass.radius = 0.5;
|
||
this.composer.addPass(bloomPass);
|
||
|
||
// Modules
|
||
this.themeManager = new ThemeManager();
|
||
this.particles = new ParticleEngine(this.scene);
|
||
this.particles.updateColors(this.themeManager.currentTheme);
|
||
this.particles.setShape(this.themeManager.currentTheme.shape);
|
||
|
||
// Resize
|
||
window.addEventListener('resize', () => {
|
||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||
this.camera.updateProjectionMatrix();
|
||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
this.composer.setSize(window.innerWidth, window.innerHeight);
|
||
});
|
||
|
||
// Initialize Gesture
|
||
const video = document.getElementById('input-video');
|
||
this.gestureCtrl = new GestureController(video, (cmd) => this.handleCommand(cmd));
|
||
|
||
// Hide Loader
|
||
setTimeout(() => {
|
||
this.gestureCtrl.start();
|
||
document.getElementById('loader').style.opacity = 0;
|
||
setTimeout(()=> document.getElementById('loader').remove(), 1000);
|
||
}, 1500);
|
||
|
||
this.animate();
|
||
}
|
||
|
||
handleCommand(cmd) {
|
||
const p = this.particles;
|
||
const hint = document.getElementById('gesture-hint');
|
||
|
||
// Reset frame state
|
||
p.state.repel = false;
|
||
p.state.attract = false;
|
||
p.state.timeScale = 1.0;
|
||
|
||
switch(cmd.type) {
|
||
case 'IDLE':
|
||
// Auto scatter slowly
|
||
if(p.state.mode !== 'SCATTER') p.state.mode = 'SCATTER';
|
||
hint.innerText = "双手合十 · 唤醒记忆";
|
||
break;
|
||
|
||
case 'MOVE':
|
||
// One hand repel (brushing through stars)
|
||
p.state.mode = 'SCATTER';
|
||
p.state.repel = true;
|
||
p.state.mouse.set(cmd.pos.x, cmd.pos.y, 0);
|
||
hint.innerText = "单手 · 拨动星尘";
|
||
break;
|
||
|
||
case 'PINCH':
|
||
// Gravity Well
|
||
p.state.mode = 'SCATTER';
|
||
p.state.attract = true;
|
||
p.state.mouse.set(cmd.pos.x, cmd.pos.y, 0);
|
||
hint.innerText = "捏合 · 捕获引力";
|
||
break;
|
||
|
||
case 'NAMASTE':
|
||
// Aggregate to Shape
|
||
p.state.mode = 'AGGREGATE';
|
||
hint.innerText = "合十 · 凝聚形态";
|
||
break;
|
||
|
||
case 'SPREAD':
|
||
// Bullet time
|
||
p.state.mode = 'SCATTER';
|
||
p.state.timeScale = 0.1; // Slow motion
|
||
hint.innerText = "张开 · 凝固时间";
|
||
break;
|
||
|
||
case 'DUAL_MOVE':
|
||
p.state.mode = 'SCATTER';
|
||
p.state.repel = true; // Two repulsion points logic omitted for brevity, using mouse 1
|
||
p.state.mouse.set(cmd.p1.x, cmd.p1.y, 0);
|
||
break;
|
||
}
|
||
}
|
||
|
||
animate() {
|
||
requestAnimationFrame(this.animate.bind(this));
|
||
|
||
// Rotate Scene slowly
|
||
this.scene.rotation.y += 0.001 * this.particles.state.timeScale;
|
||
|
||
this.particles.animate();
|
||
this.composer.render();
|
||
}
|
||
}
|
||
|
||
// Start
|
||
new App();
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|