/** * 文件功能:聊天室歼-35 战机入场特效 * * 使用全屏透明 Canvas 绘制隐身战机高速掠过、喷口尾焰、音爆环、 * 流光航迹与战术 HUD 字幕,作为座驾/载具入场的战机版本原型。 */ const J35Effect = (() => { const DURATION = 8200; const STEEL = "#9ca3af"; const DARK_STEEL = "#1f2937"; const COCKPIT = "#0f172a"; const AFTERBURNER = "#38bdf8"; const WARNING_RED = "#ef4444"; /** * 缓出曲线,用于战机高速进场后略微减速展示。 * * @param {number} t 0 到 1 的进度 * @returns {number} */ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } /** * 缓入缓出曲线,用于 HUD 与音爆环淡入淡出。 * * @param {number} t 0 到 1 的进度 * @returns {number} */ function easeInOutSine(t) { return -(Math.cos(Math.PI * t) - 1) / 2; } /** * 创建高速流光粒子。 * * @param {number} w 画布宽度 * @param {number} h 画布高度 * @returns {Array>} */ function createSpeedLines(w, h) { return Array.from({ length: 110 }, () => ({ x: Math.random() * w, y: h * (0.18 + Math.random() * 0.68), speed: 2 + Math.random() * 5, length: 90 + Math.random() * 190, alpha: 0.18 + Math.random() * 0.42, color: [AFTERBURNER, "#ffffff", "#fde68a", "#60a5fa"][Math.floor(Math.random() * 4)], })); } /** * 绘制夜航背景与扫描网格。 * * @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.16) * Math.min(1, (1 - progress) / 0.12); const gradient = ctx.createRadialGradient(w * 0.5, h * 0.48, 0, w * 0.5, h * 0.5, Math.max(w, h) * 0.82); gradient.addColorStop(0, `rgba(15,23,42,${0.52 * fade})`); gradient.addColorStop(0.55, `rgba(8,47,73,${0.26 * fade})`); gradient.addColorStop(1, "rgba(0,0,0,0)"); ctx.save(); ctx.fillStyle = gradient; ctx.fillRect(0, 0, w, h); ctx.globalCompositeOperation = "lighter"; for (let i = 0; i < 10; i++) { const y = h * (0.18 + i * 0.07); ctx.strokeStyle = `rgba(56,189,248,${0.08 * fade})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, y + Math.sin(progress * 18 + i) * 6); ctx.lineTo(w, y + Math.cos(progress * 15 + i) * 6); ctx.stroke(); } ctx.restore(); } /** * 绘制高速运动线,方向与战机飞行一致。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {Array>} lines 流光线集合 * @param {number} w 画布宽度 * @param {number} progress 播放进度 */ function drawSpeedLines(ctx, lines, w, progress) { const fade = Math.min(1, progress / 0.12) * Math.min(1, (1 - progress) / 0.1); ctx.save(); ctx.globalCompositeOperation = "lighter"; lines.forEach((line, index) => { const travel = (progress * (900 + line.speed * 120) + index * 71) % (w + 520); const x = w + 260 - travel; ctx.globalAlpha = line.alpha * fade; ctx.strokeStyle = line.color; ctx.lineWidth = 1.4 + line.speed * 0.18; ctx.shadowColor = line.color; ctx.shadowBlur = 12; ctx.beginPath(); ctx.moveTo(x, line.y); ctx.lineTo(x + line.length, line.y + Math.sin(progress * 22 + index) * 4); ctx.stroke(); }); ctx.restore(); } /** * 绘制战机尾部双发喷口尾焰。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} progress 播放进度 */ function drawAfterburners(ctx, progress) { const pulse = 0.78 + Math.sin(progress * 70) * 0.16; ctx.save(); ctx.globalCompositeOperation = "lighter"; [[168, -12], [168, 12]].forEach(([x, y]) => { const flame = ctx.createLinearGradient(x, y, x + 170, y); flame.addColorStop(0, `rgba(255,255,255,${0.9 * pulse})`); flame.addColorStop(0.18, `rgba(56,189,248,${0.78 * pulse})`); flame.addColorStop(0.58, `rgba(59,130,246,${0.3 * pulse})`); flame.addColorStop(1, "rgba(59,130,246,0)"); ctx.fillStyle = flame; ctx.shadowColor = AFTERBURNER; ctx.shadowBlur = 26; ctx.beginPath(); ctx.moveTo(x, y - 9); ctx.bezierCurveTo(x + 58, y - 18, x + 115, y - 18, x + 178, y); ctx.bezierCurveTo(x + 115, y + 18, x + 58, y + 18, x, y + 9); ctx.closePath(); ctx.fill(); }); ctx.restore(); } /** * 绘制歼-35 风格隐身战机主体。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} x 战机中心 x * @param {number} y 战机中心 y * @param {number} scale 缩放比例 * @param {number} progress 播放进度 */ function drawJet(ctx, x, y, scale, progress) { ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale); ctx.rotate(Math.sin(progress * 10) * 0.018); drawAfterburners(ctx, progress); const bodyGradient = ctx.createLinearGradient(-210, -62, 190, 62); bodyGradient.addColorStop(0, "#d1d5db"); bodyGradient.addColorStop(0.34, "#6b7280"); bodyGradient.addColorStop(0.64, "#374151"); bodyGradient.addColorStop(1, "#111827"); ctx.save(); ctx.shadowColor = "rgba(148,163,184,0.9)"; ctx.shadowBlur = 18; // 主翼与机体采用多边形硬折线,体现隐身战机边缘。 ctx.fillStyle = bodyGradient; ctx.beginPath(); ctx.moveTo(-230, 0); ctx.lineTo(-96, -44); ctx.lineTo(12, -120); ctx.lineTo(58, -42); ctx.lineTo(170, -70); ctx.lineTo(142, -18); ctx.lineTo(202, 0); ctx.lineTo(142, 18); ctx.lineTo(170, 70); ctx.lineTo(58, 42); ctx.lineTo(12, 120); ctx.lineTo(-96, 44); ctx.closePath(); ctx.fill(); const spineGradient = ctx.createLinearGradient(-200, -16, 178, 16); spineGradient.addColorStop(0, "#e5e7eb"); spineGradient.addColorStop(0.45, "#6b7280"); spineGradient.addColorStop(1, "#1f2937"); ctx.fillStyle = spineGradient; ctx.beginPath(); ctx.moveTo(-222, 0); ctx.lineTo(-80, -24); ctx.lineTo(92, -18); ctx.lineTo(184, 0); ctx.lineTo(92, 18); ctx.lineTo(-80, 24); ctx.closePath(); ctx.fill(); // 机头下方深色进气道。 ctx.fillStyle = "rgba(3,7,18,0.72)"; ctx.beginPath(); ctx.moveTo(-92, -33); ctx.lineTo(-38, -74); ctx.lineTo(-8, -55); ctx.lineTo(-60, -24); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(-92, 33); ctx.lineTo(-38, 74); ctx.lineTo(-8, 55); ctx.lineTo(-60, 24); ctx.closePath(); ctx.fill(); // 座舱盖。 const canopy = ctx.createLinearGradient(-148, -18, -58, 18); canopy.addColorStop(0, "#020617"); canopy.addColorStop(0.55, "#1e3a8a"); canopy.addColorStop(1, "#111827"); ctx.fillStyle = canopy; ctx.beginPath(); ctx.moveTo(-160, 0); ctx.bezierCurveTo(-130, -28, -78, -28, -48, 0); ctx.bezierCurveTo(-78, 26, -130, 26, -160, 0); ctx.fill(); // 双垂尾。 ctx.fillStyle = DARK_STEEL; ctx.beginPath(); ctx.moveTo(78, -44); ctx.lineTo(154, -120); ctx.lineTo(132, -38); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(78, 44); ctx.lineTo(154, 120); ctx.lineTo(132, 38); ctx.closePath(); ctx.fill(); // 中国战机识别元素:低调红星,不做过大以免影响隐身外形。 drawRedStar(ctx, 18, -47, 15); drawRedStar(ctx, 18, 47, 15); // 面板高光线。 ctx.strokeStyle = "rgba(229,231,235,0.34)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-205, 0); ctx.lineTo(168, 0); ctx.moveTo(-70, -22); ctx.lineTo(86, -16); ctx.moveTo(-70, 22); ctx.lineTo(86, 16); ctx.stroke(); ctx.restore(); ctx.restore(); } /** * 绘制低调红星机徽。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} x 中心 x * @param {number} y 中心 y * @param {number} radius 半径 */ function drawRedStar(ctx, x, y, radius) { ctx.save(); ctx.translate(x, y); ctx.fillStyle = "rgba(239,68,68,0.9)"; ctx.strokeStyle = "rgba(254,202,202,0.68)"; ctx.lineWidth = 1.5; ctx.beginPath(); for (let i = 0; i < 10; i++) { const angle = -Math.PI / 2 + (i * Math.PI) / 5; const r = i % 2 === 0 ? radius : radius * 0.42; const px = Math.cos(angle) * r; const py = Math.sin(angle) * r; if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } } ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.restore(); } /** * 绘制高速掠过时的音爆环。 * * @param {CanvasRenderingContext2D} ctx Canvas 上下文 * @param {number} w 画布宽度 * @param {number} h 画布高度 * @param {number} progress 播放进度 */ function drawSonicRing(ctx, w, h, progress) { const ringProgress = Math.max(0, Math.min(1, (progress - 0.38) / 0.22)); if (ringProgress <= 0 || ringProgress >= 1) { return; } const alpha = Math.sin(ringProgress * Math.PI); ctx.save(); ctx.globalCompositeOperation = "lighter"; ctx.strokeStyle = `rgba(224,242,254,${0.42 * alpha})`; ctx.lineWidth = 5; ctx.beginPath(); ctx.ellipse(w * 0.5, h * 0.54, w * (0.08 + ringProgress * 0.32), h * (0.03 + ringProgress * 0.11), 0, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = `rgba(56,189,248,${0.2 * alpha})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(w * 0.5, h * 0.54, w * (0.12 + ringProgress * 0.44), h * (0.05 + ringProgress * 0.16), 0, 0, Math.PI * 2); 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.13) / 0.18)); const leave = Math.min(1, Math.max(0, (1 - progress) / 0.16)); const alpha = easeInOutSine(enter) * leave; const y = h * 0.18 - (1 - enter) * 26; ctx.save(); ctx.globalAlpha = alpha; ctx.textAlign = "center"; ctx.shadowColor = "rgba(56,189,248,0.95)"; ctx.shadowBlur = 22; ctx.fillStyle = "rgba(2,6,23,0.62)"; ctx.strokeStyle = "rgba(56,189,248,0.72)"; ctx.lineWidth = 2; roundRect(ctx, w * 0.5 - 340, y - 56, 680, 120, 18); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#bae6fd"; ctx.font = "700 16px serif"; ctx.fillText("STEALTH FIGHTER ARRIVAL", w * 0.5, y - 24); ctx.fillStyle = "#e0f2fe"; ctx.font = "700 18px serif"; ctx.fillText(userInfo, w * 0.5, y + 8, 620); ctx.fillStyle = "#ffffff"; ctx.font = "900 34px serif"; ctx.fillText(title, w * 0.5, y + 45, 620); 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(); } /** * 启动歼-35 战机入场特效。 * * @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 speedLines = createSpeedLines(w, h); const title = String(options.effect_title || "中国歼-35 破空入场").trim() || "中国歼-35 破空入场"; 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 elapsed = now - startTime; const progress = Math.min(1, elapsed / DURATION); const entry = easeOutCubic(Math.min(1, progress / 0.62)); const exit = easeInOutSine(Math.max(0, (progress - 0.76) / 0.24)); const jetX = w * 1.2 - entry * w * 0.82 - exit * w * 0.58; const jetY = h * 0.58 + Math.sin(progress * 18) * 10; const scale = Math.min(1.14, Math.max(0.68, w / 1180)); ctx.clearRect(0, 0, w, h); drawBackdrop(ctx, w, h, progress); drawSpeedLines(ctx, speedLines, w, progress); drawSonicRing(ctx, w, h, progress); drawJet(ctx, jetX, jetY, 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.J35Effect = J35Effect;