318 lines
11 KiB
HTML
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>
|