001project_wildgrowth/backend/scripts/xhs-cover-playground.ts

180 lines
4.9 KiB
TypeScript
Raw Permalink 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.

/**
* 小红书封面 Playground
* 用 node-canvas 画一张「截图风格」的封面,验证能否做出来。
*
* 运行:在 backend 目录下执行
* npm run playground:xhs-cover
* 或
* npx ts-node scripts/xhs-cover-playground.ts
*
* 输出backend/playground-xhs-cover.png用系统看图打开即可
*/
import { createCanvas, CanvasRenderingContext2D } from 'canvas';
import fs from 'fs/promises';
import path from 'path';
// 小红书常用比例 3:4先用小尺寸快速验证可改为 1080×1440 出高清)
const WIDTH = 540;
const HEIGHT = 720;
function wrapText(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
fontSize: number,
maxLines = 6
): string[] {
const lines: string[] = [];
let currentLine = '';
for (const char of text.split('')) {
const testLine = currentLine + char;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine.length > 0) {
lines.push(currentLine);
currentLine = char;
if (lines.length >= maxLines) break;
} else {
currentLine = testLine;
}
}
if (currentLine.length > 0) lines.push(currentLine);
return lines;
}
async function main() {
const canvas = createCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext('2d');
// 1. 浅黄背景
ctx.fillStyle = '#FFFDE7';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// 2. 网格(细线)
const gridStep = 24;
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
ctx.lineWidth = 0.5;
for (let x = 0; x <= WIDTH; x += gridStep) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, HEIGHT);
ctx.stroke();
}
for (let y = 0; y <= HEIGHT; y += gridStep) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(WIDTH, y);
ctx.stroke();
}
// 3. 白色圆角卡片(居中,留边距)
const padding = 48;
const cardX = padding;
const cardY = 120;
const cardW = WIDTH - padding * 2;
const cardH = 380;
const radius = 20;
// 圆角矩形(兼容无 roundRect 的环境)
function drawRoundRect(x: number, y: number, w: number, h: number, r: number) {
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();
}
ctx.fillStyle = '#FFFFFF';
drawRoundRect(cardX, cardY, cardW, cardH, radius);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.04)';
ctx.lineWidth = 0.5;
drawRoundRect(cardX, cardY, cardW, cardH, radius);
ctx.stroke();
// 4. 卡片顶部黄色小条(像截图里的 accent tab
const tabW = 60;
const tabH = 12;
const tabX = cardX + (cardW - tabW) / 2;
const tabY = cardY - tabH / 2;
ctx.fillStyle = '#FFD93D';
drawRoundRect(tabX, tabY, tabW, tabH, 4);
ctx.fill();
// 5. 卡片内多行文字(加粗、黑色)
const textX = cardX + 32;
const textY = cardY + 44;
const lineHeight = 38;
const maxTextWidth = cardW - 64;
// 尽量用系统里有的中文字体Mac 有 PingFang SC
ctx.font = 'bold 26px "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif';
ctx.fillStyle = '#000000';
ctx.textAlign = 'left';
const lines = [
'谁懂!',
'人生最好的事情',
'不过是:',
'拥有好朋友和',
'幸福生活!',
];
let y = textY;
for (const line of lines) {
const wrapped = wrapText(ctx, line, maxTextWidth, 26, 1);
for (const seg of wrapped) {
ctx.fillText(seg, textX, y);
y += lineHeight;
}
}
// 6. 卡片左下角装饰虚线(几条短横线)
const dashY = cardY + cardH - 40;
ctx.strokeStyle = '#FFD93D';
ctx.lineWidth = 2;
ctx.setLineDash([6, 6]);
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(textX + i * 36, dashY);
ctx.lineTo(textX + i * 36 + 24, dashY);
ctx.stroke();
}
ctx.setLineDash([]);
// 7. 右下角「贴纸」占位(画一个圆 + 文字,模拟 emoji 位;真 emoji 可后续用图片贴)
const stickerX = cardX + cardW - 72;
const stickerY = cardY + cardH - 72;
ctx.fillStyle = '#FFF3CD';
ctx.beginPath();
ctx.arc(stickerX, stickerY, 32, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = '20px "PingFang SC", sans-serif';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText('贴纸', stickerX, stickerY + 6);
// 8. 导出 PNG
const outPath = path.join(process.cwd(), 'playground-xhs-cover.png');
const buffer = canvas.toBuffer('image/png');
await fs.writeFile(outPath, buffer);
console.log('✅ 已生成:', outPath);
console.log(' 用系统看图/预览打开即可查看效果。');
}
main().catch((e) => {
console.error('生成失败:', e);
process.exit(1);
});