Files
nanocode/particle-lab/23-taiji/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

318 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>太极粒子 - Taiji Particles</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000000;
cursor: pointer;
}
#canvas-container {
width: 100vw;
height: 100vh;
}
.info {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.4);
font-family: 'Courier New', monospace;
font-size: 12px;
letter-spacing: 2px;
pointer-events: none;
text-transform: uppercase;
}
.title {
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.6);
font-family: 'Courier New', monospace;
font-size: 14px;
letter-spacing: 4px;
pointer-events: none;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div class="title">TAIJI • 太极</div>
<div class="info">Click to change direction • 点击切换方向</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Configuration
const PARTICLE_COUNT = 6000;
const TAIJI_RADIUS = 180;
const ROTATION_SPEED = 0.005;
const FLOW_SPEED = 0.02;
// State
let rotationDirection = 1;
let rotationSpeed = ROTATION_SPEED;
let time = 0;
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 400;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Particle geometry
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(PARTICLE_COUNT * 3);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const initialPositions = new Float32Array(PARTICLE_COUNT * 3);
const particleData = new Float32Array(PARTICLE_COUNT * 4); // angle, radius, speed, phase
// Create particle texture
function createParticleTexture() {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 32, 32);
const texture = new THREE.CanvasTexture(canvas);
return texture;
}
// Taiji mathematical formula
// Check if point is in yin (black) region
function isInYinRegion(x, y) {
const r = TAIJI_RADIUS;
const halfR = r / 2;
// Overall circle
if (x * x + y * y > r * r) return false;
// S-curve boundary using sine wave approximation
// The S-curve goes from top to bottom
const curveX = -Math.sin(Math.PI * (y + r) / (2 * r)) * halfR;
// Left side (top half is white, bottom is black)
// Right side (top half is black, bottom is white)
if (x < curveX) {
return y < 0; // Left side: bottom is black
} else {
return y > 0; // Right side: top is black
}
}
// Check if point is fish eye center
function isFishEye(x, y) {
const r = TAIJI_RADIUS;
const eyeR = r / 6;
const eyeOffset = r / 2;
// White eye in black region (top right area)
const dx1 = x - eyeOffset;
const dy1 = y - 0;
if (dx1 * dx1 + dy1 * dy1 < eyeR * eyeR) return 1; // White eye
// Black eye in white region (bottom left area)
const dx2 = x + eyeOffset;
const dy2 = y - 0;
if (dx2 * dx2 + dy2 * dy2 < eyeR * eyeR) return 2; // Black eye
return 0;
}
// Generate particles following taiji curve
function generateTaijiParticles() {
let index = 0;
const r = TAIJI_RADIUS;
for (let i = 0; i < PARTICLE_COUNT; i++) {
let x, y, z;
let inTaiji = false;
let attempts = 0;
// Rejection sampling to get particles in taiji shape
do {
// Generate random position in bounding box
x = (Math.random() - 0.5) * 2 * r;
y = (Math.random() - 0.5) * 2 * r;
z = (Math.random() - 0.5) * 40; // Slight depth
// Check if inside taiji circle
if (x * x + y * y <= r * r) {
inTaiji = true;
}
attempts++;
} while (!inTaiji && attempts < 100);
// Store position
positions[index * 3] = x;
positions[index * 3 + 1] = y;
positions[index * 3 + 2] = z;
initialPositions[index * 3] = x;
initialPositions[index * 3 + 1] = y;
initialPositions[index * 3 + 2] = z;
// Determine color based on region
const eye = isFishEye(x, y);
const inYin = isInYinRegion(x, y);
let isWhite;
if (eye === 1) {
isWhite = true; // White eye in black
} else if (eye === 2) {
isWhite = false; // Black eye in white
} else {
isWhite = !inYin; // Normal region
}
if (isWhite) {
colors[index * 3] = 1.0;
colors[index * 3 + 1] = 1.0;
colors[index * 3 + 2] = 1.0;
} else {
colors[index * 3] = 0.0;
colors[index * 3 + 1] = 0.0;
colors[index * 3 + 2] = 0.0;
}
// Store particle data for animation
const angle = Math.atan2(y, x);
const radius = Math.sqrt(x * x + y * y);
particleData[index * 4] = angle;
particleData[index * 4 + 1] = radius;
particleData[index * 4 + 2] = Math.random() * 0.02 + 0.01; // Individual speed
particleData[index * 4 + 3] = Math.random() * Math.PI * 2; // Phase
index++;
}
}
generateTaijiParticles();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// Material
const material = new THREE.PointsMaterial({
size: 3,
vertexColors: true,
map: createParticleTexture(),
transparent: true,
opacity: 0.9,
blending: THREE.NormalBlending,
depthWrite: false,
sizeAttenuation: true
});
// Create points
const particles = new THREE.Points(geometry, material);
scene.add(particles);
// Add glow effect with larger particles
const glowGeometry = geometry.clone();
const glowMaterial = new THREE.PointsMaterial({
size: 6,
vertexColors: true,
map: createParticleTexture(),
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
});
const glowParticles = new THREE.Points(glowGeometry, glowMaterial);
scene.add(glowParticles);
// Animation
function animate() {
requestAnimationFrame(animate);
time += rotationSpeed * rotationDirection;
// Rotate entire taiji
particles.rotation.z = time;
glowParticles.rotation.z = time;
// Animate individual particles with flow effect
const positions = geometry.attributes.position.array;
const initial = initialPositions;
const data = particleData;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const idx = i * 3;
const dataIdx = i * 4;
const baseAngle = data[dataIdx];
const radius = data[dataIdx + 1];
const speed = data[dataIdx + 2];
const phase = data[dataIdx + 3];
// Calculate flow along the S-curve
const flowTime = time * 2 + phase;
const flowOffset = Math.sin(flowTime) * 5;
const radialPulse = Math.sin(flowTime * 0.5 + baseAngle * 2) * 2;
// Add subtle breathing effect
const breath = Math.sin(time + baseAngle * 3) * 3;
// Apply transformations
const currentRadius = radius + radialPulse + breath * 0.3;
const currentAngle = baseAngle + Math.sin(flowTime * 0.3) * 0.1;
positions[idx] = Math.cos(currentAngle) * currentRadius;
positions[idx + 1] = Math.sin(currentAngle) * currentRadius;
positions[idx + 2] = initial[idx + 2] + Math.sin(flowTime + baseAngle) * 5;
}
geometry.attributes.position.needsUpdate = true;
glowGeometry.attributes.position.array.set(positions);
glowGeometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
// Click interaction
document.addEventListener('click', () => {
rotationDirection *= -1;
rotationSpeed = ROTATION_SPEED * (1 + Math.random() * 2);
});
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start animation
animate();
</script>
</body>
</html>