Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

247 lines
8.7 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>02 - Moving Dots | 粒子动起来</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0a;
overflow: hidden;
font-family: 'Space Mono', monospace;
}
#canvas-container {
width: 100vw;
height: 100vh;
}
.info {
position: fixed;
top: 20px;
left: 20px;
color: #00ff88;
font-size: 14px;
pointer-events: none;
text-shadow: 0 0 10px #00ff88;
z-index: 100;
}
.info h1 { font-size: 18px; margin-bottom: 8px; }
.info p { opacity: 0.8; font-size: 12px; margin: 4px 0; }
.controls {
position: fixed;
bottom: 20px;
left: 20px;
color: #fff;
font-size: 12px;
background: rgba(0,0,0,0.5);
padding: 15px;
border-radius: 8px;
border: 1px solid #333;
}
.control-group {
margin: 8px 0;
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
width: 80px;
color: #aaa;
}
.control-group input[type="range"] {
width: 150px;
}
.control-group span {
color: #00ff88;
min-width: 40px;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div class="info">
<h1>02 - Moving Dots</h1>
<p>每个粒子独立运动</p>
<p>粒子数: <span id="count">0</span></p>
<p>FPS: <span id="fps">0</span></p>
</div>
<div class="controls">
<div class="control-group">
<label>速度</label>
<input type="range" id="speed" min="0.1" max="3" step="0.1" value="1">
<span id="speedVal">1.0</span>
</div>
<div class="control-group">
<label>振幅</label>
<input type="range" id="amp" min="1" max="20" step="1" value="5">
<span id="ampVal">5</span>
</div>
<div class="control-group">
<label>频率</label>
<input type="range" id="freq" min="0.1" max="2" step="0.1" value="0.5">
<span id="freqVal">0.5</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// 场景设置
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a0a, 0.01);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 60;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 粒子参数
const PARTICLE_COUNT = 2000;
const geometry = new THREE.BufferGeometry();
// 存储初始位置和运动参数
const positions = new Float32Array(PARTICLE_COUNT * 3);
const initialPositions = new Float32Array(PARTICLE_COUNT * 3);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const phases = new Float32Array(PARTICLE_COUNT); // 每个粒子的相位偏移
const speeds = new Float32Array(PARTICLE_COUNT); // 每个粒子的速度
// 生成粒子
for (let i = 0; i < PARTICLE_COUNT; i++) {
// 初始位置:螺旋分布
const angle = (i / PARTICLE_COUNT) * Math.PI * 20; // 10圈螺旋
const radius = 5 + (i / PARTICLE_COUNT) * 25;
const height = (Math.random() - 0.5) * 20;
const x = Math.cos(angle) * radius;
const y = height;
const z = Math.sin(angle) * radius;
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
initialPositions[i * 3] = x;
initialPositions[i * 3 + 1] = y;
initialPositions[i * 3 + 2] = z;
// 颜色:根据半径渐变(蓝→青→绿→黄)
const hue = 0.5 + (radius / 30) * 0.3; // 0.5(青) ~ 0.8(黄)
const color = new THREE.Color().setHSL(hue, 0.9, 0.5);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
// 每个粒子独特的运动参数
phases[i] = Math.random() * Math.PI * 2;
speeds[i] = 0.5 + Math.random() * 1.5;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 粒子材质
const material = new THREE.PointsMaterial({
size: 0.8,
vertexColors: true,
transparent: true,
opacity: 0.9,
sizeAttenuation: true,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
// UI 控制
const speedSlider = document.getElementById('speed');
const ampSlider = document.getElementById('amp');
const freqSlider = document.getElementById('freq');
const speedVal = document.getElementById('speedVal');
const ampVal = document.getElementById('ampVal');
const freqVal = document.getElementById('freqVal');
speedSlider.addEventListener('input', (e) => speedVal.textContent = e.target.value);
ampSlider.addEventListener('input', (e) => ampVal.textContent = e.target.value);
freqSlider.addEventListener('input', (e) => freqVal.textContent = e.target.value);
// 更新计数
document.getElementById('count').textContent = PARTICLE_COUNT;
// FPS 计算
let lastTime = performance.now();
let frameCount = 0;
const fpsEl = document.getElementById('fps');
// 动画循环
let time = 0;
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
frameCount++;
if (now - lastTime >= 1000) {
fpsEl.textContent = frameCount;
frameCount = 0;
lastTime = now;
}
// 获取控制值
const globalSpeed = parseFloat(speedSlider.value);
const amplitude = parseFloat(ampSlider.value);
const frequency = parseFloat(freqSlider.value);
time += 0.01 * globalSpeed;
const positions = geometry.attributes.position.array;
// 更新每个粒子位置(核心算法)
for (let i = 0; i < PARTICLE_COUNT; i++) {
const ix = i * 3;
const iy = i * 3 + 1;
const iz = i * 3 + 2;
const initX = initialPositions[ix];
const initY = initialPositions[iy];
const initZ = initialPositions[iz];
// 波浪运动:基于时间和相位的正弦波
const wave1 = Math.sin(time * speeds[i] + phases[i]) * amplitude;
const wave2 = Math.cos(time * speeds[i] * frequency + phases[i]) * amplitude * 0.5;
// 垂直波动
positions[iy] = initY + wave1;
// 径向波动
const angle = Math.atan2(initZ, initX);
const radius = Math.sqrt(initX * initX + initZ * initZ);
const newRadius = radius + wave2;
positions[ix] = Math.cos(angle) * newRadius;
positions[iz] = Math.sin(angle) * newRadius;
}
// 标记位置属性需要更新
geometry.attributes.position.needsUpdate = true;
// 整体缓慢旋转
particles.rotation.y = time * 0.1;
renderer.render(scene, camera);
}
animate();
// 响应窗口调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>