185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
/**
|
||
* 生成视频封面图(数字游民插画风格,无序号)
|
||
* 返回 data URL,可直接用作 video poster
|
||
*/
|
||
export function generateVideoPoster(
|
||
title: string,
|
||
_lessonId: string,
|
||
options?: {
|
||
width?: number;
|
||
height?: number;
|
||
bgGradient?: [string, string];
|
||
accentColor?: string;
|
||
}
|
||
): string {
|
||
const width = options?.width ?? 640;
|
||
const height = options?.height ?? 360;
|
||
const [from, to] = options?.bgGradient ?? ["#0ea5e9", "#06b6d4"];
|
||
const accent = options?.accentColor ?? "#ffffff";
|
||
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
const ctx = canvas.getContext("2d");
|
||
if (!ctx) return "";
|
||
|
||
// 数字游民插画:天空渐变背景
|
||
const skyGrad = ctx.createLinearGradient(0, 0, 0, height);
|
||
skyGrad.addColorStop(0, "#87CEEB");
|
||
skyGrad.addColorStop(0.5, "#B8E0F0");
|
||
skyGrad.addColorStop(0.75, "#E8F4F8");
|
||
skyGrad.addColorStop(1, "#F5E6D3");
|
||
ctx.fillStyle = skyGrad;
|
||
ctx.fillRect(0, 0, width, height);
|
||
|
||
// 海洋
|
||
ctx.fillStyle = "rgba(14, 165, 233, 0.25)";
|
||
ctx.fillRect(0, height * 0.65, width, height * 0.35);
|
||
ctx.strokeStyle = "rgba(255,255,255,0.4)";
|
||
ctx.lineWidth = 2;
|
||
for (let i = 0; i < 6; i++) {
|
||
ctx.beginPath();
|
||
const x = (i * width) / 5;
|
||
ctx.moveTo(x, height * 0.72);
|
||
ctx.quadraticCurveTo(x + 50, height * 0.68, x + 100, height * 0.72);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// 太阳
|
||
const sunGrad = ctx.createRadialGradient(width * 0.85, height * 0.15, 0, width * 0.85, height * 0.15, 50);
|
||
sunGrad.addColorStop(0, "#FEF3C7");
|
||
sunGrad.addColorStop(0.6, "#FCD34D");
|
||
sunGrad.addColorStop(1, "rgba(252, 211, 77, 0)");
|
||
ctx.fillStyle = sunGrad;
|
||
ctx.beginPath();
|
||
ctx.arc(width * 0.85, height * 0.15, 50, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = "#FCD34D";
|
||
ctx.beginPath();
|
||
ctx.arc(width * 0.85, height * 0.15, 18, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// 椰子树(简化插画)
|
||
const palmX = width * 0.12;
|
||
const palmY = height * 0.55;
|
||
ctx.fillStyle = "#8B7355";
|
||
ctx.fillRect(palmX - 4, palmY, 8, height * 0.35);
|
||
ctx.fillStyle = "#22C55E";
|
||
for (let i = 0; i < 5; i++) {
|
||
const a = (i / 5) * Math.PI * 0.8 - Math.PI * 0.2;
|
||
ctx.beginPath();
|
||
ctx.moveTo(palmX, palmY);
|
||
ctx.quadraticCurveTo(palmX + Math.cos(a) * 35, palmY - 25, palmX + Math.cos(a) * 55, palmY - 45);
|
||
ctx.strokeStyle = "#22C55E";
|
||
ctx.lineWidth = 6;
|
||
ctx.lineCap = "round";
|
||
ctx.stroke();
|
||
}
|
||
|
||
// 地球/ globe
|
||
const globeX = width * 0.78;
|
||
const globeY = height * 0.35;
|
||
ctx.strokeStyle = "rgba(255,255,255,0.6)";
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
ctx.arc(globeX, globeY, 45, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.ellipse(globeX, globeY, 45, 15, 0.3, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.ellipse(globeX, globeY, 15, 45, -0.2, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.fillStyle = "rgba(14, 165, 233, 0.15)";
|
||
ctx.fill();
|
||
|
||
// 笔记本电脑(数字游民工作象征)
|
||
const lapX = width * 0.25;
|
||
const lapY = height * 0.5;
|
||
ctx.fillStyle = "#E5E7EB";
|
||
ctx.fillRect(lapX - 35, lapY - 20, 70, 45);
|
||
ctx.fillStyle = "#94A3B8";
|
||
ctx.fillRect(lapX - 32, lapY - 18, 64, 35);
|
||
ctx.fillStyle = "#0ea5e9";
|
||
ctx.globalAlpha = 0.6;
|
||
ctx.fillRect(lapX - 28, lapY - 14, 56, 27);
|
||
ctx.globalAlpha = 1;
|
||
ctx.fillStyle = "#64748B";
|
||
ctx.fillRect(lapX - 38, lapY + 22, 76, 6);
|
||
ctx.fillRect(lapX - 2, lapY + 18, 4, 8);
|
||
|
||
// 云朵
|
||
ctx.fillStyle = "rgba(255,255,255,0.7)";
|
||
ctx.beginPath();
|
||
ctx.arc(width * 0.5, height * 0.2, 25, 0, Math.PI * 2);
|
||
ctx.arc(width * 0.55, height * 0.18, 20, 0, Math.PI * 2);
|
||
ctx.arc(width * 0.6, height * 0.22, 22, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// 底部半透明遮罩,让标题更清晰
|
||
const overlay = ctx.createLinearGradient(0, height * 0.5, 0, height);
|
||
overlay.addColorStop(0, "rgba(0,0,0,0)");
|
||
overlay.addColorStop(0.5, "rgba(0,0,0,0.1)");
|
||
overlay.addColorStop(1, "rgba(0,0,0,0.4)");
|
||
ctx.fillStyle = overlay;
|
||
ctx.fillRect(0, 0, width, height);
|
||
|
||
// 标题:多行自动换行
|
||
ctx.fillStyle = accent;
|
||
ctx.font = `600 ${Math.min(width * 0.045, 28)}px system-ui, "PingFang SC", sans-serif`;
|
||
ctx.textAlign = "left";
|
||
ctx.textBaseline = "top";
|
||
|
||
const maxWidth = width * 0.9;
|
||
const lineHeight = Math.min(width * 0.05, 32);
|
||
const paddingX = width * 0.06;
|
||
const paddingY = height * 0.68;
|
||
|
||
const words = title.split("");
|
||
let line = "";
|
||
const lines: string[] = [];
|
||
|
||
for (const char of words) {
|
||
const testLine = line + char;
|
||
const metrics = ctx.measureText(testLine);
|
||
if (metrics.width > maxWidth && line) {
|
||
lines.push(line);
|
||
line = char;
|
||
} else {
|
||
line = testLine;
|
||
}
|
||
}
|
||
if (line) lines.push(line);
|
||
|
||
ctx.shadowColor = "rgba(0,0,0,0.35)";
|
||
ctx.shadowBlur = 6;
|
||
ctx.shadowOffsetY = 1;
|
||
|
||
const maxLines = 2;
|
||
const startY = paddingY;
|
||
lines.slice(0, maxLines).forEach((ln, i) => {
|
||
ctx.fillText(ln, paddingX, startY + i * lineHeight);
|
||
});
|
||
|
||
ctx.shadowColor = "transparent";
|
||
ctx.shadowBlur = 0;
|
||
|
||
// 播放图标(右下角)
|
||
const playSize = Math.min(width, height) * 0.1;
|
||
const playX = width - width * 0.12;
|
||
const playY = height - height * 0.15;
|
||
ctx.fillStyle = "rgba(255,255,255,0.9)";
|
||
ctx.beginPath();
|
||
ctx.arc(playX, playY, playSize, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = from;
|
||
ctx.beginPath();
|
||
ctx.moveTo(playX - playSize * 0.3, playY - playSize * 0.5);
|
||
ctx.lineTo(playX - playSize * 0.3, playY + playSize * 0.5);
|
||
ctx.lineTo(playX + playSize * 0.5, playY);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
return canvas.toDataURL("image/jpeg", 0.92);
|
||
}
|