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

367 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>15 - FIREWORKS | 中国烟花</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
overflow: hidden;
font-family: 'Space Mono', monospace;
}
#canvas-container { width: 100vw; height: 100vh; }
.info {
position: fixed; top: 20px; left: 20px;
color: #ffd700; font-size: 14px; pointer-events: none; z-index: 100;
text-shadow: 0 0 10px rgba(255,215,0,0.8);
background: rgba(0,0,0,0.5); padding: 15px; border-radius: 8px;
}
.info h1 { font-size: 20px; margin-bottom: 8px; }
.info p { opacity: 0.9; font-size: 12px; margin: 4px 0; }
.controls {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%);
display: flex; gap: 15px;
background: rgba(0,0,0,0.7);
padding: 15px 25px; border-radius: 30px;
border: 1px solid rgba(255,215,0,0.3);
}
.btn {
background: linear-gradient(135deg, #ff0040, #ff6b00);
border: none; color: white; padding: 10px 20px;
border-radius: 20px; cursor: pointer; font-family: inherit;
font-size: 12px; transition: all 0.3s;
box-shadow: 0 4px 15px rgba(255,0,64,0.4);
}
.btn:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(255,0,64,0.6); }
.btn.gold { background: linear-gradient(135deg, #ffd700, #ffaa00); color: #333; }
.blessing {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 80px; color: #ffd700;
text-shadow: 0 0 30px rgba(255,215,0,0.8), 0 0 60px rgba(255,0,0,0.5);
opacity: 0; pointer-events: none;
transition: opacity 0.5s;
font-family: 'STKaiti', 'KaiTi', serif;
}
.blessing.show { opacity: 1; animation: float 3s ease-in-out infinite; }
@keyframes float {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -55%) scale(1.05); }
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div class="info">
<h1>15 - FIREWORKS 🧨</h1>
<p>中国传统烟花 - 爆竹声中一岁除</p>
<p>粒子: <span id="pcount">0</span> | 火箭: <span id="rcount">0</span></p>
<p>FPS: <span id="fps">0</span></p>
</div>
<div class="controls">
<button class="btn" onclick="launchFirework()">🎆 发射烟花</button>
<button class="btn gold" onclick="launchBlessing()">🧧 祝福</button>
<button class="btn" onclick="toggleAuto()" id="autoBtn">▶ 自动</button>
</div>
<div class="blessing" id="blessing">恭喜发财</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ========== 配置 ==========
const GRAVITY = -0.15;
// 颜色表
const COLORS = [
0xff0000, // 红
0xffd700, // 金
0xff6600, // 橘
0xff00ff, // 紫
0x00ff00, // 绿
0x00ffff, // 青
0xff1493, // 粉
0xffff00, // 黄
];
// ========== Three.js ==========
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x090a0f, 0.002);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 3000);
camera.position.set(0, 60, 250);
camera.lookAt(0, 130, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// ========== 发光粒子纹理 ==========
function createGlowTexture() {
const c = document.createElement('canvas');
c.width = 64; c.height = 64;
const x = c.getContext('2d');
const g = x.createRadialGradient(32,32,0, 32,32,32);
g.addColorStop(0, 'rgba(255,255,255,1)');
g.addColorStop(0.2, 'rgba(255,255,255,0.8)');
g.addColorStop(0.5, 'rgba(255,255,255,0.3)');
g.addColorStop(1, 'rgba(255,255,255,0)');
x.fillStyle = g;
x.fillRect(0,0,64,64);
return new THREE.CanvasTexture(c);
}
const glowTexture = createGlowTexture();
const particleMaterial = new THREE.SpriteMaterial({
map: glowTexture,
color: 0xffffff,
blending: THREE.AdditiveBlending,
depthWrite: false
});
// ========== 数据 ==========
let particles = []; // Sprite对象
let rockets = [];
let autoLaunch = false;
let autoTimer = 0;
const pcountEl = document.getElementById('pcount');
const rcountEl = document.getElementById('rcount');
const fpsEl = document.getElementById('fps');
// ========== 创建爆炸 ==========
function explode(x, y, z, color, pattern) {
const count = 200 + Math.floor(Math.random() * 300);
for (let i = 0; i < count; i++) {
let vx, vy, vz;
// 稍微降低扩散速度,让花朵大小更协调
const speed = 0.6 + Math.random() * 0.8;
switch(pattern) {
case 'heart':
const t = Math.random() * Math.PI * 2;
const sc = 0.08;
vx = 16 * Math.pow(Math.sin(t), 3) * sc;
vy = (13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)) * sc;
vz = (Math.random() - 0.5) * 2;
break;
case 'ring':
const angle = Math.random() * Math.PI * 2;
vx = Math.cos(angle) * speed * 1.2;
vy = (Math.random() - 0.5) * 0.2;
vz = Math.sin(angle) * speed * 1.2;
break;
case 'willow':
const wa = Math.random() * Math.PI * 2;
vx = Math.cos(wa) * speed * 0.5;
vy = -Math.abs(Math.sin(wa)) * speed * 0.5 - 0.5;
vz = Math.sin(wa) * speed * 0.5;
break;
default:
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
vx = Math.sin(phi) * Math.cos(theta) * speed;
vy = Math.sin(phi) * Math.sin(theta) * speed;
vz = Math.cos(phi) * speed;
}
const sprite = new THREE.Sprite(particleMaterial.clone());
sprite.position.set(x, y, z);
sprite.scale.set(3, 3, 1);
sprite.material.color.setHex(color);
scene.add(sprite);
particles.push({
sprite: sprite,
vx: vx, vy: vy, vz: vz,
life: 1.0,
decay: 0.005 + Math.random() * 0.005,
baseScale: 2 + Math.random() * 2
});
}
}
// ========== 火箭 ==========
function createRocket(targetX, targetY, targetZ, color) {
const startX = (Math.random() - 0.5) * 30;
const startZ = (Math.random() - 0.5) * 20;
return {
x: startX,
y: -50,
z: startZ,
vx: (targetX - startX) * 0.012,
vy: 2.8 + Math.random() * 1.0,
vz: (targetZ - startZ) * 0.012,
targetY: targetY,
color: color,
trail: [],
trailSprites: [],
exploded: false
};
}
function launchFirework() {
const x = (Math.random() - 0.5) * 50;
// 再提高100%,让上升路径更长更壮观
const y = 110 + Math.random() * 100;
const z = (Math.random() - 0.5) * 30;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
rockets.push(createRocket(x, y, z, color));
}
function launchBlessing() {
const blessings = ['恭喜发财', '万事如意', '心想事成', '大吉大利', '年年有余'];
const el = document.getElementById('blessing');
el.textContent = blessings[Math.floor(Math.random() * blessings.length)];
el.classList.add('show');
for (let i = 0; i < 6; i++) {
setTimeout(() => {
const angle = (i / 6) * Math.PI * 2;
// 祝福模式飞得更高
rockets.push(createRocket(Math.cos(angle) * 15, 150, Math.sin(angle) * 15, COLORS[i % COLORS.length]));
}, i * 150);
}
setTimeout(() => el.classList.remove('show'), 3000);
}
function toggleAuto() {
autoLaunch = !autoLaunch;
document.getElementById('autoBtn').textContent = autoLaunch ? '⏸ 停止' : '▶ 自动';
}
// ========== 场景装饰 ==========
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(300, 300),
new THREE.MeshBasicMaterial({ color: 0x050508, transparent: true, opacity: 0.3 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -30;
scene.add(ground);
for (let i = 0; i < 25; i++) {
const h = 10 + Math.random() * 30;
const b = new THREE.Mesh(
new THREE.BoxGeometry(4 + Math.random() * 5, h, 4 + Math.random() * 5),
new THREE.MeshBasicMaterial({ color: 0x0a0a12 })
);
b.position.set((Math.random() - 0.5) * 150, -30 + h/2, -60 - Math.random() * 60);
scene.add(b);
}
// ========== 动画循环 ==========
let lastTime = performance.now(), frames = 0;
setTimeout(() => {
launchFirework();
setTimeout(launchFirework, 500);
setTimeout(launchFirework, 1000);
}, 300);
function animate() {
requestAnimationFrame(animate);
frames++;
const now = performance.now();
if (now - lastTime >= 1000) {
fpsEl.textContent = frames;
frames = 0;
lastTime = now;
pcountEl.textContent = particles.length;
rcountEl.textContent = rockets.length;
}
if (autoLaunch) {
autoTimer++;
if (autoTimer > 40) {
launchFirework();
autoTimer = 0;
}
}
// 更新火箭
for (let i = rockets.length - 1; i >= 0; i--) {
const r = rockets[i];
if (r.exploded) {
rockets.splice(i, 1);
continue;
}
r.x += r.vx;
r.y += r.vy;
r.z += r.vz;
r.vy += GRAVITY * 0.3;
r.vx *= 0.995;
r.vz *= 0.995;
// 拖尾
if (Math.random() > 0.3) {
const trailSprite = new THREE.Sprite(particleMaterial.clone());
trailSprite.position.set(r.x, r.y, r.z);
trailSprite.scale.set(2, 2, 1);
trailSprite.material.color.setHex(r.color);
scene.add(trailSprite);
r.trailSprites.push({ sprite: trailSprite, life: 1.0 });
}
if (r.vy <= 0 || r.y >= r.targetY) {
const patterns = ['sphere', 'ring', 'heart', 'willow'];
explode(r.x, r.y, r.z, r.color, patterns[Math.floor(Math.random() * patterns.length)]);
rockets.splice(i, 1);
}
}
// 清理拖尾
rockets.forEach(r => {
for (let i = r.trailSprites.length - 1; i >= 0; i--) {
const t = r.trailSprites[i];
t.life -= 0.05;
if (t.life <= 0) {
scene.remove(t.sprite);
r.trailSprites.splice(i, 1);
} else {
t.sprite.scale.setScalar(t.life * 2);
t.sprite.material.opacity = t.life;
}
}
});
// 更新爆炸粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.sprite.position.x += p.vx;
p.sprite.position.y += p.vy;
p.sprite.position.z += p.vz;
p.vy += GRAVITY;
p.vx *= 0.985;
p.vy *= 0.985;
p.vz *= 0.985;
p.life -= p.decay;
if (p.life <= 0) {
scene.remove(p.sprite);
particles.splice(i, 1);
} else {
const s = p.baseScale * p.life;
p.sprite.scale.set(s, s, 1);
p.sprite.material.opacity = p.life;
}
}
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>