diff --git a/public/js/effects/effect-manager.js b/public/js/effects/effect-manager.js index 8b1c5aa..2f1d3fc 100644 --- a/public/js/effects/effect-manager.js +++ b/public/js/effects/effect-manager.js @@ -80,6 +80,12 @@ const EffectManager = (() => { FireworksEffect.start(canvas, _cleanup); } break; + case "wedding-fireworks": + // 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒 + if (typeof FireworksEffect !== "undefined") { + FireworksEffect.startDouble(canvas, _cleanup); + } + break; case "rain": if (typeof RainEffect !== "undefined") { RainEffect.start(canvas, _cleanup); diff --git a/public/js/effects/fireworks.js b/public/js/effects/fireworks.js index f1063fa..ac3c9f5 100644 --- a/public/js/effects/fireworks.js +++ b/public/js/effects/fireworks.js @@ -234,7 +234,7 @@ const FireworksEffect = (() => { } /** - * 启动烟花特效 + * 启动烟花特效(普通版) * * @param {HTMLCanvasElement} canvas 全屏 Canvas * @param {Function} onEnd 特效结束回调 @@ -251,7 +251,6 @@ const FireworksEffect = (() => { let launchCnt = 0; const MAX_LAUNCHES = 12; - // 定时发射火箭 const launchInterval = setInterval(() => { if (launchCnt >= MAX_LAUNCHES) { clearInterval(launchInterval); @@ -261,8 +260,7 @@ const FireworksEffect = (() => { 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, h); // 传入画布高度 - rockets.push(rocket); + rockets.push(new Rocket(x, ty, color, type, h)); launchCnt++; }, 600); @@ -271,11 +269,9 @@ const FireworksEffect = (() => { function animate(now) { ctx.clearRect(0, 0, w, h); - // 更新和绘制火箭 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); @@ -285,7 +281,6 @@ const FireworksEffect = (() => { } } - // 更新和绘制粒子 particles = particles.filter((p) => p.alive); particles.forEach((p) => { p.update(); @@ -305,5 +300,116 @@ const FireworksEffect = (() => { animId = requestAnimationFrame(animate); } - return { start }; + /** + * 启动婚礼加倍烟花特效(双侧轮流发射,粒子增倍,持续更久) + * + * @param {HTMLCanvasElement} canvas 全屏 Canvas + * @param {Function} onEnd 特效结束回调 + */ + function startDouble(canvas, onEnd) { + const ctx = canvas.getContext("2d"); + const w = canvas.width; + const h = canvas.height; + const DURATION = 12000; // 比普通多 2 秒 + + let rockets = []; + let particles = []; + let animId = null; + let launchCnt = 0; + const MAX_LAUNCHES = 24; // 双倍火箭数 + + // 婚礼专属浪漫色组(增加金色/粉色) + const WEDDING_COLORS = [ + "#ff2266", + "#ff66aa", + "#ff99cc", // 粉红系 + "#ffcc00", + "#ffdd44", + "#fff066", // 金黄系 + "#cc44ff", + "#ff44cc", + "#aa00ff", // 紫色系 + "#ff4400", + "#ff8800", + "#00ddff", // 其他 + ]; + + // 定时从左右两侧交替发射 + const launchInterval = setInterval(() => { + if (launchCnt >= MAX_LAUNCHES) { + clearInterval(launchInterval); + return; + } + // 左右交替:偶数从左侧1/3,奇数从右侧2/3 + const isLeft = launchCnt % 2 === 0; + const x = isLeft + ? w * (0.05 + Math.random() * 0.4) + : w * (0.55 + Math.random() * 0.4); + const ty = h * (0.05 + Math.random() * 0.4); + const color = + WEDDING_COLORS[ + Math.floor(Math.random() * WEDDING_COLORS.length) + ]; + const type = TYPES[Math.floor(Math.random() * TYPES.length)]; + rockets.push(new Rocket(x, ty, color, type, h)); + launchCnt++; + }, 400); // 发射间隔缩短到 400ms,密度加倍 + + // 额外:开场同时发射3枚双侧礼炮 + setTimeout(() => { + [0.15, 0.5, 0.85].forEach((xRatio) => { + const color = + WEDDING_COLORS[ + Math.floor(Math.random() * WEDDING_COLORS.length) + ]; + rockets.push( + new Rocket(w * xRatio, h * 0.1, color, "sphere", h), + ); + }); + }, 100); + + const startTime = performance.now(); + + function animate(now) { + ctx.clearRect(0, 0, w, h); + + for (let i = rockets.length - 1; i >= 0; i--) { + const r = rockets[i]; + if (r.done) { + // 婚礼爆炸:粒子数×1.5(在 _burst 基础上额外补充50粒) + const burst = _burst(r.x, r.y, r.color, r.type); + // 额外补充粒子(心形/大颗) + for (let j = 0; j < 50; j++) { + const p = new Particle(r.x, r.y, r.color, "sphere", 0); + p.radius = Math.random() * 3 + 1; + burst.push(p); + } + 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); + }); + + if (now - startTime < DURATION) { + animId = requestAnimationFrame(animate); + } else { + clearInterval(launchInterval); + cancelAnimationFrame(animId); + ctx.clearRect(0, 0, w, h); + onEnd(); + } + } + + animId = requestAnimationFrame(animate); + } + + return { start, startDouble }; })(); diff --git a/resources/views/chat/partials/marriage-modals.blade.php b/resources/views/chat/partials/marriage-modals.blade.php index 48e4306..a39ca71 100644 --- a/resources/views/chat/partials/marriage-modals.blade.php +++ b/resources/views/chat/partials/marriage-modals.blade.php @@ -673,9 +673,9 @@ const me = window.chatContext.username; this.isNewlywed = (groomName === me || brideName === me); this.show = true; - // 播放烟花特效 + // 播放婚礼专属双倍礼花特效(全员) if (window.EffectManager) { - window.EffectManager.play('fireworks'); + window.EffectManager.play('wedding-fireworks'); } },