Files
chatroom/resources/js/effects/j35.js
T
2026-04-30 09:40:50 +08:00

468 lines
15 KiB
JavaScript

/**
* 文件功能:聊天室歼-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<Record<string, number|string>>}
*/
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<Record<string, number|string>>} 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 播放进度
*/
function drawHud(ctx, w, h, progress) {
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 - 230, y - 44, 460, 92, 18);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#bae6fd";
ctx.font = "700 16px serif";
ctx.fillText("STEALTH FIGHTER ARRIVAL", w * 0.5, y - 12);
ctx.fillStyle = "#ffffff";
ctx.font = "900 42px serif";
ctx.fillText("中国歼-35 破空入场", w * 0.5, y + 28);
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 结束回调
* @returns {{cancel: Function}}
*/
function start(canvas, onEnd) {
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const speedLines = createSpeedLines(w, h);
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);
if (progress < 1) {
animId = requestAnimationFrame(animate);
} else {
finish(false);
}
}
animId = requestAnimationFrame(animate);
return {
cancel() {
finish(true);
},
};
}
return { start };
})();
window.J35Effect = J35Effect;