Files
home/me.html
hehh 31b7d72123 refactor(ui): 重构界面布局与主题系统
- 移除旧版加载屏与叙事层DOM结构
- 新增基于Three.js的粒子系统引擎
- 实现动态主题检测与切换功能
- 更新CSS变量系统以支持RGB颜色模式
- 优化UI层布局结构提升响应式体验
- 添加节庆主题自动识别逻辑
- 简化HTML结构并增强语义化程度
- 调整字体与色彩配置提高可读性
- 引入新的动画呼吸效果与脉冲效果
- 重构JavaScript模块提升代码组织性
2025-12-12 12:27:20 +08:00

732 lines
28 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 {
--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>