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

180 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-02-11 15:26:03 +08:00
/**
* 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);
});