<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>心跳爱心 + 流星雨</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = 150;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// ------------------ 爱心粒子 ------------------
const heartParticleCount = 10000;
const heartScale = 3.2;
const heartZMin = -10, heartZMax = 10;
const heartPositions = new Float32Array(heartParticleCount * 3);
const heartSizes = new Float32Array(heartParticleCount);
let accepted = 0;
while (accepted < heartParticleCount) {
const t = Math.random() * Math.PI * 2;
const s = Math.pow(Math.random(), 1/6);
let x = 16 * Math.pow(Math.sin(t), 3);
let y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
x = x * s * heartScale;
y = y * s * heartScale;
const radial = Math.sqrt(x * x + y * y);
if (radial < 10 && Math.random() < 0.8) continue;
heartPositions[accepted * 3] = x;
heartPositions[accepted * 3 + 1] = y;
heartPositions[accepted * 3 + 2] = (Math.random() - 0.5) * (heartZMax - heartZMin);
heartSizes[accepted] = 1.0;
accepted++;
}
const heartGeometry = new THREE.BufferGeometry();
heartGeometry.setAttribute('position', new THREE.BufferAttribute(heartPositions, 3));
heartGeometry.setAttribute('size', new THREE.BufferAttribute(heartSizes, 1));
const heartUniforms = {
pointColor: { value: new THREE.Color(0xff69b4) },
opacity: { value: 0.9 }
};
const vertexShader = `
attribute float size;
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform vec3 pointColor;
uniform float opacity;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
gl_FragColor = vec4(pointColor, opacity);
}
`;
const heartMaterial = new THREE.ShaderMaterial({
uniforms: heartUniforms,
vertexShader,
fragmentShader,
transparent: true,
depthWrite: false
});
const heartPoints = new THREE.Points(heartGeometry, heartMaterial);
scene.add(heartPoints);
// ------------------ 掉落粒子 & 漩涡 ------------------
const fallingParticles = [];
const groundParticles = [];
let spawnInterval = 100;
let lastSpawnTime = 0;
const gravity = 0.05;
const fallThresholdY = -60;
function spawnFallingParticle() {
const index = Math.floor(Math.random() * heartParticleCount);
const i3 = index * 3;
const pos = new THREE.Vector3(
heartPositions[i3],
heartPositions[i3 + 1],
heartPositions[i3 + 2]
);
pos.x += (Math.random() - 0.5) * 2;
pos.y += (Math.random() - 0.5) * 2;
pos.z += (Math.random() - 0.5) * 2;
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 0.2,
-(Math.random() * 0.5 + 0.5),
(Math.random() - 0.5) * 0.2
);
fallingParticles.push({ position: pos, velocity: vel });
}
let fallingGeometry = new THREE.BufferGeometry();
let fallingPoints;
function initFallingPoints() {
fallingGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
const mat = new THREE.PointsMaterial({
color: 0xff69b4,
size: 0.8,
transparent: true,
opacity: 0.9,
depthWrite: false
});
fallingPoints = new THREE.Points(fallingGeometry, mat);
scene.add(fallingPoints);
}
initFallingPoints();
let groundGeometry = new THREE.BufferGeometry();
let groundPoints;
function initGroundPoints() {
groundGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
const mat = new THREE.PointsMaterial({
color: 0xff69b4,
size: 0.8,
transparent: true,
opacity: 0.9,
depthWrite: false
});
groundPoints = new THREE.Points(groundGeometry, mat);
scene.add(groundPoints);
}
initGroundPoints();
const vortexCenter = new THREE.Vector3(0, fallThresholdY, 0);
const swirlSpeed = 0.5;
// ------------------ 流星雨 ------------------
const maxMeteors = 150;
const meteors = [];
function createMeteor() {
const group = new THREE.Group();
const crossMat = new THREE.MeshBasicMaterial({
color: 0x99ccff,
transparent: true,
opacity: 0.9,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide
});
const planeGeo = new THREE.PlaneGeometry(4, 0.5);
const cross1 = new THREE.Mesh(planeGeo, crossMat);
const cross2 = new THREE.Mesh(planeGeo, crossMat);
cross2.rotation.z = Math.PI / 2;
group.add(cross1);
group.add(cross2);
const tailGeo = new THREE.ConeGeometry(0.5, 8, 8, 1, true);
const tailMat = new THREE.MeshBasicMaterial({
color: 0x00ccff,
transparent: true,
opacity: 0.6,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide
});
const tail = new THREE.Mesh(tailGeo, tailMat);
tail.position.y = -4;
tail.rotation.x = Math.PI;
group.add(tail);
group.position.set(
Math.random() * 400 - 200,
200 + Math.random() * 100,
Math.random() * 400 - 200
);
const velocity = new THREE.Vector3(-1.5, -3, 0).multiplyScalar(0.8 + Math.random() * 0.4);
scene.add(group);
meteors.push({ mesh: group, velocity });
}
// ------------------ 动画循环 ------------------
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsed = clock.elapsedTime * 1000;
const beatFactor = 1 + 0.05 * Math.sin(clock.elapsedTime * Math.PI * 2);
heartPoints.scale.set(beatFactor, beatFactor, beatFactor);
if (elapsed - lastSpawnTime > spawnInterval) {
spawnFallingParticle();
lastSpawnTime = elapsed;
}
for (let i = fallingParticles.length - 1; i >= 0; i--) {
const p = fallingParticles[i];
p.velocity.y -= gravity;
p.position.addScaledVector(p.velocity, delta * 60);
if (p.position.y < fallThresholdY) {
const dx = p.position.x - vortexCenter.x;
const dz = p.position.z - vortexCenter.z;
const radius = Math.sqrt(dx*dx + dz*dz);
const angle = Math.atan2(dz, dx);
groundParticles.push({
position: new THREE.Vector3(p.position.x, fallThresholdY, p.position.z),
radius, angle
});
fallingParticles.splice(i, 1);
}
}
const fallingPositions = new Float32Array(fallingParticles.length * 3);
fallingParticles.forEach((p, idx) => {
fallingPositions[idx * 3] = p.position.x;
fallingPositions[idx * 3 + 1] = p.position.y;
fallingPositions[idx * 3 + 2] = p.position.z;
});
fallingGeometry.setAttribute('position', new THREE.BufferAttribute(fallingPositions, 3));
fallingGeometry.attributes.position.needsUpdate = true;
fallingGeometry.setDrawRange(0, fallingParticles.length);
if (groundParticles.length > 50) {
groundParticles.forEach(p => {
p.angle += swirlSpeed * delta;
p.position.x = vortexCenter.x + p.radius * Math.cos(p.angle);
p.position.z = vortexCenter.z + p.radius * Math.sin(p.angle);
p.position.y = fallThresholdY;
});
}
const groundPositions = new Float32Array(groundParticles.length * 3);
groundParticles.forEach((p, idx) => {
groundPositions[idx * 3] = p.position.x;
groundPositions[idx * 3 + 1] = p.position.y;
groundPositions[idx * 3 + 2] = p.position.z;
});
groundGeometry.setAttribute('position', new THREE.BufferAttribute(groundPositions, 3));
groundGeometry.attributes.position.needsUpdate = true;
groundGeometry.setDrawRange(0, groundParticles.length);
// 更新流星雨
if (elapsed - lastSpawnTime > spawnInterval / 2 && meteors.length < maxMeteors) {
createMeteor();
}
for (let i = meteors.length - 1; i >= 0; i--) {
const m = meteors[i];
m.mesh.position.addScaledVector(m.velocity, delta * 60);
m.mesh.rotation.z += delta * 2;
if (m.mesh.position.y < -120 || m.mesh.position.x < -220) {
scene.remove(m.mesh);
meteors.splice(i, 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>
3D粒子爱心+流星
于 2025-04-14 17:47:14 首次发布