Files
home/me.html
hehh a0e911421f feat(tracking): 集成新的统计追踪服务
- 在 about.html 中添加 vercount 统计脚本
- 在 christmas.html 中添加 vercount 统计脚本
- 在 index.html 中替换原有的分析服务为 vercount 统计脚本
- 在 me.html 中添加 vercount 统计脚本
- 移除旧的 analysee 统计服务相关代码
- 统一使用 defer 属性优化脚本加载性能
2026-01-10 18:28:17 +08:00

621 lines
25 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>ETHEREAL | HAND PHYSICS</title>
<style>
:root {
--bg-color: #020202;
--ui-font: 'Helvetica Neue', 'Arial', sans-serif;
--serif-font: 'Times New Roman', serif;
}
body {
margin: 0;
overflow: hidden;
background-color: var(--bg-color);
font-family: var(--ui-font);
cursor: none; /* 隐藏鼠标 */
}
#canvas-container {
position: fixed;
inset: 0;
z-index: 1;
}
#input-video { display: none; }
/* 极简主义 UI */
#ui-layer {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px;
mix-blend-mode: exclusion; /* 高级混合模式 */
}
.top-bar {
display: flex;
justify-content: space-between;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 2px;
color: #fff;
opacity: 0.7;
}
.center-stage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 100%;
}
.art-title {
font-family: var(--serif-font);
font-size: 48px;
font-weight: 100;
letter-spacing: 12px;
color: #fff;
opacity: 0;
transition: opacity 2s ease;
}
.art-sub {
font-size: 10px;
letter-spacing: 6px;
color: #fff;
margin-top: 15px;
opacity: 0;
transition: opacity 2s 0.5s ease;
}
.visible { opacity: 1 !important; }
.debug-info {
position: absolute;
bottom: 40px;
left: 40px;
font-family: monospace;
font-size: 10px;
color: rgba(255,255,255,0.4);
line-height: 1.5;
}
/* 加载器 */
#loader {
position: fixed;
inset: 0;
background: #000;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 1s;
}
.loader-line {
width: 0%;
height: 1px;
background: #fff;
transition: width 0.5s;
}
</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/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>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<!-- AI 视觉库 -->
<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-line" id="loader-bar"></div></div>
<div id="canvas-container"></div>
<video id="input-video" playsinline></video>
<div id="ui-layer">
<div class="top-bar">
<span id="fps-display">FPS: --</span>
<span id="theme-display">CALIBRATING REALITY...</span>
</div>
<div class="center-stage">
<div class="art-title" id="title">VOID</div>
<div class="art-sub" id="subtitle">INTERACTIVE FLUID DYNAMICS</div>
</div>
<div class="debug-info" id="hand-state">
HANDS: SEARCHING<br>
FORCE: 0.00
</div>
</div>
<script>
/**
* ============================================================================
* 1. 数学工具与配置 (Math & Config)
* ============================================================================
*/
const CONFIG = {
particleCount: 8000, // 粒子数量,适中以保证物理计算性能
particleSize: 1.8, // 基础大小
bloomStrength: 1.2, // 辉光强度
handForceRadius: 15.0, // 手掌影响半径
friction: 0.96, // 物理摩擦力 (越小停得越快)
returnSpeed: 0.008, // 回归原位的速度 (弹性)
noiseScale: 0.02, // 噪声纹理缩放
curlStrength: 0.5 // 旋度强度 (流体感)
};
const Simplex = new SimplexNoise();
// 颜色主题定义 (Palette)
const THEMES = {
DEFAULT: { name: 'VOID / 虚空', colors: [0x888888, 0xffffff, 0x444444] },
SPRING: { name: 'FLORA / 生机', colors: [0xff3366, 0xffdd00, 0x00ff88] },
SUMMER: { name: 'OCEAN / 碧海', colors: [0x00ffff, 0x0066ff, 0xffffff] },
AUTUMN: { name: 'EMBER / 余烬', colors: [0xff4400, 0xffaa00, 0x330000] },
WINTER: { name: 'FROST / 霜雪', colors: [0xaaccff, 0xffffff, 0x8899aa] },
LOVE: { name: 'PULSE / 悸动', colors: [0xff0044, 0xff88aa, 0x440011] },
SPIRIT: { name: 'SOUL / 灵光', colors: [0x00ffcc, 0xaa00ff, 0x0000ff] }
};
// 自动主题选择器
function getTheme() {
const m = new Date().getMonth() + 1;
const d = new Date().getDate();
if (m===2 && d===14) return THEMES.LOVE;
if (m===10 && d===31) return THEMES.SPIRIT;
if (m>=3 && m<=5) return THEMES.SPRING;
if (m>=6 && m<=8) return THEMES.SUMMER;
if (m>=9 && m<=11) return THEMES.AUTUMN;
return THEMES.WINTER;
}
const ACTIVE_THEME = getTheme();
/**
* ============================================================================
* 2. 物理核心 (Physics Core) - 重写为基于力的系统
* ============================================================================
*/
class PhysicsSystem {
constructor(scene) {
this.count = CONFIG.particleCount;
this.geometry = new THREE.BufferGeometry();
// 双重缓冲数据Current(当前), Target(回归目标), Velocity(速度)
this.positions = new Float32Array(this.count * 3);
this.origins = new Float32Array(this.count * 3); // 原始位置(用于回归)
this.velocities = new Float32Array(this.count * 3);
this.colors = new Float32Array(this.count * 3);
this.sizes = new Float32Array(this.count);
this.life = new Float32Array(this.count); // 粒子生命周期/闪烁偏移
this.initParticles();
// ShaderMaterial 提供高性能渲染和柔和的光点
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pixelRatio: { value: window.devicePixelRatio },
baseSize: { value: CONFIG.particleSize }
},
vertexShader: `
attribute float size;
attribute vec3 color;
attribute float life;
varying vec3 vColor;
varying float vAlpha;
uniform float time;
uniform float pixelRatio;
uniform float baseSize;
void main() {
vColor = color;
// 粒子呼吸效果
float breath = 0.6 + 0.4 * sin(time * 2.0 + life * 10.0);
vAlpha = 0.5 + 0.5 * breath;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = baseSize * size * breath * pixelRatio * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
// 圆形绘制,边缘羽化
vec2 coord = gl_PointCoord - vec2(0.5);
float r = length(coord);
if (r > 0.5) discard;
// 核心亮,边缘暗
float glow = 1.0 - (r * 2.0);
glow = pow(glow, 1.5);
gl_FragColor = vec4(vColor, glow * vAlpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
this.mesh = new THREE.Points(this.geometry, this.material);
scene.add(this.mesh);
}
initParticles() {
// 创建一个无序但均匀的云团
const c1 = new THREE.Color(ACTIVE_THEME.colors[0]);
const c2 = new THREE.Color(ACTIVE_THEME.colors[1]);
const c3 = new THREE.Color(ACTIVE_THEME.colors[2]);
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
// 随机分布在一个宽阔的区域
const x = (Math.random() - 0.5) * 200;
const y = (Math.random() - 0.5) * 120;
const z = (Math.random() - 0.5) * 80;
this.positions[i3] = x; this.positions[i3+1] = y; this.positions[i3+2] = z;
this.origins[i3] = x; this.origins[i3+1] = y; this.origins[i3+2] = z;
// 颜色混合
const rand = Math.random();
let c;
if(rand < 0.33) c = c1;
else if(rand < 0.66) c = c2;
else c = c3;
this.colors[i3] = c.r; this.colors[i3+1] = c.g; this.colors[i3+2] = c.b;
this.sizes[i] = Math.random() * 1.5 + 0.5;
this.life[i] = Math.random();
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
this.geometry.setAttribute('life', new THREE.BufferAttribute(this.life, 1));
}
// ★★★ 核心物理更新逻辑 ★★★
update(time, handData) {
this.material.uniforms.time.value = time;
// 获取手部数据 (如果没有手,数组为空)
const hands = handData.hands || [];
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
const px = this.positions[i3];
const py = this.positions[i3+1];
const pz = this.positions[i3+2];
// 1. Curl Noise (旋度噪声) - 让粒子自然流动的关键
// 计算噪声场对速度的影响,模拟空气流动
const noiseScale = 0.015;
const n1 = Simplex.noise3D(px*noiseScale, py*noiseScale, time*0.2);
const n2 = Simplex.noise3D(px*noiseScale + 100, py*noiseScale, time*0.2);
const n3 = Simplex.noise3D(px*noiseScale + 200, py*noiseScale, time*0.2);
// 施加环境流动力
this.velocities[i3] += n1 * 0.02;
this.velocities[i3+1] += n2 * 0.02;
this.velocities[i3+2] += n3 * 0.02;
// 2. 弹性回归力 (Elasticity) - 让粒子即使被扰动也能慢慢飘回原位
// 除非被手“抓”住,否则它们有自己的家
const ox = this.origins[i3];
const oy = this.origins[i3+1];
const oz = this.origins[i3+2];
this.velocities[i3] += (ox - px) * CONFIG.returnSpeed;
this.velocities[i3+1] += (oy - py) * CONFIG.returnSpeed;
this.velocities[i3+2] += (oz - pz) * CONFIG.returnSpeed;
// 3. ★★★ 高级手势交互场 (Hand Interaction Field) ★★★
// 支持任意手势、任意数量的手
for (let h = 0; h < hands.length; h++) {
const hand = hands[h];
// 3.1 掌心力场 (Palm Force)
// 如果手张开:产生基于法线的推力 (Push)
// 如果手握拳:产生引力 (Gravity Well)
const dPx = px - hand.palm.x;
const dPy = py - hand.palm.y;
const dPz = pz - hand.palm.z;
const distSq = dPx*dPx + dPy*dPy + dPz*dPz;
// 交互半径
if (distSq < 1500) {
const dist = Math.sqrt(distSq);
const forceFactor = (1500 - distSq) / 1500; // 0 (边缘) -> 1 (中心)
if (hand.isFist) {
// 握拳:黑洞引力,且带有强烈的旋转
// 吸力
this.velocities[i3] -= dPx * 0.1 * forceFactor;
this.velocities[i3+1] -= dPy * 0.1 * forceFactor;
this.velocities[i3+2] -= dPz * 0.1 * forceFactor;
// 旋转力 (Cross Product with Up vector)
this.velocities[i3] += -dPy * 0.2 * forceFactor;
this.velocities[i3+1] += dPx * 0.2 * forceFactor;
} else {
// 张开:基于手掌法线方向的推力 (空气炮)
// 计算点乘,判断粒子是否在手掌前方
// 简单模拟:径向推开 + 手掌朝向的定向风
const pushStrength = 2.0 * hand.velocity; // 动得越快,风越大
this.velocities[i3] += hand.normal.x * pushStrength * forceFactor;
this.velocities[i3+1] += hand.normal.y * pushStrength * forceFactor;
this.velocities[i3+2] += hand.normal.z * pushStrength * forceFactor;
// 基础斥力 (防止穿模)
if (dist < 10) {
this.velocities[i3] += dPx * 0.5 * forceFactor;
this.velocities[i3+1] += dPy * 0.5 * forceFactor;
this.velocities[i3+2] += dPz * 0.5 * forceFactor;
}
}
}
// 3.2 指尖轨迹 (Finger Trails)
// 每一个指尖都是一个微小的扰动源,允许精细作画
for (let f = 0; f < 5; f++) {
const tip = hand.fingertips[f];
const dtx = px - tip.x;
const dty = py - tip.y;
const dtz = pz - tip.z;
const tipDistSq = dtx*dtx + dty*dty + dtz*dtz;
if (tipDistSq < 100) {
// 指尖产生湍流
this.velocities[i3] += (Math.random()-0.5) * 1.5;
this.velocities[i3+1] += (Math.random()-0.5) * 1.5;
this.velocities[i3+2] += (Math.random()-0.5) * 1.5;
}
}
}
// 4. 积分与阻尼
this.velocities[i3] *= CONFIG.friction;
this.velocities[i3+1] *= CONFIG.friction;
this.velocities[i3+2] *= CONFIG.friction;
this.positions[i3] += this.velocities[i3];
this.positions[i3+1] += this.velocities[i3+1];
this.positions[i3+2] += this.velocities[i3+2];
}
this.geometry.attributes.position.needsUpdate = true;
}
}
/**
* ============================================================================
* 3. 智能感知层 (Intelligent Perception)
* ============================================================================
*/
class HandInterface {
constructor() {
this.handData = { hands: [] };
// MediaPipe 初始化
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.processResults.bind(this));
// 摄像头启动
const video = document.getElementById('input-video');
const camera = new Camera(video, {
onFrame: async () => { await this.hands.send({image: video}); },
width: 640, height: 480
});
camera.start().then(() => {
// UI 更新
document.getElementById('loader-bar').style.width = '100%';
setTimeout(() => document.getElementById('loader').style.opacity = 0, 500);
setTimeout(() => {
document.getElementById('title').classList.add('visible');
document.getElementById('subtitle').classList.add('visible');
document.getElementById('loader').style.display = 'none';
}, 1000);
});
// 上一帧数据用于计算速度
this.prevHands = {};
}
processResults(results) {
const landmarks = results.multiHandLandmarks;
const worldLandmarks = results.multiHandWorldLandmarks; // 使用真实世界坐标计算法线
const currentHands = [];
const debugEl = document.getElementById('hand-state');
if (landmarks) {
landmarks.forEach((lm, index) => {
// 1. 坐标映射 (2D -> 3D Scene Space)
// 屏幕中心为 (0,0), 范围约 -60 到 60
const palmCenter = lm[9]; // 中指根部作为手掌中心
const pos = new THREE.Vector3(
(1.0 - palmCenter.x) * 140 - 70, // X 镜像翻转
-(palmCenter.y * 100 - 50), // Y 翻转
0 // Z 暂定为0后续可根据 lm.z 优化深度
);
// 2. 计算速度 (Velocity)
let velocity = 0;
if (this.prevHands[index]) {
const dist = pos.distanceTo(this.prevHands[index]);
velocity = Math.min(dist, 5.0); // 限制最大速度
}
// 3. 计算手势状态 (State Analysis)
// 3.1 握拳检测: 比较指尖(tip)到掌心(wrist/center)的距离
const wrist = lm[0];
let foldedFingers = 0;
const tips = [8, 12, 16, 20]; // 4 fingers (excluding thumb)
tips.forEach(t => {
// 简单判断: 如果指尖y坐标 低于 指根y坐标 (屏幕空间),或距离手腕太近
// 更稳健的方法是3D距离
const dTip = Math.hypot(lm[t].x - wrist.x, lm[t].y - wrist.y);
const dPip = Math.hypot(lm[t-2].x - wrist.x, lm[t-2].y - wrist.y);
if (dTip < dPip) foldedFingers++;
});
const isFist = foldedFingers >= 3;
// 3.2 手掌法线 (Normal Vector)
// 利用 P0(Wrist), P5(IndexBase), P17(PinkyBase) 计算平面法线
// 这里做一个简化估算:根据手掌移动方向和手腕-中指向量
const pWrist = new THREE.Vector3((1-lm[0].x), -lm[0].y, 0);
const pMiddle = new THREE.Vector3((1-lm[9].x), -lm[9].y, 0);
const dir = new THREE.Vector3().subVectors(pMiddle, pWrist).normalize();
// 默认法线朝向屏幕外 (0,0,1),结合方向旋转
const normal = new THREE.Vector3(0, 0, 1).add(dir.multiplyScalar(0.5)).normalize();
// 4. 收集指尖位置 (用于精细交互)
const fingertips = [4, 8, 12, 16, 20].map(i => {
return new THREE.Vector3(
(1.0 - lm[i].x) * 140 - 70,
-(lm[i].y * 100 - 50),
(lm[i].z || 0) * 50 // 尝试引入深度
);
});
currentHands.push({
id: index,
palm: pos,
velocity: velocity,
isFist: isFist,
normal: normal,
fingertips: fingertips
});
// 更新上一帧
this.prevHands[index] = pos.clone();
});
}
this.handData.hands = currentHands;
// 更新 UI Debug
if (currentHands.length > 0) {
const h = currentHands[0];
const stateStr = h.isFist ? "GRAVITY WELL (FIST)" : "WIND FORCE (OPEN)";
debugEl.innerHTML = `HANDS: DETECTED (${currentHands.length})<br>MODE: ${stateStr}<br>VEL: ${h.velocity.toFixed(2)}`;
} else {
debugEl.innerHTML = `HANDS: SEARCHING...<br>Please show your hands`;
}
}
}
/**
* ============================================================================
* 4. 渲染循环 (Main Loop)
* ============================================================================
*/
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.01);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 60; // 摄像机拉远,看到更多粒子
const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Post Processing (Bloom)
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
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);
// Init Systems
const physics = new PhysicsSystem(scene);
const perception = new HandInterface();
// Update UI Text
document.getElementById('theme-display').innerText = `THEME: ${ACTIVE_THEME.name}`;
document.getElementById('title').innerText = ACTIVE_THEME.name.split('/')[0];
// Animation Loop
const clock = new THREE.Clock();
let frames = 0;
let lastTime = 0;
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
const delta = clock.getDelta();
// FPS Counter
frames++;
if (time - lastTime >= 1) {
document.getElementById('fps-display').innerText = `FPS: ${frames}`;
frames = 0;
lastTime = time;
}
// Update Physics
physics.update(time, perception.handData);
// Subtle Camera Move
camera.position.x = Math.sin(time * 0.1) * 2;
camera.position.y = Math.cos(time * 0.1) * 2;
camera.lookAt(0,0,0);
composer.render();
}
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
<script defer src="https://events.vercount.one/js"></script>
</body>
</html>