Feat: 烟花特效完全重写,火箭升空+三种爆炸形态(球形/柳叶/环形)+粒子尾迹
This commit is contained in:
+212
-45
@@ -1,41 +1,168 @@
|
||||
/**
|
||||
* 文件功能:聊天室烟花特效
|
||||
* 文件功能:聊天室烟花特效(真实感版本)
|
||||
*
|
||||
* 使用 Canvas 粒子系统在全屏播放多发烟花爆炸动画。
|
||||
* 粒子加大、加发光描边,在浅色背景上也清晰可见。
|
||||
* 特效总时长约 5 秒,结束后自动清理并回调。
|
||||
* 完整模拟真实烟花流程:
|
||||
* 1. 火箭从底部带着尾迹飞向目标高度
|
||||
* 2. 到达位置后爆炸,产生 3 种形态:球形/柳叶/星形
|
||||
* 3. 每颗粒子带历史轨迹尾巴,随重力下坠并消散
|
||||
*
|
||||
* 全程透明 Canvas,不遮挡聊天背景。
|
||||
*/
|
||||
|
||||
const FireworksEffect = (() => {
|
||||
// 粒子类:模拟一个爆炸后的发光粒子
|
||||
// ─── 火箭类 ──────────────────────────────────────────
|
||||
class Rocket {
|
||||
/**
|
||||
* @param {number} x 发射 x 位置
|
||||
* @param {number} targetY 爆炸目标高度(距顶部比例)
|
||||
* @param {string} color 爆炸颜色
|
||||
* @param {string} type 爆炸类型:sphere / willow / ring
|
||||
*/
|
||||
constructor(x, targetY, color, type) {
|
||||
this.x = x;
|
||||
this.y = 9999; // 在 start() 里设置为 canvas.height
|
||||
this.targetY = targetY;
|
||||
this.color = color;
|
||||
this.type = type;
|
||||
this.vy = -(12 + Math.random() * 4); // 上升速度
|
||||
this.trail = []; // 尾迹历史坐标
|
||||
this.exploded = false;
|
||||
this.done = false;
|
||||
}
|
||||
|
||||
/** 更新火箭位置,到达目标高度后标记为已爆炸 */
|
||||
update() {
|
||||
this.trail.push({ x: this.x, y: this.y });
|
||||
if (this.trail.length > 10) this.trail.shift();
|
||||
this.y += this.vy;
|
||||
this.vy *= 0.98; // 轻微减速(仿真阻力)
|
||||
if (this.y <= this.targetY) {
|
||||
this.exploded = true;
|
||||
this.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** 绘制火箭(白色头部 + 橙色尾迹) */
|
||||
draw(ctx) {
|
||||
// 尾迹(由新到旧逐渐变淡)
|
||||
for (let i = 0; i < this.trail.length; i++) {
|
||||
const a = (i / this.trail.length) * 0.6;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = a;
|
||||
ctx.fillStyle = "#ffaa44";
|
||||
ctx.shadowColor = "#ff6600";
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.trail[i].x, this.trail[i].y, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
// 火箭头部(亮白色光点)
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.shadowColor = "#ffcc88";
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 爆炸粒子类 ──────────────────────────────────────
|
||||
class Particle {
|
||||
constructor(x, y, color) {
|
||||
/**
|
||||
* @param {number} x 爆炸中心 x
|
||||
* @param {number} y 爆炸中心 y
|
||||
* @param {string} color 粒子颜色
|
||||
* @param {string} type 爆炸类型
|
||||
* @param {number} angle 发射角度(ring 类型用)
|
||||
*/
|
||||
constructor(x, y, color, type, angle) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.color = color;
|
||||
// 随机方向和速度
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = Math.random() * 7 + 3;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
this.trail = [];
|
||||
|
||||
let speed;
|
||||
if (type === "ring") {
|
||||
// 环形:均匀角度,固定速度
|
||||
speed = 5 + Math.random() * 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
this.gravity = 0.06;
|
||||
this.decay = 0.014;
|
||||
this.radius = 2;
|
||||
} else if (type === "willow") {
|
||||
// 柳叶:慢速,在空中下垂
|
||||
speed = Math.random() * 3 + 1;
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
this.vx = Math.cos(a) * speed;
|
||||
this.vy = Math.sin(a) * speed - 2; // 初速稍微向上
|
||||
this.gravity = 0.07;
|
||||
this.decay = 0.009; // 衰减慢,拖出长尾
|
||||
this.radius = 1.5;
|
||||
} else {
|
||||
// sphere:标准球形爆炸
|
||||
speed = Math.random() * 6 + 2;
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
this.vx = Math.cos(a) * speed;
|
||||
this.vy = Math.sin(a) * speed;
|
||||
this.gravity = 0.1;
|
||||
this.decay = 0.016;
|
||||
this.radius = Math.random() * 2 + 1.5;
|
||||
}
|
||||
|
||||
this.alpha = 1;
|
||||
this.gravity = 0.12;
|
||||
this.decay = Math.random() * 0.01 + 0.01; // 衰减略慢,显色更久
|
||||
this.radius = Math.random() * 4 + 2; // 增大粒子半径
|
||||
// 部分粒子有闪烁效果
|
||||
this.sparkle = Math.random() > 0.6;
|
||||
this.frame = 0;
|
||||
}
|
||||
|
||||
/** 每帧更新粒子位置和状态 */
|
||||
/** 更新粒子物理状态 */
|
||||
update() {
|
||||
this.frame++;
|
||||
// 保存轨迹历史(尾迹长度由透明度控制)
|
||||
this.trail.push({ x: this.x, y: this.y });
|
||||
if (this.trail.length > 6) this.trail.shift();
|
||||
|
||||
this.vy += this.gravity;
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.vx *= 0.98;
|
||||
this.vy *= 0.98;
|
||||
this.alpha -= this.decay;
|
||||
this.vx *= 0.985;
|
||||
this.vy *= 0.985;
|
||||
|
||||
// 闪烁:每隔几帧透明度轻微抖动
|
||||
if (this.sparkle && this.frame % 4 === 0) {
|
||||
this.alpha = Math.max(
|
||||
0,
|
||||
this.alpha - this.decay * (0.5 + Math.random()),
|
||||
);
|
||||
} else {
|
||||
this.alpha -= this.decay;
|
||||
}
|
||||
}
|
||||
|
||||
/** 绘制粒子(发光效果,在浅色背景上也突出) */
|
||||
/** 绘制粒子及其运动轨迹尾迹 */
|
||||
draw(ctx) {
|
||||
// 绘制尾迹
|
||||
for (let i = 0; i < this.trail.length; i++) {
|
||||
const a = (i / this.trail.length) * this.alpha * 0.45;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = a;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
this.trail[i].x,
|
||||
this.trail[i].y,
|
||||
this.radius * 0.55,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
// 绘制粒子主体(带发光)
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.max(0, this.alpha);
|
||||
ctx.fillStyle = this.color;
|
||||
@@ -46,32 +173,56 @@ const FireworksEffect = (() => {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
get alive() {
|
||||
return this.alpha > 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
// 预定义烟花颜色组(饱和度高,避免和浅蓝背景撞色)
|
||||
// ─── 预定义颜色 / 类型 ───────────────────────────────
|
||||
const COLORS = [
|
||||
"#ff2200",
|
||||
"#ff7700",
|
||||
"#ffcc00",
|
||||
"#00cc33",
|
||||
"#00dd44",
|
||||
"#cc00ff",
|
||||
"#ff0088",
|
||||
"#00aaff",
|
||||
"#00bbff",
|
||||
"#ff4488",
|
||||
"#ff6600",
|
||||
"#aaff00",
|
||||
"#ff2255",
|
||||
"#ffaa00",
|
||||
];
|
||||
const TYPES = ["sphere", "willow", "ring"];
|
||||
|
||||
/**
|
||||
* 发射一枚烟花,返回粒子数组
|
||||
* 生成一批爆炸粒子
|
||||
*
|
||||
* @param {number} x 爆炸中心 x
|
||||
* @param {number} y 爆炸中心 y
|
||||
* @param {string} color 颜色
|
||||
* @param {string} type 爆炸类型
|
||||
* @returns {Particle[]}
|
||||
*/
|
||||
function _burst(x, y, count) {
|
||||
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||
function _burst(x, y, color, type) {
|
||||
const particles = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push(new Particle(x, y, color));
|
||||
const count = type === "ring" ? 80 : type === "willow" ? 120 : 100;
|
||||
|
||||
if (type === "ring") {
|
||||
// 环形:均匀分布
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = ((Math.PI * 2) / count) * i;
|
||||
particles.push(new Particle(x, y, color, type, angle));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push(new Particle(x, y, color, type, 0));
|
||||
}
|
||||
// willow/sphere 中心加一颗白色闪光核心粒子
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const p = new Particle(x, y, "#ffffff", "sphere", 0);
|
||||
p.decay *= 2;
|
||||
p.radius = 1;
|
||||
particles.push(p);
|
||||
}
|
||||
}
|
||||
return particles;
|
||||
}
|
||||
@@ -86,35 +237,51 @@ const FireworksEffect = (() => {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const DURATION = 5000; // 总时长(ms)
|
||||
const DURATION = 6000;
|
||||
|
||||
let rockets = [];
|
||||
let particles = [];
|
||||
let animId = null;
|
||||
let launchCount = 0;
|
||||
const MAX_LAUNCHES = 10; // 总发射枚数(增加)
|
||||
let launchCnt = 0;
|
||||
const MAX_LAUNCHES = 8;
|
||||
|
||||
// 定时发射烟花
|
||||
// 定时发射火箭
|
||||
const launchInterval = setInterval(() => {
|
||||
if (launchCount >= MAX_LAUNCHES) {
|
||||
if (launchCnt >= MAX_LAUNCHES) {
|
||||
clearInterval(launchInterval);
|
||||
return;
|
||||
}
|
||||
const x = w * (0.1 + Math.random() * 0.8);
|
||||
const y = h * (0.05 + Math.random() * 0.5);
|
||||
const cnt = Math.floor(Math.random() * 50) + 80; // 每枚 80-130 粒子(增多)
|
||||
particles = particles.concat(_burst(x, y, cnt));
|
||||
launchCount++;
|
||||
}, 450);
|
||||
const x = w * (0.15 + Math.random() * 0.7);
|
||||
const ty = h * (0.08 + Math.random() * 0.45);
|
||||
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||
const type = TYPES[Math.floor(Math.random() * TYPES.length)];
|
||||
const rocket = new Rocket(x, ty, color, type);
|
||||
rocket.y = h; // 从底部发射
|
||||
rockets.push(rocket);
|
||||
launchCnt++;
|
||||
}, 600);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// 动画循环
|
||||
function animate(now) {
|
||||
// 清除画布(透明,不遮挡聊天背景)
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// 更新并绘制存活粒子
|
||||
particles = particles.filter((p) => p.alpha > 0.02);
|
||||
// 更新和绘制火箭
|
||||
for (let i = rockets.length - 1; i >= 0; i--) {
|
||||
const r = rockets[i];
|
||||
if (r.done) {
|
||||
// 火箭爆炸:生成粒子
|
||||
const burst = _burst(r.x, r.y, r.color, r.type);
|
||||
particles = particles.concat(burst);
|
||||
rockets.splice(i, 1);
|
||||
} else {
|
||||
r.update();
|
||||
r.draw(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新和绘制粒子
|
||||
particles = particles.filter((p) => p.alive);
|
||||
particles.forEach((p) => {
|
||||
p.update();
|
||||
p.draw(ctx);
|
||||
|
||||
Reference in New Issue
Block a user