367 lines
14 KiB
HTML
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>
|