diff --git a/image-generator/xiaohongshu-widgets/render-engine.js b/image-generator/xiaohongshu-widgets/render-engine.js new file mode 100644 index 0000000..6c6e303 --- /dev/null +++ b/image-generator/xiaohongshu-widgets/render-engine.js @@ -0,0 +1,151 @@ +// 小红书小组件渲染引擎 v1.0 +// 将intent-mapper产出的widget栈渲染为完整HTML +// 国作登字-2026-A-00037559 + +import { WIDGETS, randomVariant } from './widgets.js' + +const FONT = "'Noto Sans CJK SC','WenQuanYi Micro Hei','PingFang SC',sans-serif" +const TITLE_FONT = "'Noto Serif CJK SC','Source Han Serif SC',serif" + +const PRESET_SCHEMES = { + warm: { bg: '#FEF7F0', cardBg: '#FFFFFF', primary: '#5D4037', accent: '#E07A5F', muted: '#8D6E63', warm: '#FAE1DD', border: '#F0E6D3' }, + tech: { bg: '#0A0E27', cardBg: '#121838', primary: '#E0E6FF', accent: '#4F8CFF', muted: '#6B7D99', warm: '#1A1F3A', border: '#1E2A4A' }, + green: { bg: '#F5F9F2', cardBg: '#FFFFFF', primary: '#1B3A1B', accent: '#5A8F5A', muted: '#6B8B6B', warm: '#E8F0E0', border: '#D4E4CC' }, + minimal: { bg: '#FFFFFF', cardBg: '#FAFAFA', primary: '#111111', accent: '#333333', muted: '#888888', warm: '#F5F5F5', border: '#E0E0E0' }, + rose: { bg: '#FDF2F8', cardBg: '#FFFFFF', primary: '#4A1942', accent: '#9D4E8D', muted: '#8E6290', warm: '#FCE4EC', border: '#F0D4E0' }, +} + +const SCHEME_NAMES = { + warm: '奶油暖调', tech: '科技蓝', green: '文艺绿植', minimal: '极简黑白', rose: '玫瑰粉调', +} + +export function buildCSS(scheme, customVars = {}) { + const s = PRESET_SCHEMES[scheme] || PRESET_SCHEMES.warm + return ` +:root { + --bg: ${customVars.bg || s.bg}; + --cardBg: ${customVars.cardBg || s.cardBg}; + --primary: ${customVars.primary || s.primary}; + --accent: ${customVars.accent || s.accent}; + --muted: ${customVars.muted || s.muted}; + --warm: ${customVars.warm || s.warm}; + --border: ${customVars.border || s.border}; +} +*{margin:0;padding:0;box-sizing:border-box} +body{ + width:1080px;height:1440px;overflow:hidden; + font-family:${FONT}; + background:var(--bg); + display:flex;justify-content:center;align-items:center; +} +.card{ + width:960px;min-height:1320px;background:var(--cardBg); + border-radius:60px;box-shadow:0 20px 80px rgba(0,0,0,.06); + display:flex;flex-direction:column; + padding:50px 60px 40px; + position:relative;overflow:hidden; + gap:24px; +} +.widget-title{ + text-align:center;padding-top:10px; +} +.widget-title h1{ + font-family:${TITLE_FONT}; + font-size:62px;font-weight:800;color:var(--primary); + line-height:1.2;letter-spacing:1px; +} +.widget-title .hl{ + color:var(--accent);position:relative;display:inline-block; +} +.widget-row{ + display:flex;align-items:center;justify-content:center; +} +.widget-row.start{justify-content:flex-start} +.widget-row.between{justify-content:space-between} +.flex-1{flex:1} +.decor-section{text-align:center;padding:8px 0} +` +} + +export function renderWidget(widgetId, variantId, params = {}) { + const w = WIDGETS[widgetId] + if (!w) return '' + + const variant = variantId + ? w.variants.find(v => v.id === variantId) + : randomVariant(widgetId) + + if (!variant) return '' + + try { + return w.render({ ...variant, ...params }) + } catch (e) { + return `` + } +} + +export function renderStack(stack, options = {}) { + const scheme = options.scheme || 'warm' + const title = options.title || '' + const css = buildCSS(scheme, options.customVars || {}) + + let body = '
' + + // 装饰区域(顶部) + const decorWidgets = stack.filter(s => { + const w = WIDGETS[s.component] + return w && w.category === 'decor' + }) + if (decorWidgets.length > 0) { + body += '
' + body += decorWidgets.map(s => renderWidget(s.component, s.variant, s.params)).join('') + body += '
' + } + + // 标题 + if (title) { + body += `

${title}

` + } + + // 主要内容widget + const mainWidgets = stack.filter(s => { + const w = WIDGETS[s.component] + return w && w.category !== 'decor' + }) + + for (const s of mainWidgets) { + body += `
${renderWidget(s.component, s.variant, s.params)}
` + } + + body += '
' + + return `${body}` +} + +// 完全随机风格生成 +export function randomizeComposition(baseComposition) { + const { contentType, mood, stack } = baseComposition + + // 随机丢弃20%的widget + const filtered = stack.filter(() => Math.random() > 0.2) + if (filtered.length === 0) filtered.push(stack[0]) + + // 随机替换变体 + const randomized = filtered.map(s => ({ + ...s, + variant: WIDGETS[s.component]?.variants + ? WIDGETS[s.component].variants[Math.floor(Math.random() * WIDGETS[s.component].variants.length)].id + : s.variant, + })) + + // 随机加一个装饰 + const allDecor = ['dot-grid','gradient-stripe','sticker-badge','hand-drawn-circle','corner-fold'] + if (Math.random() > 0.4) { + const d = allDecor[Math.floor(Math.random() * allDecor.length)] + if (!randomized.find(s => s.component === d)) { + randomized.unshift({ component: d, variant: randomVariant(d)?.id, params: {} }) + } + } + + return { ...baseComposition, stack: randomized } +}