From 4da2d19b1fc7b43cb7bcdb0305715cc8c3486c76 Mon Sep 17 00:00:00 2001 From: lkddi Date: Fri, 27 Feb 2026 14:22:13 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=96=B0=E5=A2=9E=E4=B8=8B=E9=9B=AA?= =?UTF-8?q?=E7=89=B9=E6=95=88=EF=BC=8C=E5=8A=A0=E5=BC=BA=E7=83=9F=E8=8A=B1?= =?UTF-8?q?/=E4=B8=8B=E9=9B=A8=E5=9C=A8=E6=B5=85=E8=89=B2=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E7=9A=84=E6=98=BE=E8=89=B2=EF=BC=88=E5=8F=91=E5=85=89?= =?UTF-8?q?=E7=B2=92=E5=AD=90+=E6=B7=B1=E8=89=B2=E9=9B=A8=E7=BA=BF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AdminCommandController.php | 2 +- public/js/effects/effect-manager.js | 7 +- public/js/effects/fireworks.js | 65 +++++------ public/js/effects/rain.js | 33 +++--- public/js/effects/snow.js | 107 ++++++++++++++++++ resources/views/chat/frame.blade.php | 3 +- .../views/chat/partials/input-bar.blade.php | 3 + 7 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 public/js/effects/snow.js diff --git a/app/Http/Controllers/AdminCommandController.php b/app/Http/Controllers/AdminCommandController.php index c9285d1..0801dba 100644 --- a/app/Http/Controllers/AdminCommandController.php +++ b/app/Http/Controllers/AdminCommandController.php @@ -386,7 +386,7 @@ class AdminCommandController extends Controller { $request->validate([ 'room_id' => 'required|integer', - 'type' => 'required|in:fireworks,rain,lightning', + 'type' => 'required|in:fireworks,rain,lightning,snow', ]); $admin = Auth::user(); diff --git a/public/js/effects/effect-manager.js b/public/js/effects/effect-manager.js index 33c8210..90edca4 100644 --- a/public/js/effects/effect-manager.js +++ b/public/js/effects/effect-manager.js @@ -51,7 +51,7 @@ const EffectManager = (() => { /** * 播放指定特效 * - * @param {string} type 特效类型:fireworks / rain / lightning + * @param {string} type 特效类型:fireworks / rain / lightning / snow */ function play(type) { // 防重入:同时只允许一个特效 @@ -81,6 +81,11 @@ const EffectManager = (() => { LightningEffect.start(canvas, _cleanup); } break; + case "snow": + if (typeof SnowEffect !== "undefined") { + SnowEffect.start(canvas, _cleanup); + } + break; default: console.warn(`[EffectManager] 未知特效类型:${type}`); _cleanup(); diff --git a/public/js/effects/fireworks.js b/public/js/effects/fireworks.js index c64d404..8f6e2b8 100644 --- a/public/js/effects/fireworks.js +++ b/public/js/effects/fireworks.js @@ -2,7 +2,8 @@ * 文件功能:聊天室烟花特效 * * 使用 Canvas 粒子系统在全屏播放多发烟花爆炸动画。 - * 特效总时长约 4 秒,结束后自动清理并回调。 + * 粒子加大、加发光描边,在浅色背景上也清晰可见。 + * 特效总时长约 5 秒,结束后自动清理并回调。 */ const FireworksEffect = (() => { @@ -14,13 +15,13 @@ const FireworksEffect = (() => { this.color = color; // 随机方向和速度 const angle = Math.random() * Math.PI * 2; - const speed = Math.random() * 6 + 2; + const speed = Math.random() * 7 + 3; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.alpha = 1; this.gravity = 0.12; - this.decay = Math.random() * 0.012 + 0.012; // 透明度每帧衰减量 - this.radius = Math.random() * 3 + 1; + this.decay = Math.random() * 0.01 + 0.01; // 衰减略慢,显色更久 + this.radius = Math.random() * 4 + 2; // 增大粒子半径 } /** 每帧更新粒子位置和状态 */ @@ -28,16 +29,18 @@ const FireworksEffect = (() => { this.vy += this.gravity; this.x += this.vx; this.y += this.vy; - this.vx *= 0.98; // 空气阻力 + this.vx *= 0.98; this.vy *= 0.98; this.alpha -= this.decay; } - /** 绘制粒子 */ + /** 绘制粒子(发光效果,在浅色背景上也突出) */ draw(ctx) { ctx.save(); ctx.globalAlpha = Math.max(0, this.alpha); ctx.fillStyle = this.color; + ctx.shadowColor = this.color; + ctx.shadowBlur = 8; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); @@ -45,29 +48,24 @@ const FireworksEffect = (() => { } } - // 预定义烟花颜色组 + // 预定义烟花颜色组(饱和度高,避免和浅蓝背景撞色) const COLORS = [ - "#ff4444", - "#ff8800", - "#ffdd00", - "#44ff44", - "#44ddff", - "#8844ff", - "#ff44cc", - "#ffffff", - "#ffaaaa", - "#aaffaa", - "#aaaaff", - "#ffffaa", + "#ff2200", + "#ff7700", + "#ffcc00", + "#00cc33", + "#cc00ff", + "#ff0088", + "#00aaff", + "#ff4488", + "#ff6600", + "#aaff00", + "#ff2255", + "#ffaa00", ]; /** * 发射一枚烟花,返回粒子数组 - * - * @param {number} x 爆炸中心 x - * @param {number} y 爆炸中心 y - * @param {number} count 粒子数量 - * @returns {Particle[]} */ function _burst(x, y, count) { const color = COLORS[Math.floor(Math.random() * COLORS.length)]; @@ -88,12 +86,12 @@ const FireworksEffect = (() => { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; - const DURATION = 4500; // 总时长(ms) + const DURATION = 5000; // 总时长(ms) let particles = []; let animId = null; let launchCount = 0; - const MAX_LAUNCHES = 8; // 总共发射几枚烟花 + const MAX_LAUNCHES = 10; // 总发射枚数(增加) // 定时发射烟花 const launchInterval = setInterval(() => { @@ -101,21 +99,21 @@ const FireworksEffect = (() => { clearInterval(launchInterval); return; } - const x = w * (0.15 + Math.random() * 0.7); // 避免贴近边缘 - const y = h * (0.1 + Math.random() * 0.5); // 在屏幕上半区爆炸 - const count = Math.floor(Math.random() * 40) + 60; - particles = particles.concat(_burst(x, y, count)); + 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++; - }, 500); + }, 450); const startTime = performance.now(); // 动画循环 function animate(now) { - // 清除画布(保持透明,不遮挡聊天背景) + // 清除画布(透明,不遮挡聊天背景) ctx.clearRect(0, 0, w, h); - // 更新并绘制存活粒子(粒子自带 alpha 衰减,视觉上有淡出效果) + // 更新并绘制存活粒子 particles = particles.filter((p) => p.alpha > 0.02); particles.forEach((p) => { p.update(); @@ -125,7 +123,6 @@ const FireworksEffect = (() => { if (now - startTime < DURATION) { animId = requestAnimationFrame(animate); } else { - // 特效结束:清空 canvas 后回调 clearInterval(launchInterval); cancelAnimationFrame(animId); ctx.clearRect(0, 0, w, h); diff --git a/public/js/effects/rain.js b/public/js/effects/rain.js index 4d9c472..52b39f0 100644 --- a/public/js/effects/rain.js +++ b/public/js/effects/rain.js @@ -2,6 +2,7 @@ * 文件功能:聊天室下雨特效 * * 使用 Canvas 绘制斜向雨线,模拟真实降雨视觉效果。 + * 加粗加深雨线颜色,在浅色背景上清晰可见。 * 特效总时长约 8 秒,结束后自动清理并回调。 */ @@ -14,17 +15,15 @@ const RainEffect = (() => { /** * 重置/初始化雨滴位置 - * - * @param {number} w Canvas 宽度 - * @param {number} h Canvas 高度 */ reset(w, h) { this.x = Math.random() * w; - this.y = Math.random() * -h; // 从屏幕上方随机位置开始 - this.len = Math.random() * 20 + 10; // 雨线长度 - this.speed = Math.random() * 8 + 6; // 下落速度 - this.angle = (Math.PI / 180) * (75 + Math.random() * 10); // 倾斜角(接近竖直偏右) - this.alpha = Math.random() * 0.3 + 0.2; // 透明度 + this.y = Math.random() * -h; + this.len = Math.random() * 25 + 12; // 雨线长度(稍加长) + this.speed = Math.random() * 10 + 7; // 下落速度(加快) + this.angle = (Math.PI / 180) * (75 + Math.random() * 10); + this.alpha = Math.random() * 0.5 + 0.4; // 提高透明度上限 (0.4-0.9,原 0.2-0.5) + this.strokeW = Math.random() * 1.5 + 0.8; // 线条宽度随机(原 0.8 固定) this.w = w; this.h = h; } @@ -33,17 +32,18 @@ const RainEffect = (() => { update() { this.x += Math.cos(this.angle) * this.speed * 0.3; this.y += Math.sin(this.angle) * this.speed; - // 落出屏幕后重置 if (this.y > this.h + this.len) { this.reset(this.w, this.h); } } - /** 绘制雨滴线段 */ + /** 绘制雨滴线段(加深蓝色,在浅色背景上更明显) */ draw(ctx) { ctx.save(); - ctx.strokeStyle = `rgba(155, 200, 255, ${this.alpha})`; - ctx.lineWidth = 0.8; + ctx.strokeStyle = `rgba(50, 130, 220, ${this.alpha})`; // 加深蓝色(原浅蓝 155,200,255) + ctx.lineWidth = this.strokeW; + ctx.shadowColor = "rgba(30, 100, 200, 0.4)"; + ctx.shadowBlur = 2; ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo( @@ -65,13 +65,12 @@ const RainEffect = (() => { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; - const DURATION = 8000; // 总时长(ms) - const DROP_COUNT = 180; // 雨滴数量 + const DURATION = 8000; + const DROP_COUNT = 200; // 增加雨滴数量(原 180) - // 初始化所有雨滴,随机分布在屏幕各处(避免开始时从顶部一起落) const drops = Array.from({ length: DROP_COUNT }, () => { const d = new Drop(w, h); - d.y = Math.random() * h; // 初始 Y 随机,不全部从顶部开始 + d.y = Math.random() * h; return d; }); @@ -79,7 +78,7 @@ const RainEffect = (() => { const startTime = performance.now(); function animate(now) { - // 清除画布(保持透明,不遮挡聊天背景) + // 清除画布(透明,不遮挡聊天背景) ctx.clearRect(0, 0, w, h); drops.forEach((d) => { diff --git a/public/js/effects/snow.js b/public/js/effects/snow.js new file mode 100644 index 0000000..a279763 --- /dev/null +++ b/public/js/effects/snow.js @@ -0,0 +1,107 @@ +/** + * 文件功能:聊天室下雪特效 + * + * 使用 Canvas 绘制随机飘落的雪花圆点,模拟冬日飘雪效果。 + * 雪花大小、速度、飘动幅度随机,在浅色背景上以白色+深描边显示。 + * 特效总时长约 10 秒,结束后自动清理并回调。 + */ + +const SnowEffect = (() => { + // 雪花类 + class Flake { + constructor(w, h) { + this.w = w; + this.h = h; + this.reset(true); + } + + /** + * 重置雪花位置 + * + * @param {boolean} initial 是否初始化(初始化时 Y 随机分布全屏,否则从顶部重生) + */ + reset(initial = false) { + this.x = Math.random() * this.w; + this.y = initial ? Math.random() * this.h : -10; + this.r = Math.random() * 4 + 2; // 半径 2-6 + this.speed = Math.random() * 1.5 + 0.5; // 下落速度 + this.drift = Math.random() * 0.8 - 0.4; // 水平漂移 + this.alpha = Math.random() * 0.4 + 0.6; // 透明度 0.6-1.0 + this.angle = 0; + this.wobble = Math.random() * 0.04 + 0.01; // 左右摇摆频率 + } + + /** 每帧更新雪花位置 */ + update() { + this.angle += this.wobble; + this.x += Math.sin(this.angle) * this.drift + this.drift * 0.3; + this.y += this.speed; + if (this.y > this.h + 10) { + this.reset(false); + } + } + + /** 绘制雪花(白色圆点 + 深色描边,在浅色背景上可见) */ + draw(ctx) { + ctx.save(); + ctx.globalAlpha = this.alpha; + // 外圈:半透明蓝灰描边 + ctx.beginPath(); + ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); + ctx.strokeStyle = "rgba(80, 120, 180, 0.6)"; + ctx.lineWidth = 0.8; + ctx.stroke(); + // 内部:白色填充 + ctx.fillStyle = "#ffffff"; + ctx.shadowColor = "rgba(150, 180, 255, 0.8)"; + ctx.shadowBlur = 4; + ctx.fill(); + ctx.restore(); + } + } + + /** + * 启动下雪特效 + * + * @param {HTMLCanvasElement} canvas 全屏 Canvas + * @param {Function} onEnd 特效结束回调 + */ + function start(canvas, onEnd) { + const ctx = canvas.getContext("2d"); + const w = canvas.width; + const h = canvas.height; + const DURATION = 10000; // 总时长(ms) + const FLAKE_COUNT = 160; // 雪花数量 + + // 初始化雪花,随机分布全屏(避免开始时全堆在顶部) + const flakes = Array.from( + { length: FLAKE_COUNT }, + () => new Flake(w, h), + ); + + let animId = null; + const startTime = performance.now(); + + function animate(now) { + // 清除画布(透明,不遮挡聊天背景) + ctx.clearRect(0, 0, w, h); + + flakes.forEach((f) => { + f.update(); + f.draw(ctx); + }); + + if (now - startTime < DURATION) { + animId = requestAnimationFrame(animate); + } else { + cancelAnimationFrame(animId); + ctx.clearRect(0, 0, w, h); + onEnd(); + } + } + + animId = requestAnimationFrame(animate); + } + + return { start }; +})(); diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index bfc2e4f..d576ac1 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -94,11 +94,12 @@ {{-- ═══════════ 聊天室交互脚本(独立文件维护) ═══════════ --}} @include('chat.partials.user-actions') - {{-- 全屏特效系统:管理员烟花/下雨/雷电 --}} + {{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}} + @include('chat.partials.scripts') diff --git a/resources/views/chat/partials/input-bar.blade.php b/resources/views/chat/partials/input-bar.blade.php index 18405ff..3f43a09 100644 --- a/resources/views/chat/partials/input-bar.blade.php +++ b/resources/views/chat/partials/input-bar.blade.php @@ -87,6 +87,9 @@ + @endif