Files
gitlab-instance-0a899031_di…/app/course/videoPoster.ts
2026-03-08 01:10:31 -06:00

185 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生成视频封面图(数字游民插画风格,无序号)
* 返回 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);
}