Gemini-3D粒子手势控制
·
代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D手势粒子系统</title>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<script src="https://cdnjs.cloudflare.com/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/@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/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; overflow: hidden; background-color: #000000; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
#canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#ui-layer { position: absolute; z-index: 10; pointer-events: none; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: space-between; padding: 20px; box-sizing: border-box; }
.pointer-events-auto { pointer-events: auto; }
#video-preview {
position: absolute; bottom: 20px; right: 20px; width: 160px; height: 120px;
border-radius: 12px; border: 2px solid rgba(255,255,255,0.2);
transform: scaleX(-1);
background: #000;
opacity: 0.6;
transition: opacity 0.3s;
box-shadow: 0 0 15px rgba(99, 102, 241, 0.5);
display: none;
z-index: 20;
}
#video-preview.active { display: block; }
#video-preview:hover { opacity: 1; }
.glass-panel {
background: rgba(10, 10, 10, 0.75);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
transition: all 0.3s ease;
}
.btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #ccc;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
}
.btn:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-1px); color: #fff; box-shadow: 0 0 10px rgba(255,255,255,0.1); }
.btn.active { background: rgba(99, 102, 241, 0.2); border-color: #6366f1; color: #818cf8; box-shadow: 0 0 15px rgba(99, 102, 241, 0.3); }
input[type="range"] { accent-color: #6366f1; }
.custom-scroll::-webkit-scrollbar { width: 4px; }
.custom-scroll::-webkit-scrollbar-track { background: rgba(255,255,255,0.02); }
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 2px; }
#loader {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #000; z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.8s ease;
pointer-events: none;
}
.spinner {
width: 60px; height: 60px; border: 3px solid rgba(255,255,255,0.1);
border-radius: 50%; border-top-color: #818cf8;
animation: spin 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
box-shadow: 0 0 20px rgba(99, 102, 241, 0.5);
}
@keyframes spin { to { transform: rotate(360deg); } }
#retry-cam-btn {
display: none; margin-top: 5px; background: #ef4444; color: white;
font-size: 10px; padding: 4px 8px; border-radius: 4px; cursor: pointer;
}
</style>
</head>
<body>
<div id="loader">
<div class="spinner"></div>
<p class="mt-6 text-gray-300 font-light tracking-widest text-sm">QUANTUM PARTICLES</p>
<p class="text-xs text-gray-500 mt-2">System Initializing...</p>
</div>
<div id="canvas-container"></div>
<div id="ui-layer">
<div class="flex justify-between items-start pointer-events-auto">
<div class="glass-panel border-l-4 border-indigo-500">
<h1 class="text-2xl font-black italic tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-300 to-cyan-300 filter drop-shadow-lg">
NEON MORPH
</h1>
<p class="text-[10px] text-indigo-200/70 mt-1 uppercase tracking-widest">Ultimate Edition</p>
</div>
<div class="glass-panel flex gap-2">
<button id="btn-audio" class="btn">🎵 律动</button>
<button id="btn-bloom" class="btn active">✨ 辉光</button>
<button id="btn-fullscreen" class="btn">⛶ 全屏</button>
<button id="btn-snapshot" class="btn">📸 截图</button>
</div>
</div>
<div class="absolute top-28 left-5 glass-panel w-72 pointer-events-auto flex flex-col gap-4 max-h-[75vh] overflow-hidden border border-white/5">
<div class="flex-1 overflow-y-auto custom-scroll pr-2">
<label class="text-[10px] uppercase text-indigo-300/80 font-bold mb-3 block sticky top-0 bg-[#0a0a0a]/90 backdrop-blur-md z-10 py-2 border-b border-white/5">
Geometries (30)
</label>
<div class="grid grid-cols-2 gap-2" id="shape-buttons">
</div>
</div>
<div class="h-px bg-gradient-to-r from-transparent via-white/10 to-transparent w-full"></div>
<div class="flex-shrink-0">
<label class="text-[10px] uppercase text-indigo-300/80 font-bold mb-3 block">Appearance</label>
<div class="mb-4">
<div class="flex justify-between text-xs text-gray-400 mb-1">
<span>Particle Size</span>
<span id="size-val">0.12</span>
</div>
<input type="range" id="size-slider" min="0.01" max="0.4" step="0.01" value="0.12" class="w-full h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-gray-400">Base Color</span>
<input type="color" id="color-picker" value="#6366f1" class="bg-transparent border-0 w-8 h-6 cursor-pointer rounded overflow-hidden">
</div>
</div>
<div class="h-px bg-gradient-to-r from-transparent via-white/10 to-transparent w-full"></div>
<div class="flex-shrink-0">
<div class="flex justify-between items-end">
<div class="text-[10px] text-gray-500 font-mono">
<div>FPS: <span id="fps-counter" class="text-green-400">--</span></div>
<div>OBJ: <span class="text-indigo-400">30k</span></div>
</div>
<div class="flex flex-col items-end">
<div id="status-gesture" class="text-xs px-2 py-1 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/30">
LOADING AI...
</div>
<button id="retry-cam-btn">Manual Start</button>
</div>
</div>
<div id="camera-tip" class="text-[9px] text-gray-500 mt-1 text-right">
* AI 加载中,请先使用鼠标
</div>
</div>
</div>
</div>
<video id="video-preview" class="pointer-events-auto" playsinline></video>
<script>
setTimeout(() => {
const loader = document.getElementById('loader');
if (loader) loader.style.opacity = '0';
setTimeout(() => { if(loader) loader.style.display = 'none'; }, 800);
}, 1500);
const CONFIG = {
particleCount: 30000,
bgParticleCount: 2000,
baseSize: 0.12,
morphSpeed: 0.08,
handInfluenceSpeed: 0.15,
audioSensitivity: 2.0,
bloomStrength: 1.2,
bloomRadius: 0.5,
bloomThreshold: 0.15
};
const STATE = {
currentShape: 'sphere',
targetScale: 1,
currentScale: 1,
handDetected: false,
audioEnabled: false,
bloomEnabled: true,
hueOffset: 0,
mouse: new THREE.Vector2(-9999, -9999),
time: 0
};
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x020205);
scene.fog = new THREE.FogExp2(0x020205, 0.02);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 28;
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
let composer;
let renderPass, bloomPass;
function initBloom() {
try {
if (typeof THREE.EffectComposer === 'undefined') throw new Error("No Composer Lib");
renderPass = new THREE.RenderPass(scene, camera);
bloomPass = new THREE.UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
CONFIG.bloomStrength, CONFIG.bloomRadius, CONFIG.bloomThreshold
);
composer = new THREE.EffectComposer(renderer);
composer.addPass(renderPass);
composer.addPass(bloomPass);
console.log("Bloom initialized successfully.");
} catch (e) {
console.warn("Bloom failed to init (Hardware limitation?), disabling.", e);
STATE.bloomEnabled = false;
document.getElementById('btn-bloom').classList.remove('active');
document.getElementById('btn-bloom').style.opacity = '0.5';
composer = null;
}
}
initBloom();
function getParticleTexture() {
const canvas = document.createElement('canvas');
canvas.width = 32; canvas.height = 32;
const ctx = canvas.getContext('2d');
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
grad.addColorStop(0.4, 'rgba(200, 220, 255, 0.6)');
grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 32, 32);
return new THREE.CanvasTexture(canvas);
}
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(CONFIG.particleCount * 3);
const colors = new Float32Array(CONFIG.particleCount * 3);
const colorObj = new THREE.Color();
for (let i = 0; i < CONFIG.particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = (Math.random() - 0.5) * 100;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
colorObj.setHSL(Math.random(), 0.8, 0.5);
colors[i * 3] = colorObj.r;
colors[i * 3 + 1] = colorObj.g;
colors[i * 3 + 2] = colorObj.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: CONFIG.baseSize,
map: getParticleTexture(),
transparent: true,
opacity: 0.9,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
const bgGeo = new THREE.BufferGeometry();
const bgPos = new Float32Array(CONFIG.bgParticleCount * 3);
for(let i=0; i<CONFIG.bgParticleCount*3; i++) bgPos[i] = (Math.random()-0.5)*200;
bgGeo.setAttribute('position', new THREE.BufferAttribute(bgPos, 3));
const bgParticles = new THREE.Points(bgGeo, new THREE.PointsMaterial({
size: 0.4, color: 0x445588, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending
}));
scene.add(bgParticles);
const shapes = {};
function generateShapes() {
const count = CONFIG.particleCount; const PI = Math.PI;
function pt(r) {
const t = Math.random() * PI * 2; const p = Math.acos((Math.random() * 2) - 1);
return [r * Math.sin(p) * Math.cos(t), r * Math.sin(p) * Math.sin(t), r * Math.cos(p)];
}
shapes.sphere = []; for (let i = 0; i < count; i++) shapes.sphere.push(...pt(12));
shapes.chaos = []; for (let i = 0; i < count; i++) shapes.chaos.push(...pt(Math.random() * 22));
shapes.cube = []; for(let i=0; i<count; i++) { const s = 18; shapes.cube.push((Math.random()-0.5)*s, (Math.random()-0.5)*s, (Math.random()-0.5)*s); }
shapes.heart = [];
for (let i = 0; i < count; i++) {
const t = Math.random() * PI * 2; const r = Math.random();
const x = 16 * Math.pow(Math.sin(t), 3);
const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
shapes.heart.push(x*0.6*Math.sqrt(r), y*0.6*Math.sqrt(r), (Math.random()-0.5)*5*Math.sqrt(r));
}
shapes.torus = [];
for (let i = 0; i < count; i++) {
const u = Math.random() * PI * 2; const v = Math.random() * PI * 2;
shapes.torus.push((10 + 3.5 * Math.cos(v)) * Math.cos(u), (10 + 3.5 * Math.cos(v)) * Math.sin(u), 3.5 * Math.sin(v));
}
shapes.dna = [];
for(let i=0; i<count; i++) {
const t = (i / count) * PI * 25 - PI * 12.5; const r = 6; const o = (i % 2 === 0) ? 0 : PI;
let x = r * Math.cos(t + o); let y = t * 1.5; let z = r * Math.sin(t + o);
if (Math.random() > 0.85) { x *= Math.random(); z *= Math.random(); }
shapes.dna.push(x, y, z);
}
const simpleShapes = {
'mobius': () => { const u=Math.random()*PI*2; const v=Math.random()*2-1; const R=9; return [(R+v*Math.cos(u/2))*Math.cos(u), (R+v*Math.cos(u/2))*Math.sin(u), v*Math.sin(u/2)*3]; },
'galaxy': (i) => { const a=5; const ai=i%a; const r=Math.random()*20; const t=r*0.6+(ai*2*PI/a); return [r*Math.cos(t), (Math.random()-0.5)*(22-r)*0.3, r*Math.sin(t)]; },
'atom': () => (Math.random()<0.15) ? pt(3) : [14*Math.cos(Math.random()*PI*2), (Math.random()-0.5), 14*Math.sin(Math.random()*PI*2)],
'pyramid': () => { const h=Math.random()*18-9; const r=(9-h)/1.4; return [(Math.random()-0.5)*2*r, h, (Math.random()-0.5)*2*r]; },
'tunnel': (i) => [Math.cos(i)*10 + (Math.random()-0.5)*2, Math.sin(i)*10 + (Math.random()-0.5)*2, (Math.random()-0.5)*80],
'wave': (i) => { const s=Math.sqrt(count); return [(i%s-s/2)*0.8, Math.sin((i%s)*0.15 + Math.floor(i/s)*0.15)*5, (Math.floor(i/s)-s/2)*0.8]; },
'cylinder': () => { const t=Math.random()*PI*2; return [7*Math.cos(t), (Math.random()-0.5)*24, 7*Math.sin(t)]; },
'hourglass': () => { const y=(Math.random()-0.5)*20; const r=Math.abs(y)*0.7+0.5; const t=Math.random()*PI*2; return [r*Math.cos(t), y, r*Math.sin(t)]; },
'cone': () => { const h=Math.random()*16-8; const r=(8-h)*0.8; const t=Math.random()*PI*2; return [r*Math.cos(t), h, r*Math.sin(t)]; },
'ring': () => { const t=Math.random()*PI*2; const r=14+(Math.random()-0.5)*3; return [r*Math.cos(t), (Math.random()-0.5)*1.5, r*Math.sin(t)]; },
'cross': () => (Math.random()>0.5) ? [(Math.random()-0.5)*5, (Math.random()-0.5)*24, (Math.random()-0.5)*5] : [(Math.random()-0.5)*24, (Math.random()-0.5)*5, (Math.random()-0.5)*5],
'grid': (i) => { const s=Math.cbrt(count); const d=22/s; return [(i%s-s/2)*d, (Math.floor((i/s)%s)-s/2)*d, (Math.floor(i/(s*s))-s/2)*d]; },
'lotus': () => { const u=Math.random()*PI*2; const v=Math.random()*PI; const r=9*(1+0.5*Math.sin(5*u)*Math.sin(v)); return [r*Math.sin(v)*Math.cos(u), r*Math.cos(v)-6, r*Math.sin(v)*Math.sin(u)]; },
'saturn': () => (Math.random()>0.6) ? [ (14+Math.random()*6)*Math.cos(Math.random()*PI*2), (Math.random()-0.5)*0.6, (14+Math.random()*6)*Math.sin(Math.random()*PI*2) ] : pt(8),
'mushroom': () => (Math.random()>0.3) ? [10*Math.sin(Math.random()*PI/2)*Math.cos(Math.random()*PI*2), 10*Math.cos(Math.random()*PI/2)+3, 10*Math.sin(Math.random()*PI/2)*Math.sin(Math.random()*PI*2)] : [3*Math.cos(Math.random()*PI*2), Math.random()*12-9, 3*Math.sin(Math.random()*PI*2)],
'star': () => { const t=Math.random()*PI*2; const p=Math.acos(2*Math.random()-1); const l=10+6*(Math.sin(5*t)*Math.sin(5*p)); const r=Math.random()*l; return [r*Math.sin(p)*Math.cos(t), r*Math.sin(p)*Math.sin(t), r*Math.cos(p)]; },
'diamond': () => { const x=Math.random()-0.5; const y=Math.random()-0.5; const z=Math.random()-0.5; const s=14/(Math.abs(x)+Math.abs(y)+Math.abs(z)); return [x*s, y*s, z*s]; },
'shell': () => { const t=Math.random()*PI*4; const r=Math.exp(t*0.2); return [r*Math.cos(t*3), t*2-6, r*Math.sin(t*3)]; },
'vortex': () => { const h=(Math.random()-0.5)*24; const r=Math.abs(h)+1; const t=h*2.5; return [r*Math.cos(t), h, r*Math.sin(t)]; },
'infinity': () => { const t=Math.random()*PI*2; const s=12/(1+Math.sin(t)**2); return [s*Math.cos(t), s*Math.sin(t)*Math.cos(t), (Math.random()-0.5)*3]; },
'cloud': () => [(Math.random()-0.5)*30, (Math.random()-0.5)*10, (Math.random()-0.5)*20],
'blackhole': () => { const t=Math.random()*PI*2; const r=5+15*Math.random(); const y = -15/r; return [r*Math.cos(t), y, r*Math.sin(t)]; },
'rain': () => [(Math.random()-0.5)*30, Math.random()*30-15, (Math.random()-0.5)*30],
'nebula': () => pt(Math.random()*5 + 10) // 简单的雾状
};
for(let key in simpleShapes) {
shapes[key] = [];
for(let i=0; i<count; i++) shapes[key].push(...(simpleShapes[key](i) || pt(10)));
}
}
generateShapes();
let analyser, dataArray;
function initAudio() {
if (analyser) return;
const ctx = new (window.AudioContext || window.webkitAudioContext)();
analyser = ctx.createAnalyser();
analyser.fftSize = 256;
dataArray = new Uint8Array(analyser.frequencyBinCount);
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
ctx.createMediaStreamSource(stream).connect(analyser);
STATE.audioEnabled = true;
document.getElementById('btn-audio').classList.add('active');
document.getElementById('btn-audio').innerText = "🎵 监听中";
}).catch(e => console.error("Audio denied", e));
}
const videoElement = document.getElementById('video-preview');
let cameraUtils;
function onResults(results) {
const statusEl = document.getElementById('status-gesture');
const tipEl = document.getElementById('camera-tip');
if (statusEl.innerHTML.includes("LOADING") || statusEl.innerHTML.includes("SLOW")) {
videoElement.classList.add('active');
tipEl.style.display = 'none';
document.getElementById('retry-cam-btn').style.display = 'none';
}
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
STATE.handDetected = true;
const lm = results.multiHandLandmarks[0];
const dist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y);
let target = (dist - 0.02) * 7;
STATE.targetScale = Math.max(0.1, Math.min(target, 2.5));
particles.rotation.y = (lm[9].x - 0.5) * 3;
particles.rotation.x = (lm[9].y - 0.5) * 3;
statusEl.innerText = "HAND DETECTED";
statusEl.className = "text-xs px-2 py-1 rounded bg-green-500/20 text-green-400 border border-green-500/30";
} else {
STATE.handDetected = false;
STATE.targetScale = 1;
statusEl.innerText = "WAITING HAND";
statusEl.className = "text-xs px-2 py-1 rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30";
}
}
async function startCamera() {
try {
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({maxNumHands: 1, modelComplexity: 0, minDetectionConfidence: 0.5});
hands.onResults(onResults);
cameraUtils = new Camera(videoElement, {
onFrame: async () => {
if (videoElement.readyState >= 2) await hands.send({image: videoElement});
},
width: 320, height: 240
});
await cameraUtils.start();
} catch(e) {
console.warn("Camera init failed, using mouse fallback.", e);
document.getElementById('status-gesture').innerText = "CAMERA FAIL";
document.getElementById('camera-tip').innerText = "Camera failed, use mouse.";
}
}
setTimeout(startCamera, 1000);
const shapesList = [
{id: 'sphere', icon: '🔵', n:'Sphere'}, {id: 'heart', icon: '❤️', n:'Heart'}, {id: 'dna', icon: '🧬', n:'DNA'}, {id: 'galaxy', icon: '🌌', n:'Galaxy'},
{id: 'mobius', icon: '♾️', n:'Mobius'}, {id: 'atom', icon: '⚛️', n:'Atom'}, {id: 'pyramid', icon: '📐', n:'Pyramid'}, {id: 'lotus', icon: '🪷', n:'Lotus'},
{id: 'star', icon: '⭐', n:'Star'}, {id: 'saturn', icon: '🪐', n:'Saturn'}, {id: 'vortex', icon: '🌪️', n:'Vortex'}, {id: 'wave', icon: '🌊', n:'Wave'},
{id: 'chaos', icon: '💥', n:'Chaos'}, {id: 'torus', icon: '🍩', n:'Torus'}, {id: 'tunnel', icon: '🚇', n:'Tunnel'}, {id: 'diamond', icon: '💎', n:'Gem'},
{id: 'grid', icon: '🕸️', n:'Grid'}, {id: 'shell', icon: '🐚', n:'Shell'}, {id: 'hourglass', icon: '⏳', n:'Time'}, {id: 'cube', icon: '🧊', n:'Cube'},
{id: 'ring', icon: '💍', n:'Ring'}, {id: 'cross', icon: '✝️', n:'Cross'}, {id: 'cone', icon: '🔺', n:'Cone'}, {id: 'cylinder', icon: '🔋', n:'Cylinder'},
{id: 'mushroom', icon: '🍄', n:'Shroom'}, {id: 'infinity', icon: '🔄', n:'Infinity'}, {id: 'cloud', icon: '☁️', n:'Cloud'}, {id: 'blackhole', icon: '🕳️', n:'Void'},
{id: 'rain', icon: '🌧️', n:'Rain'}, {id: 'nebula', icon: '✨', n:'Nebula'}
];
const btnBox = document.getElementById('shape-buttons');
shapesList.forEach(s => {
const b = document.createElement('button');
b.className = `btn ${s.id==='sphere'?'active':''} justify-center text-[10px] py-2`;
b.innerHTML = `<span class="opacity-80">${s.icon}</span> ${s.n}`;
b.onclick = () => {
STATE.currentShape = s.id;
document.querySelectorAll('#shape-buttons .btn').forEach(btn=>btn.classList.remove('active'));
b.classList.add('active');
};
btnBox.appendChild(b);
});
document.getElementById('size-slider').oninput = (e) => {
material.size = parseFloat(e.target.value);
document.getElementById('size-val').innerText = e.target.value;
};
document.getElementById('color-picker').oninput = (e) => {
};
document.getElementById('btn-audio').onclick = initAudio;
document.getElementById('btn-bloom').onclick = function() {
STATE.bloomEnabled = !STATE.bloomEnabled;
this.classList.toggle('active');
};
document.getElementById('btn-fullscreen').onclick = () => {
if(!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{});
else document.exitFullscreen().catch(()=>{});
};
document.getElementById('btn-snapshot').onclick = () => {
renderer.render(scene, camera);
const link = document.createElement('a');
link.download = 'neon-particles.png';
link.href = renderer.domElement.toDataURL('image/png');
link.click();
};
window.addEventListener('mousemove', (e) => {
STATE.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
STATE.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
if (!STATE.handDetected) {
particles.rotation.x += e.movementY * 0.001;
particles.rotation.y += e.movementX * 0.001;
}
});
// --- 9. 渲染守卫循环 (Crash Protection) ---
const clock = new THREE.Clock();
let frameCount = 0;
let lastTime = 0;
const fpsElem = document.getElementById('fps-counter');
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
STATE.time += delta;
// FPS Counter
frameCount++;
if(STATE.time - lastTime >= 1) {
fpsElem.innerText = frameCount;
frameCount = 0;
lastTime = STATE.time;
}
// Physics & Morphing
STATE.currentScale += (STATE.targetScale - STATE.currentScale) * CONFIG.handInfluenceSpeed;
let audioAmp = 0;
if(STATE.audioEnabled && analyser) {
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for(let i=0; i<dataArray.length; i++) sum += dataArray[i];
audioAmp = sum / dataArray.length / 255;
}
const pos = particles.geometry.attributes.position.array;
const col = particles.geometry.attributes.color.array;
const target = shapes[STATE.currentShape] || shapes.sphere;
const baseColorHex = document.getElementById('color-picker').value;
const baseCol = new THREE.Color(baseColorHex);
const baseHSL = {};
baseCol.getHSL(baseHSL);
STATE.hueOffset += 0.002; // Color Cycle
for(let i=0; i<CONFIG.particleCount; i++) {
const i3 = i * 3;
let tx = target[i3], ty = target[i3+1], tz = target[i3+2];
tx *= STATE.currentScale; ty *= STATE.currentScale; tz *= STATE.currentScale;
// Audio React
if(STATE.audioEnabled) {
const beat = 1 + audioAmp * CONFIG.audioSensitivity * (1 - Math.abs(ty)/25);
tx *= beat; ty *= beat; tz *= beat;
} else {
const breathe = 1 + Math.sin(STATE.time * 2) * 0.03;
tx *= breathe; ty *= breathe; tz *= breathe;
}
// Morphing
pos[i3] += (tx - pos[i3]) * CONFIG.morphSpeed;
pos[i3+1] += (ty - pos[i3+1]) * CONFIG.morphSpeed;
pos[i3+2] += (tz - pos[i3+2]) * CONFIG.morphSpeed;
// Dynamic Coloring
const dist = Math.sqrt(tx*tx + ty*ty + tz*tz);
let h = baseHSL.h + STATE.hueOffset + (dist * 0.015);
let s = 0.8;
let l = 0.5 + Math.sin(STATE.time * 4 + i) * 0.15;
if(STATE.audioEnabled) l += audioAmp * 0.4; // Flash on beat
colorObj.setHSL(h % 1, s, l);
col[i3] = colorObj.r; col[i3+1] = colorObj.g; col[i3+2] = colorObj.b;
}
particles.geometry.attributes.position.needsUpdate = true;
particles.geometry.attributes.color.needsUpdate = true;
// Auto Rotate
if(!STATE.handDetected) {
particles.rotation.y += 0.002;
bgParticles.rotation.y -= 0.0005;
}
// --- Render Guard (渲染守卫) ---
// 核心逻辑:尝试用Bloom渲染,如果报错(显卡不兼容/Context丢失),自动降级为普通渲染
try {
if(STATE.bloomEnabled && composer) {
composer.render();
} else {
renderer.render(scene, camera);
}
} catch(e) {
console.warn("Render Guard: Bloom crashed! Disabling bloom to prevent black screen.", e);
STATE.bloomEnabled = false;
document.getElementById('btn-bloom').classList.remove('active');
// Force standard render immediately
renderer.render(scene, camera);
}
}
animate();
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if(composer) composer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
部分效果图

更多推荐


所有评论(0)