276 lines
9.5 KiB
HTML
276 lines
9.5 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>26 - 金色粒子爆炸</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
overflow: hidden;
|
|
background: #000;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
canvas { display: block; }
|
|
#ui {
|
|
position: fixed;
|
|
top: 0; left: 0; width: 100%; height: 100%;
|
|
pointer-events: none; z-index: 10;
|
|
}
|
|
#title {
|
|
position: absolute;
|
|
top: 25px; left: 30px;
|
|
color: #ffd700;
|
|
font-size: 16px;
|
|
letter-spacing: 4px;
|
|
text-shadow: 0 0 10px #ffaa00;
|
|
}
|
|
#fps {
|
|
position: absolute;
|
|
top: 20px; right: 25px;
|
|
color: #888;
|
|
font-size: 12px;
|
|
}
|
|
#instruction {
|
|
position: absolute;
|
|
bottom: 30px; left: 50%;
|
|
transform: translateX(-50%);
|
|
color: #aaa;
|
|
font-size: 12px;
|
|
letter-spacing: 2px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="canvas"></canvas>
|
|
<div id="ui">
|
|
<div id="title">26 - 金色粒子爆炸</div>
|
|
<div id="fps">FPS: 60</div>
|
|
<div id="instruction">点击画布 · 触发爆炸</div>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
// 粒子池
|
|
const MAX_PARTICLES = 6000;
|
|
const particles = [];
|
|
|
|
class Particle {
|
|
constructor(x, y, type = 'explosion') {
|
|
this.reset(x, y, type);
|
|
}
|
|
|
|
reset(x, y, type) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.type = type;
|
|
|
|
// 初始方向 - 爆炸向外
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = type === 'core' ?
|
|
Math.random() * 3 + 1 : // 核心慢速
|
|
Math.random() * 8 + 2; // 外围快速
|
|
|
|
this.vx = Math.cos(angle) * speed;
|
|
this.vy = Math.sin(angle) * speed;
|
|
|
|
// 基础属性
|
|
this.life = 1.0;
|
|
this.decay = type === 'core' ?
|
|
0.003 + Math.random() * 0.004 : // 核心活得久
|
|
0.008 + Math.random() * 0.012; // 外围衰减快
|
|
|
|
this.size = type === 'core' ?
|
|
2 + Math.random() * 4 : // 核心较大
|
|
1 + Math.random() * 3; // 外围较小
|
|
|
|
this.maxSize = this.size * (2 + Math.random() * 2);
|
|
|
|
// 颜色 - 金色系
|
|
const hue = 35 + Math.random() * 25; // 35-60度,金黄到橙黄
|
|
const sat = 80 + Math.random() * 20; // 高饱和
|
|
const light = type === 'core' ?
|
|
70 + Math.random() * 30 : // 核心亮
|
|
50 + Math.random() * 40; // 外围变化
|
|
|
|
this.hue = hue;
|
|
this.sat = sat;
|
|
this.light = light;
|
|
|
|
// 拖尾
|
|
this.history = [];
|
|
this.historyLength = type === 'core' ? 8 : 5;
|
|
}
|
|
|
|
update() {
|
|
// 保存历史位置(拖尾)
|
|
this.history.push({ x: this.x, y: this.y });
|
|
if (this.history.length > this.historyLength) {
|
|
this.history.shift();
|
|
}
|
|
|
|
// 移动
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
// 阻力 - 逐渐减速
|
|
this.vx *= 0.985;
|
|
this.vy *= 0.985;
|
|
|
|
// 重力 - 轻微下落
|
|
this.vy += 0.02;
|
|
|
|
// 大小变化 - 先膨胀后收缩
|
|
if (this.life > 0.7) {
|
|
this.size += 0.15;
|
|
} else {
|
|
this.size -= 0.08;
|
|
}
|
|
this.size = Math.max(0.5, Math.min(this.size, this.maxSize));
|
|
|
|
// 生命衰减
|
|
this.life -= this.decay;
|
|
|
|
return this.life > 0 && this.size > 0.3;
|
|
}
|
|
|
|
draw(ctx) {
|
|
const alpha = this.life * (this.type === 'core' ? 1 : 0.9);
|
|
|
|
// 绘制拖尾
|
|
if (this.history.length > 1) {
|
|
for (let i = 0; i < this.history.length - 1; i++) {
|
|
const p1 = this.history[i];
|
|
const p2 = this.history[i + 1];
|
|
const trailAlpha = alpha * (i / this.history.length) * 0.5;
|
|
|
|
ctx.strokeStyle = `hsla(${this.hue}, ${this.sat}%, ${this.light}%, ${trailAlpha})`;
|
|
ctx.lineWidth = this.size * (i / this.history.length);
|
|
ctx.beginPath();
|
|
ctx.moveTo(p1.x, p1.y);
|
|
ctx.lineTo(p2.x, p2.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// 绘制粒子本体 - 多层光晕
|
|
for (let i = 3; i >= 0; i--) {
|
|
const r = this.size * (1 + i * 0.8);
|
|
const a = alpha * (1 - i * 0.25);
|
|
|
|
const g = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, r);
|
|
g.addColorStop(0, `hsla(${this.hue}, ${this.sat}%, ${this.light + 20}%, ${a})`);
|
|
g.addColorStop(0.3, `hsla(${this.hue}, ${this.sat}%, ${this.light}%, ${a * 0.7})`);
|
|
g.addColorStop(0.7, `hsla(${this.hue}, ${this.sat - 20}%, ${this.light - 10}%, ${a * 0.3})`);
|
|
g.addColorStop(1, `hsla(${this.hue}, ${this.sat - 40}%, ${this.light - 20}%, 0)`);
|
|
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 爆炸函数
|
|
function createExplosion(x, y) {
|
|
// 核心粒子 - 金色最亮
|
|
for (let i = 0; i < 800; i++) {
|
|
if (particles.length < MAX_PARTICLES) {
|
|
particles.push(new Particle(x, y, 'core'));
|
|
}
|
|
}
|
|
|
|
// 外围粒子 - 更多更散
|
|
for (let i = 0; i < 2000; i++) {
|
|
if (particles.length < MAX_PARTICLES) {
|
|
const offset = (Math.random() - 0.5) * 40;
|
|
particles.push(new Particle(x + offset, y + offset, 'outer'));
|
|
}
|
|
}
|
|
|
|
// 闪光粒子 - 极小极亮
|
|
for (let i = 0; i < 500; i++) {
|
|
if (particles.length < MAX_PARTICLES) {
|
|
const p = new Particle(x, y, 'spark');
|
|
p.vx *= 1.5;
|
|
p.vy *= 1.5;
|
|
p.size = 0.5 + Math.random();
|
|
p.light = 90;
|
|
particles.push(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始爆炸
|
|
function init() {
|
|
createExplosion(canvas.width / 2, canvas.height / 2);
|
|
}
|
|
|
|
// 动画循环
|
|
let frames = 0;
|
|
let lastTime = performance.now();
|
|
const fpsEl = document.getElementById('fps');
|
|
|
|
function animate() {
|
|
// 半透明覆盖 - 拖尾效果
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 更新和绘制粒子
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const p = particles[i];
|
|
const alive = p.update();
|
|
if (alive) {
|
|
p.draw(ctx);
|
|
} else {
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// 自动补充爆炸(可选)
|
|
if (particles.length < 200 && Math.random() > 0.95) {
|
|
createExplosion(
|
|
canvas.width * 0.3 + Math.random() * canvas.width * 0.4,
|
|
canvas.height * 0.3 + Math.random() * canvas.height * 0.4
|
|
);
|
|
}
|
|
|
|
// FPS
|
|
frames++;
|
|
const now = performance.now();
|
|
if (now - lastTime >= 1000) {
|
|
fpsEl.textContent = 'FPS: ' + frames + ' | Particles: ' + particles.length;
|
|
frames = 0;
|
|
lastTime = now;
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// 交互
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
createExplosion(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
const t = e.touches[0];
|
|
createExplosion(t.clientX, t.clientY);
|
|
}, { passive: false });
|
|
|
|
init();
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html>
|