a
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user