D122: xhs widget system - HTML/CSS render engine + 5 color schemes
This commit is contained in:
parent
dd73c2174c
commit
0d75824d15
151
image-generator/xiaohongshu-widgets/render-engine.js
Normal file
151
image-generator/xiaohongshu-widgets/render-engine.js
Normal file
@ -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 `<!-- widget render error: ${widgetId} -->`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderStack(stack, options = {}) {
|
||||||
|
const scheme = options.scheme || 'warm'
|
||||||
|
const title = options.title || ''
|
||||||
|
const css = buildCSS(scheme, options.customVars || {})
|
||||||
|
|
||||||
|
let body = '<div class="card">'
|
||||||
|
|
||||||
|
// 装饰区域(顶部)
|
||||||
|
const decorWidgets = stack.filter(s => {
|
||||||
|
const w = WIDGETS[s.component]
|
||||||
|
return w && w.category === 'decor'
|
||||||
|
})
|
||||||
|
if (decorWidgets.length > 0) {
|
||||||
|
body += '<div class="decor-section">'
|
||||||
|
body += decorWidgets.map(s => renderWidget(s.component, s.variant, s.params)).join('')
|
||||||
|
body += '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
if (title) {
|
||||||
|
body += `<div class="widget-title"><h1>${title}</h1></div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要内容widget
|
||||||
|
const mainWidgets = stack.filter(s => {
|
||||||
|
const w = WIDGETS[s.component]
|
||||||
|
return w && w.category !== 'decor'
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const s of mainWidgets) {
|
||||||
|
body += `<div class="widget-row start">${renderWidget(s.component, s.variant, s.params)}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
body += '</div>'
|
||||||
|
|
||||||
|
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><style>${css}</style></head><body>${body}</body></html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完全随机风格生成
|
||||||
|
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 }
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user