/** * 文件功能:聊天室东风-5C洲际导弹发射预览特效 * * 使用全屏透明 Canvas 绘制风格化洲际导弹升空、尾焰、烟尘冲击波、 * 雷达扫描网格和测试 HUD。该效果只用于聊天室视觉预览,不表达真实装备参数。 */ const Df5cEffect = (() => { const DURATION = 8200; const FIRE = "#fb923c"; const HOT = "#fef3c7"; const RED = "#dc2626"; const BODY = "#e5e7eb"; const BODY_DARK = "#64748b"; /** * 缓入缓出曲线,用于导弹升空和 HUD 动画。 * * @param {number} t 0 到 1 的进度 * @returns {number} */ function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } /** * 缓出曲线,用于烟尘扩散。 * * @param {number} t 0 到 1 的进度 * @returns {number} */ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } /** * 创建尾焰和烟尘粒子。 * * @param {number} count 粒子数量 * @returns {Array>} */ function createParticles(count) { return Array.from({ length: count }, () => ({ angle: Math.random() * Math.PI * 2, spread: 0.3 + Math.random() * 1.3, speed: 0.4 + Math.random() * 2.4, size: 4 + Math.random() * 18, alpha: 0.12 + Math.random() * 0.6, phase: Math.random() * Math.PI * 2, })); } /** * 绘制夜空、雷达网格和扫描线。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 */ function drawBackdrop(ctx, w, h, progress) { const fade = Math.min(1, progress / 0.14) * Math.min(1, (1 - progress) / 0.12); const sky = ctx.createLinearGradient(0, 0, 0, h); sky.addColorStop(0, `rgba(2,6,23,${0.86 * fade})`); sky.addColorStop(0.58, `rgba(15,23,42,${0.62 * fade})`); sky.addColorStop(1, `rgba(30,41,59,${0.26 * fade})`); ctx.fillStyle = sky; ctx.fillRect(0, 0, w, h); ctx.save(); ctx.globalAlpha = fade; ctx.strokeStyle = "rgba(56,189,248,0.16)"; ctx.lineWidth = 1; for (let x = -w; x < w * 2; x += 72) { ctx.beginPath(); ctx.moveTo(x + progress * 120, 0); ctx.lineTo(x - h * 0.55 + progress * 120, h); ctx.stroke(); } for (let y = h * 0.2; y < h; y += 46) { ctx.beginPath(); ctx.moveTo(0, y + Math.sin(progress * 18 + y) * 2); ctx.lineTo(w, y + Math.cos(progress * 14 + y) * 2); ctx.stroke(); } ctx.strokeStyle = "rgba(248,113,113,0.34)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(w * 0.16, h * 0.76, w * (0.18 + progress * 0.22), -Math.PI * 0.95, -Math.PI * 0.12); ctx.stroke(); const beamAngle = -Math.PI * 0.85 + progress * Math.PI * 1.15; ctx.strokeStyle = "rgba(34,211,238,0.34)"; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(w * 0.16, h * 0.76); ctx.lineTo(w * 0.16 + Math.cos(beamAngle) * w * 0.42, h * 0.76 + Math.sin(beamAngle) * w * 0.42); ctx.stroke(); ctx.restore(); } /** * 绘制发射井底座、光柱和冲击波。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 */ function drawLaunchPad(ctx, w, h, progress) { const ignition = Math.min(1, progress / 0.24); const pulse = Math.sin(progress * Math.PI * 12) * 0.5 + 0.5; const cx = w * 0.18; const cy = h * 0.78; ctx.save(); ctx.globalCompositeOperation = "lighter"; const beam = ctx.createRadialGradient(cx, cy, 0, cx, cy, h * 0.42); beam.addColorStop(0, `rgba(251,146,60,${0.54 * ignition})`); beam.addColorStop(0.3, `rgba(254,243,199,${0.18 * ignition})`); beam.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = beam; ctx.fillRect(0, 0, w, h); ctx.strokeStyle = `rgba(251,146,60,${(0.32 + pulse * 0.2) * ignition})`; ctx.lineWidth = 5; ctx.beginPath(); ctx.ellipse(cx, cy, w * (0.06 + progress * 0.28), h * (0.025 + progress * 0.08), 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); ctx.save(); ctx.fillStyle = "rgba(15,23,42,0.82)"; ctx.beginPath(); ctx.ellipse(cx, cy + 18, w * 0.12, h * 0.035, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "rgba(148,163,184,0.7)"; ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); } /** * 绘制尾焰和烟尘。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {Array>} particles 粒子数组 * @param {number} tailX 尾部 x * @param {number} tailY 尾部 y * @param {number} progress 播放进度 */ function drawExhaust(ctx, particles, tailX, tailY, progress) { ctx.save(); ctx.globalCompositeOperation = "lighter"; particles.forEach((particle, index) => { const t = (progress * 3.2 + index * 0.013) % 1; const spread = easeOutCubic(t) * 118 * particle.spread; const x = tailX - spread * 0.62 + Math.cos(particle.angle) * spread * 0.36; const y = tailY + spread * 0.86 + Math.sin(particle.angle + particle.phase) * spread * 0.24; const alpha = particle.alpha * (1 - t); const radius = particle.size * (0.7 + t * 2.4); ctx.globalAlpha = alpha; ctx.fillStyle = t < 0.34 ? HOT : t < 0.62 ? FIRE : "rgba(148,163,184,0.9)"; ctx.shadowColor = t < 0.55 ? FIRE : "rgba(148,163,184,0.8)"; ctx.shadowBlur = radius * 1.2; ctx.beginPath(); ctx.ellipse(x, y, radius * 0.9, radius * 1.35, -0.38, 0, Math.PI * 2); ctx.fill(); }); ctx.restore(); } /** * 绘制东风-5C风格化导弹。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} x 导弹中心 x * @param {number} y 导弹中心 y * @param {number} scale 缩放比例 * @param {number} progress 播放进度 */ function drawMissile(ctx, x, y, scale, progress) { ctx.save(); ctx.translate(x, y); // 导弹沿左下到右上的轨迹飞行,箭体头部必须朝右上,尾焰才会落在后方。 ctx.rotate(0.62 + Math.sin(progress * 7) * 0.012); ctx.scale(scale, scale); ctx.save(); ctx.shadowColor = "rgba(251,146,60,0.9)"; ctx.shadowBlur = 26; ctx.fillStyle = "rgba(251,146,60,0.86)"; ctx.beginPath(); ctx.moveTo(-28, 170); ctx.lineTo(0, 260); ctx.lineTo(28, 170); ctx.closePath(); ctx.fill(); ctx.fillStyle = "rgba(254,243,199,0.86)"; ctx.beginPath(); ctx.moveTo(-12, 176); ctx.lineTo(0, 236); ctx.lineTo(12, 176); ctx.closePath(); ctx.fill(); ctx.restore(); const body = ctx.createLinearGradient(-44, -178, 44, 168); body.addColorStop(0, "#f8fafc"); body.addColorStop(0.45, BODY); body.addColorStop(1, BODY_DARK); ctx.fillStyle = body; roundRect(ctx, -42, -156, 84, 326, 40); ctx.fill(); ctx.fillStyle = RED; ctx.beginPath(); ctx.moveTo(-42, -126); ctx.quadraticCurveTo(0, -214, 42, -126); ctx.lineTo(42, -92); ctx.lineTo(-42, -92); ctx.closePath(); ctx.fill(); ctx.fillStyle = "#111827"; ctx.fillRect(-42, -74, 84, 10); ctx.fillRect(-42, 74, 84, 10); ctx.fillStyle = "rgba(239,68,68,0.92)"; ctx.fillRect(-42, -22, 84, 30); ctx.fillStyle = "#111827"; ctx.font = "900 30px serif"; ctx.textAlign = "center"; ctx.fillText("DF-5C", 0, 50); ctx.fillStyle = "#fef3c7"; ctx.font = "900 20px serif"; ctx.fillText("★", 0, -1); ctx.fillStyle = "#334155"; ctx.beginPath(); ctx.moveTo(-42, 102); ctx.lineTo(-104, 166); ctx.lineTo(-42, 152); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(42, 102); ctx.lineTo(104, 166); ctx.lineTo(42, 152); ctx.closePath(); ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.42)"; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-20, -112); ctx.lineTo(-20, 130); ctx.stroke(); ctx.restore(); } /** * 绘制测试 HUD 文案。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 * @param {string} title 入场标题 * @param {string} userInfo 用户身份信息 */ function drawHud(ctx, w, h, progress, title, userInfo) { const enter = Math.min(1, Math.max(0, (progress - 0.1) / 0.18)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.14)); const alpha = easeInOutCubic(enter) * leave; const y = h * 0.16 - (1 - enter) * 20; ctx.save(); ctx.globalAlpha = alpha; ctx.textAlign = "center"; ctx.fillStyle = "rgba(15,23,42,0.68)"; ctx.strokeStyle = "rgba(248,113,113,0.72)"; ctx.lineWidth = 2; roundRect(ctx, w * 0.5 - 350, y - 56, 700, 120, 18); ctx.fill(); ctx.stroke(); ctx.shadowColor = "rgba(248,113,113,0.95)"; ctx.shadowBlur = 22; ctx.fillStyle = "#fee2e2"; ctx.font = "700 16px serif"; ctx.fillText("DF-5C STRATEGIC LAUNCH PREVIEW", w * 0.5, y - 24); ctx.fillStyle = "#fecaca"; ctx.font = "700 18px serif"; ctx.fillText(userInfo, w * 0.5, y + 8, 640); ctx.fillStyle = "#ffffff"; ctx.font = "900 34px serif"; ctx.fillText(title, w * 0.5, y + 45, 640); ctx.restore(); } /** * 绘制圆角矩形路径。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} x 左上角 x * @param {number} y 左上角 y * @param {number} w 宽度 * @param {number} h 高度 * @param {number} r 圆角半径 */ function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); } /** * 启动东风-5C洲际导弹发射预览特效。 * * @param {HTMLCanvasElement} canvas 全屏特效画布 * @param {Function} onEnd 结束回调 * @param {object} options 特效附加参数 * @returns {{cancel: Function}} */ function start(canvas, onEnd, options = {}) { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const particles = createParticles(120); const title = String(options.effect_title || "东风-5C 洲际导弹 升空").trim() || "东风-5C 洲际导弹 升空"; const userInfo = String(options.effect_user_info || "").trim(); const startTime = performance.now(); let animId = null; let finished = false; /** * 统一结束动画,手动取消时只清理不回调。 * * @param {boolean} canceled 是否为手动取消 */ function finish(canceled) { if (finished) { return; } finished = true; if (animId) { cancelAnimationFrame(animId); } ctx.clearRect(0, 0, w, h); if (!canceled) { onEnd(); } } /** * 逐帧绘制发射动画。 * * @param {number} now 当前高精度时间 */ function animate(now) { const progress = Math.min(1, (now - startTime) / DURATION); const launch = easeInOutCubic(Math.min(1, progress / 0.78)); const launchX = w * (0.18 + launch * 0.66); const launchY = h * (0.78 - launch * 0.95); const scale = Math.min(1.08, Math.max(0.7, w / 1240)); const tailX = launchX - Math.sin(0.62) * 168 * scale; const tailY = launchY + Math.cos(0.62) * 168 * scale; ctx.clearRect(0, 0, w, h); drawBackdrop(ctx, w, h, progress); drawLaunchPad(ctx, w, h, progress); drawExhaust(ctx, particles, tailX, tailY, progress); drawMissile(ctx, launchX, launchY, scale, progress); drawHud(ctx, w, h, progress, title, userInfo); if (progress < 1) { animId = requestAnimationFrame(animate); } else { finish(false); } } animId = requestAnimationFrame(animate); return { cancel() { finish(true); }, }; } return { start }; })(); window.Df5cEffect = Df5cEffect;