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

376 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>38 - Fluid Orb</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; }
canvas { display: block; }
#info {
position: fixed; top: 10px; left: 10px;
color: #fff; font-size: 11px; z-index: 100; pointer-events: none;
text-shadow: 0 0 10px rgba(0,0,0,0.8);
}
#controls {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%); display: flex; gap: 12px; z-index: 100;
}
button {
padding: 10px 20px; background: rgba(0,0,0,0.6);
border: 2px solid; color: #fff; cursor: pointer;
transition: all 0.3s; border-radius: 6px;
font-size: 13px; font-weight: bold;
}
button.red { border-color: #ff3366; box-shadow: 0 0 15px rgba(255,51,102,0.3); }
button.red:hover { box-shadow: 0 0 30px rgba(255,51,102,0.6); }
button.blue { border-color: #3366ff; box-shadow: 0 0 15px rgba(51,102,255,0.3); }
button.blue:hover { box-shadow: 0 0 30px rgba(51,102,255,0.6); }
button.stir { border-color: #cc33ff; }
</style>
</head>
<body>
<div id="info">
38 - Paint Mixing<br>
<span id="stats">Loading...</span>
</div>
<div id="controls">
<button id="btn-red" class="red">Add Red</button>
<button id="btn-stir" class="stir">Stir</button>
<button id="btn-blue" class="blue">Add Blue</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
// ============================================
// 配置 - 保守参数确保稳定
// ============================================
const CONFIG = {
particleCount: 15000, // 减少粒子数
particleSize: 0.12,
bloomStrength: 1.8, // 降低避免过曝
bloomRadius: 0.6,
bloomThreshold: 0.2,
containerRadius: 1.0
};
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x020205);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 2.5;
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
document.body.appendChild(renderer.domElement);
// ============================================
// Bloom
// ============================================
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
CONFIG.bloomStrength,
CONFIG.bloomRadius,
CONFIG.bloomThreshold
));
// ============================================
// 粒子系统
// ============================================
const positions = new Float32Array(CONFIG.particleCount * 3);
const velocities = new Float32Array(CONFIG.particleCount * 3);
const colors = new Float32Array(CONFIG.particleCount * 3);
const sizes = new Float32Array(CONFIG.particleCount);
const types = new Int8Array(CONFIG.particleCount); // 1=红, -1=蓝
// 正确初始化 - 双螺旋分布
const phi = (1 + Math.sqrt(5)) / 2; // 黄金比例
for (let i = 0; i < CONFIG.particleCount; i++) {
// 黄金角度分布
const y = 1 - (i / (CONFIG.particleCount - 1)) * 2;
const radius = Math.sqrt(Math.max(0, 1 - y * y));
const theta = phi * i * 2 * Math.PI;
// 交替红蓝,创建条纹
const isRed = (i % 2 === 0);
const angleOffset = isRed ? 0 : Math.PI;
const r = 0.85 + (Math.random() - 0.5) * 0.15;
const x = r * radius * Math.cos(theta + angleOffset);
const z = r * radius * Math.sin(theta + angleOffset);
const yPos = y * 0.9;
positions[i * 3] = x;
positions[i * 3 + 1] = yPos;
positions[i * 3 + 2] = z;
velocities[i * 3] = 0;
velocities[i * 3 + 1] = 0;
velocities[i * 3 + 2] = 0;
types[i] = isRed ? 1 : -1;
// 初始颜色
if (isRed) {
colors[i * 3] = 0.6;
colors[i * 3 + 1] = 0.1;
colors[i * 3 + 2] = 0.15;
} else {
colors[i * 3] = 0.1;
colors[i * 3 + 1] = 0.15;
colors[i * 3 + 2] = 0.7;
}
sizes[i] = CONFIG.particleSize * (0.8 + Math.random() * 0.4);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// 柔和粒子纹理
const spriteCanvas = document.createElement('canvas');
spriteCanvas.width = 128;
spriteCanvas.height = 128;
const ctx = spriteCanvas.getContext('2d');
const grad = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
grad.addColorStop(0, 'rgba(255,255,255,1)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.7)');
grad.addColorStop(0.6, 'rgba(255,255,255,0.2)');
grad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 128, 128);
const spriteTexture = new THREE.CanvasTexture(spriteCanvas);
const material = new THREE.ShaderMaterial({
uniforms: { pointTexture: { value: spriteTexture } },
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (350.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform sampler2D pointTexture;
varying vec3 vColor;
void main() {
vec4 texColor = texture2D(pointTexture, gl_PointCoord);
gl_FragColor = vec4(vColor * 1.5, texColor.a);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
// ============================================
// Curl Noise
// ============================================
function curlNoise(x, y, z, time) {
const scale = 2.0;
const t = time * 0.3;
return {
x: Math.sin(y * scale + t) * Math.cos(z * scale + t * 0.7) * 0.8,
y: Math.sin(z * scale + t * 0.8) * Math.cos(x * scale + t * 0.6) * 0.8,
z: Math.sin(x * scale + t * 0.5) * Math.cos(y * scale + t * 0.9) * 0.8
};
}
// ============================================
// 物理
// ============================================
let stirPower = 0;
function updatePhysics(time) {
const posArray = geometry.attributes.position.array;
const colArray = geometry.attributes.color.array;
// 衰减搅拌
stirPower *= 0.98;
for (let i = 0; i < CONFIG.particleCount; i++) {
const idx = i * 3;
const px = posArray[idx];
const py = posArray[idx + 1];
const pz = posArray[idx + 2];
const dist = Math.sqrt(px*px + py*py + pz*pz);
// Curl噪声 - 创造流体感
const curl = curlNoise(px * 1.2, py * 1.2, pz * 1.2, time);
const baseFlow = 0.08 + stirPower * 0.3;
velocities[idx] += curl.x * baseFlow * 0.016;
velocities[idx + 1] += curl.y * baseFlow * 0.016;
velocities[idx + 2] += curl.z * baseFlow * 0.016;
// 漩涡
const vortex = 0.02 + stirPower * 0.1;
velocities[idx] += -pz * vortex * 0.016;
velocities[idx + 2] += px * vortex * 0.016;
// 边界
if (dist > CONFIG.containerRadius * 0.9) {
const nx = px / dist;
const ny = py / dist;
const nz = pz / dist;
const push = (dist - CONFIG.containerRadius * 0.9) * 20;
velocities[idx] -= nx * push * 0.016;
velocities[idx + 1] -= ny * push * 0.016;
velocities[idx + 2] -= nz * push * 0.016;
}
// 向心力
velocities[idx] += px * (-0.015 * dist) * 0.016;
velocities[idx + 1] += py * (-0.015 * dist) * 0.016;
velocities[idx + 2] += pz * (-0.015 * dist) * 0.016;
// 阻尼
velocities[idx] *= 0.99;
velocities[idx + 1] *= 0.99;
velocities[idx + 2] *= 0.99;
// 更新位置
posArray[idx] += velocities[idx] * 0.016;
posArray[idx + 1] += velocities[idx + 1] * 0.016;
posArray[idx + 2] += velocities[idx + 2] * 0.016;
// 硬边界
const newDist = Math.sqrt(posArray[idx]**2 + posArray[idx+1]**2 + posArray[idx+2]**2);
if (newDist > CONFIG.containerRadius) {
const s = CONFIG.containerRadius * 0.95 / newDist;
posArray[idx] *= s;
posArray[idx + 1] *= s;
posArray[idx + 2] *= s;
}
// 颜色更新 - 混合区域用 #F08650 橙红色
const type = types[i];
const speed = Math.sqrt(velocities[idx]**2 + velocities[idx+1]**2 + velocities[idx+2]**2);
const bright = 1.0 + speed * 2;
// #F08650 橙红色混合目标
const mixR = 0.94, mixG = 0.53, mixB = 0.31;
// 根据运动混合颜色 - 高速时趋向橙红,而非白色
if (type > 0) { // 红
const blend = Math.min(1, speed * 2); // 混合因子
colArray[idx] = (0.7 * bright) * (1 - blend) + mixR * blend * bright;
colArray[idx + 1] = 0.05 * (1 - blend) + mixG * blend * bright;
colArray[idx + 2] = 0.1 * (1 - blend) + mixB * blend * bright;
} else { // 蓝
const blend = Math.min(1, speed * 2);
colArray[idx] = 0.05 * (1 - blend) + mixR * blend * bright;
colArray[idx + 1] = 0.1 * (1 - blend) + mixG * blend * bright;
colArray[idx + 2] = (0.7 * bright) * (1 - blend) + mixB * blend * bright;
}
}
geometry.attributes.position.needsUpdate = true;
geometry.attributes.color.needsUpdate = true;
}
// ============================================
// 动画
// ============================================
let time = 0;
let frame = 0;
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
time += 0.016;
frame++;
if (frame % 60 === 0) {
const now = performance.now();
const fps = Math.round(60000 / (now - lastTime));
lastTime = now;
document.getElementById('stats').textContent =
`${CONFIG.particleCount.toLocaleString()} pts | ${fps} FPS`;
}
updatePhysics(time);
particleSystem.rotation.y += 0.001;
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);
});
document.getElementById('btn-red').addEventListener('click', () => {
stirPower = Math.min(stirPower + 0.5, 2);
// 随机改变一些蓝色为红色
for (let i = 0; i < CONFIG.particleCount; i++) {
if (types[i] === -1 && Math.random() < 0.1) {
types[i] = 1;
velocities[i*3] += (Math.random()-0.5);
velocities[i*3+1] += (Math.random()-0.5);
velocities[i*3+2] += (Math.random()-0.5);
}
}
});
document.getElementById('btn-blue').addEventListener('click', () => {
stirPower = Math.min(stirPower + 0.5, 2);
for (let i = 0; i < CONFIG.particleCount; i++) {
if (types[i] === 1 && Math.random() < 0.1) {
types[i] = -1;
velocities[i*3] += (Math.random()-0.5);
velocities[i*3+1] += (Math.random()-0.5);
velocities[i*3+2] += (Math.random()-0.5);
}
}
});
document.getElementById('btn-stir').addEventListener('click', () => {
stirPower = 2.5;
// 给所有粒子随机扰动
for (let i = 0; i < CONFIG.particleCount; i++) {
velocities[i*3] += (Math.random() - 0.5) * 2;
velocities[i*3+1] += (Math.random() - 0.5) * 2;
velocities[i*3+2] += (Math.random() - 0.5) * 2;
}
});
animate();
</script>
</body>
</html>