Files
home/me.html
hehh 75231ee73a refactor(ui): 重构界面与粒子系统提升视觉体验
- 更新标题为 FLUID ARCHIVE 并调整字体与背景色
- 简化 UI 布局,增强沉浸感并隐藏鼠标指针
- 优化加载动画为旋转圆环,改善过渡效果
- 粒子系统重写:减少数量、引入着色器材质与辉光效果
- 改进粒子物理行为,支持跟随、聚合与自由漂浮模式
- 更新主题配置逻辑,简化节气与节日判断规则
- 修复手势控制响应延迟与丢失问题,增强交互反馈
- 调整摄像机视角与后处理参数以增强空间氛围感
2025-12-12 12:27:36 +08:00

613 lines
23 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 | FLUID ARCHIVE</title>
<style>
:root {
--font-serif: 'Times New Roman', serif;
--font-sans: 'Arial', sans-serif;
--ui-color: 255, 255, 255;
}
body {
margin: 0;
overflow: hidden;
background-color: #000000;
color: rgb(var(--ui-color));
font-family: var(--font-sans);
user-select: none;
cursor: none; /* 隐藏鼠标,增加沉浸感 */
}
#canvas-container {
position: fixed;
inset: 0;
z-index: 1;
}
#input-video {
display: none; /* 隐藏原始视频流 */
}
/* UI HUD */
#ui-layer {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px;
background: radial-gradient(circle at center, transparent 20%, rgba(0,0,0,0.8) 100%);
}
.header {
display: flex;
justify-content: space-between;
font-size: 10px;
letter-spacing: 2px;
opacity: 0.6;
text-transform: uppercase;
}
.center-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 100%;
}
.n-title {
font-family: var(--font-serif);
font-size: clamp(30px, 4vw, 80px);
letter-spacing: 10px;
font-weight: 300;
text-shadow: 0 0 20px rgba(255,255,255,0.3);
margin-bottom: 10px;
transition: opacity 1s;
}
.n-sub {
font-size: 12px;
letter-spacing: 4px;
opacity: 0.8;
}
.footer {
text-align: center;
font-size: 12px;
letter-spacing: 2px;
opacity: 0.5;
animation: breathe 3s infinite ease-in-out;
}
/* 加载动画 */
#loader {
position: fixed;
inset: 0;
background: #000;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 0.8s;
}
.loader-ring {
width: 40px;
height: 40px;
border: 1px solid rgba(255,255,255,0.3);
border-top: 1px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes breathe { 0%,100% { opacity: 0.3; } 50% { opacity: 0.8; } }
@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/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/@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-ring"></div></div>
<div id="canvas-container"></div>
<video id="input-video" playsinline></video>
<div id="ui-layer">
<div class="header">
<span id="theme-display">DETECTING TIME...</span>
<span id="hand-status">WAITING INPUT</span>
</div>
<div class="center-text">
<div class="n-title" id="main-title">HONESTY</div>
<div class="n-sub" id="sub-title">FLUID MEMORY</div>
</div>
<div class="footer" id="hint-text">
单手·流体跟随 | 合十·凝聚形态
</div>
</div>
<script>
/**
* ============================================================================
* 1. 节日与主题配置 (Theme Config)
* ============================================================================
*/
const Themes = {
DEFAULT: { name: 'Saturn / 深空', c1: [0.1, 0.5, 1.0], c2: [0.8, 0.9, 1.0], shape: 'SATURN' }, // 蓝白
SPRING: { name: 'Spring / 春笺', c1: [1.0, 0.0, 0.2], c2: [1.0, 0.8, 0.0], shape: 'LANTERN' }, // 红金
VALENTINE: { name: 'Love / 心语', c1: [1.0, 0.2, 0.6], c2: [0.8, 0.0, 0.8], shape: 'HEART' }, // 粉紫
QINGMING: { name: 'Rain / 雨巷', c1: [0.0, 0.3, 0.2], c2: [0.6, 0.7, 0.7], shape: 'WILLOW' }, // 青灰
MIDAUTUMN: { name: 'Moon / 月笺', c1: [0.0, 0.1, 0.3], c2: [1.0, 0.9, 0.6], shape: 'MOON' }, // 深蓝金
NATIONAL: { name: 'Glory / 山河', c1: [0.9, 0.1, 0.0], c2: [1.0, 1.0, 0.0], shape: 'STAR' }, // 红黄
HALLOWEEN: { name: 'Night / 夜宴', c1: [1.0, 0.5, 0.0], c2: [0.4, 0.0, 0.6], shape: 'PUMPKIN' }, // 橙紫
GHOST: { name: 'River / 幽思', c1: [0.0, 0.8, 0.5], c2: [0.1, 0.9, 0.8], shape: 'LOTUS' }, // 荧光绿
CHRISTMAS: { name: 'Snow / 雪颂', c1: [0.0, 0.4, 0.1], c2: [0.8, 0.1, 0.1], shape: 'TREE' } // 绿红
};
// 简单的日期检测算法
function getThemeByDate() {
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
// 逻辑:如果在特定日期范围内,返回对应主题,否则返回 DEFAULT
if (m === 2 && d > 10 && d < 18) return Themes.VALENTINE;
if (m === 2 && d < 10) return Themes.SPRING; // 假设春节
if (m === 4 && d < 10) return Themes.QINGMING;
if (m === 9 && d > 10 && d < 20) return Themes.MIDAUTUMN;
if (m === 10 && d < 8) return Themes.NATIONAL;
if (m === 10 && d > 25) return Themes.HALLOWEEN;
if (m === 8 && d > 10 && d < 20) return Themes.GHOST;
if (m === 12 && d > 15) return Themes.CHRISTMAS;
return Themes.DEFAULT;
}
const currentTheme = getThemeByDate();
// 更新UI
document.getElementById('theme-display').innerText = `THEME: ${currentTheme.name}`;
/**
* ============================================================================
* 2. 粒子物理引擎 (Physics Engine)
* ============================================================================
*/
class ParticleSystem {
constructor(scene) {
this.scene = scene;
this.count = 6000; // 优化:减少数量,不再那么密集
this.geometry = new THREE.BufferGeometry();
this.positions = new Float32Array(this.count * 3);
this.velocities = new Float32Array(this.count * 3);
this.targets = new Float32Array(this.count * 3); // 聚合时的目标位置
this.colors = new Float32Array(this.count * 3);
this.sizes = new Float32Array(this.count);
this.randoms = new Float32Array(this.count * 3); // 用于噪点运动
// 状态管理
this.state = {
mode: 'SCATTER', // SCATTER(散开/漂浮), FOLLOW(跟随), AGGREGATE(聚合)
handPos: new THREE.Vector3(0, 0, 0),
scatterForce: 0.95, // 摩擦力
speed: 1.0
};
this.initParticles();
this.createShaderMaterial();
this.mesh = new THREE.Points(this.geometry, this.material);
this.scene.add(this.mesh);
// 预计算形状
this.calculateShape(currentTheme.shape);
}
initParticles() {
const color1 = new THREE.Color().setRGB(...currentTheme.c1);
const color2 = new THREE.Color().setRGB(...currentTheme.c2);
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
// 初始随机分布 (宽屏分布)
this.positions[i3] = (Math.random() - 0.5) * 60;
this.positions[i3+1] = (Math.random() - 0.5) * 40;
this.positions[i3+2] = (Math.random() - 0.5) * 30;
// 随机颜色混合
const mixRatio = Math.random();
this.colors[i3] = color1.r * mixRatio + color2.r * (1-mixRatio);
this.colors[i3+1] = color1.g * mixRatio + color2.g * (1-mixRatio);
this.colors[i3+2] = color1.b * mixRatio + color2.b * (1-mixRatio);
// 随机大小 (让画面更有层次)
this.sizes[i] = Math.random() * 1.5 + 0.5;
// 随机参数
this.randoms[i3] = Math.random();
this.randoms[i3+1] = Math.random();
this.randoms[i3+2] = 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));
}
// 解决“粒子是方块”的问题:使用 Shader 绘制圆形
createShaderMaterial() {
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pixelRatio: { value: window.devicePixelRatio }
},
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
uniform float pixelRatio;
uniform float time;
void main() {
vColor = color;
vec3 pos = position;
// 简单的呼吸效果
float breath = 1.0 + sin(time * 2.0 + position.x) * 0.1;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = size * 12.0 * pixelRatio * (20.0 / -mvPosition.z) * breath;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
// 绘制圆形
float r = distance(gl_PointCoord, vec2(0.5));
if (r > 0.5) discard;
// 径向渐变 (柔和边缘)
float glow = 1.0 - (r * 2.0);
glow = pow(glow, 2.0);
gl_FragColor = vec4(vColor, glow);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending // 叠加发光混合模式
});
}
// 计算特定主题的形状坐标
calculateShape(type) {
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
let x=0, y=0, z=0;
// 简单的形状数学生成
const u = Math.random() * Math.PI * 2;
const v = Math.random() * Math.PI;
if (type === 'SATURN') {
// 土星:球体 + 环
if (Math.random() > 0.4) {
const r = 5;
x = r * Math.sin(v) * Math.cos(u);
y = r * Math.sin(v) * Math.sin(u);
z = r * Math.cos(v);
} else {
const r = 8 + Math.random() * 3;
x = r * Math.cos(u);
z = r * Math.sin(u);
y = (Math.random()-0.5) * 0.5;
}
} else if (type === 'HEART') {
// 爱心方程
const t = Math.PI * (Math.random() - 0.5) * 2; // -PI to PI
// 3D Heart approximation
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 * 4;
x *= 0.4; y *= 0.4; z *= 0.4;
} else {
// 默认球体
const r = 7 * Math.sqrt(Math.random());
x = r * Math.sin(v) * Math.cos(u);
y = r * Math.sin(v) * Math.sin(u);
z = r * Math.cos(v);
}
this.targets[i3] = x;
this.targets[i3+1] = y;
this.targets[i3+2] = z;
}
}
update(time) {
this.material.uniforms.time.value = time;
const positions = this.geometry.attributes.position.array;
// 物理参数
const friction = this.state.scatterForce; // 阻力
const returnForce = 0.03; // 聚合时的吸力
const followForce = 0.08; // 跟随时的吸力
const noiseStrength = 0.05; // 漂浮时的扰动
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
// 1. 计算目标力
let ax = 0, ay = 0, az = 0;
if (this.state.mode === 'AGGREGATE') {
// 聚合:被吸向预定形状
ax = (this.targets[i3] - positions[i3]) * returnForce;
ay = (this.targets[i3+1] - positions[i3+1]) * returnForce;
az = (this.targets[i3+2] - positions[i3+2]) * returnForce;
}
else if (this.state.mode === 'FOLLOW') {
// 跟随:被吸向手部位置 (解决“排斥”问题)
// 增加一点随机偏移,让它们围绕手旋转而不是坍缩成一个点
const dx = this.state.handPos.x - positions[i3];
const dy = this.state.handPos.y - positions[i3+1];
const dz = this.state.handPos.z - positions[i3+2];
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
// 距离越近,吸力越弱,防止穿模
if (dist > 1.0) {
ax = dx * followForce * 0.5;
ay = dy * followForce * 0.5;
az = dz * followForce * 0.5;
}
// 添加螺旋运动 (Cross product模拟)
ax += (Math.random()-0.5) * 0.5;
ay += (Math.random()-0.5) * 0.5;
}
else {
// SCATTER / IDLE: 自由漂浮 + 噪点运动
// 回归到一个较大的随机区域,而不是无限飞走
const homeX = (this.randoms[i3]-0.5) * 80;
const homeY = (this.randoms[i3+1]-0.5) * 60;
// 缓慢的布朗运动
ax = (Math.sin(time + i) * noiseStrength) + (homeX - positions[i3]) * 0.002;
ay = (Math.cos(time + i) * noiseStrength) + (homeY - positions[i3+1]) * 0.002;
az = (Math.sin(time * 0.5 + i) * noiseStrength) - positions[i3+2] * 0.002;
}
// 2. 应用力到速度
this.velocities[i3] += ax;
this.velocities[i3+1] += ay;
this.velocities[i3+2] += az;
// 3. 应用阻力
this.velocities[i3] *= friction;
this.velocities[i3+1] *= friction;
this.velocities[i3+2] *= friction;
// 4. 更新位置
positions[i3] += this.velocities[i3] * this.state.speed;
positions[i3+1] += this.velocities[i3+1] * this.state.speed;
positions[i3+2] += this.velocities[i3+2] * this.state.speed;
}
this.geometry.attributes.position.needsUpdate = true;
}
// 状态切换方法
setState(mode, handPos = null) {
if (mode === 'SCATTER' && this.state.mode === 'AGGREGATE') {
// 解决问题4如果从聚合状态丢失手势给一个向外的“炸开”力
this.explode();
}
this.state.mode = mode;
if (handPos) {
this.state.handPos.copy(handPos);
}
}
explode() {
// 给所有粒子一个瞬间的随机向外速度
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
this.velocities[i3] += (Math.random() - 0.5) * 2.0;
this.velocities[i3+1] += (Math.random() - 0.5) * 2.0;
this.velocities[i3+2] += (Math.random() - 0.5) * 2.0;
}
}
}
/**
* ============================================================================
* 3. 程序主逻辑 (Main App)
* ============================================================================
*/
const scene = new THREE.Scene();
// 雾效增强空间感
scene.fog = new THREE.FogExp2(0x000000, 0.015);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 30;
const renderer = new THREE.WebGLRenderer({
antialias: false, // 后处理时不需要原生抗锯齿
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 后处理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 = 1.8; // 强辉光
bloomPass.radius = 0.8;
bloomPass.threshold = 0.1;
composer.addPass(bloomPass);
// 初始化粒子系统
const particles = new ParticleSystem(scene);
// 手势识别
const videoElement = document.getElementById('input-video');
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(onHandsResults);
const cameraUtils = new Camera(videoElement, {
onFrame: async () => {
await hands.send({image: videoElement});
},
width: 640,
height: 480
});
// 启动
setTimeout(() => {
cameraUtils.start();
document.getElementById('loader').style.opacity = '0';
setTimeout(()=>document.getElementById('loader').remove(), 800);
}, 1000);
// 手势逻辑处理
function onHandsResults(results) {
const landmarks = results.multiHandLandmarks;
const statusEl = document.getElementById('hand-status');
const hintEl = document.getElementById('hint-text');
if (!landmarks || landmarks.length === 0) {
statusEl.innerText = "NO SIGNAL";
// 解决问题4手势丢失自动散开
if (particles.state.mode !== 'SCATTER') {
particles.setState('SCATTER');
hintEl.innerText = "等待输入...";
}
return;
}
statusEl.innerText = "SIGNAL LINKED";
// 坐标映射:将归一化的 0-1 坐标映射到 3D 世界坐标 (-25 到 25)
// 注意x 需要镜像翻转 (1-x)
const mapHand = (lm) => {
return new THREE.Vector3(
(1.0 - lm.x) * 50 - 25,
-(lm.y * 30 - 15),
0
);
};
// 取第一只手做跟随
const hand1 = landmarks[0][9]; // 中指根部
const pos1 = mapHand(hand1);
if (landmarks.length === 2) {
// 双手逻辑
const hand2 = landmarks[1][9];
const pos2 = mapHand(hand2);
// 计算双手距离
const dist = pos1.distanceTo(pos2);
if (dist < 6.0) {
// 合十 -> 聚合
particles.setState('AGGREGATE');
hintEl.innerText = "正在凝聚记忆...";
} else if (dist > 25.0) {
// 张开 -> 子弹时间 (减速)
particles.setState('SCATTER');
particles.state.speed = 0.1;
hintEl.innerText = "时间凝固";
} else {
// 双手移动 -> 强力跟随
particles.setState('FOLLOW', pos1.lerp(pos2, 0.5)); // 跟随双手中心
particles.state.speed = 1.0;
hintEl.innerText = "双倍引力";
}
} else {
// 单手逻辑 -> 跟随 (解决问题3)
// 捏合检测
const thumb = landmarks[0][4];
const index = landmarks[0][8];
const pinchDist = Math.hypot(thumb.x - index.x, thumb.y - index.y);
if (pinchDist < 0.05) {
// 捏合 -> 极强吸力
particles.setState('FOLLOW', pos1);
hintEl.innerText = "捕获中...";
} else {
// 普通移动 -> 柔和跟随
particles.setState('FOLLOW', pos1);
hintEl.innerText = "流体跟随";
}
particles.state.speed = 1.0;
}
}
// 渲染循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
particles.update(time);
// 摄像机微动,增加沉浸感
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();
}
// 窗口大小适配
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
particles.material.uniforms.pixelRatio.value = window.devicePixelRatio;
});
animate();
</script>
</body>
</html>