import { readFileSync, writeFileSync } from 'node:fs'; import { resolve, extname } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = fileURLToPath(new URL('../', import.meta.url)); const markdown = readFileSync(resolve(root, 'designv2.md'), 'utf8'); const samples = JSON.parse(readFileSync(resolve(root, 'reference/color-study/samples.json'), 'utf8')); const esc = value => String(value).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const mime = {'.png':'image/png','.svg':'image/svg+xml','.json':'application/json','.py':'text/plain','.md':'text/plain'}; const assets = new Map(); function asset(path) { const absolute = resolve(root, path); if (!assets.has(absolute)) assets.set(absolute, `data:${mime[extname(path)] || 'application/octet-stream'};base64,${readFileSync(absolute).toString('base64')}`); return assets.get(absolute); } function inline(text) { const held = []; const hold = html => `\u0000${held.push(html)-1}\u0000`; text = text.replace(/`([^`]+)`/g, (_, value) => hold(`${esc(value)}`)); text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, path) => hold(`${esc(alt)}`)); text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, path) => { if (/^https?:\/\//.test(path)) return hold(`${esc(label)}`); const image = /\.(png|svg|jpg)$/.test(path); return hold(`${esc(label)}`); }); return esc(text).replace(/\*\*(.+?)\*\*/g, '$1').replace(/\u0000(\d+)\u0000/g, (_, n) => held[n]); } const lines = markdown.split('\n'), rendered = [], toc = []; for (let i=0; i${inline(h[2])}`);if(level===2)toc.push(`${esc(h[2])}`);i++;continue;} if(line.startsWith('|')){const rows=[];while(is.trim()));const header=rows.shift();if(rows[0]?.every(s=>/^:?-+:?$/.test(s)))rows.shift();rendered.push(`
${header.map(s=>``).join('')}${rows.map(row=>`${row.map(s=>``).join('')}`).join('')}
${inline(s)}
${inline(s)}
`);continue;} if(/^(- |\d+\. )/.test(line)){const ordered=/^\d/.test(line),items=[];while(i${items.map(s=>`
  • ${inline(s)}
  • `).join('')}`);continue;} const p=[line];i++;while(i${inline(p.join(' '))}

    `); } const swatches=samples.samples.map(s=>``).join(''); const seconds=[0,14,42,55,85,110,114,130,170,187]; const gallery=seconds.map(t=>``).join(''); const contrast=samples.samples.filter(s=>/^tile\./.test(s.role)&&!s.role.includes('Side')&&s.role!=='tile.arrow').map(s=>`${s.role}↑${s.whiteContrast}:1 ${s.whiteContrast<3?'· 需适配':''}↑${s.darkContrast}:1`).join(''); const html=`HEX TRAILS · Design v2 视频用色手册
    HEX TRAILS / VIDEO COLOR STUDY

    Design v2 · 视频用色手册

    七个棋子色族 · 三套场景节奏 · 单片薄倒角适配

    10 张证据帧 / 36 个区域采样 / 完整规范 · 独立离线 HTML

    采样色板

    点击复制色值。HEX 是解码帧区域中位数,不是原游戏材质常量;本页未改动 Lab 配色。

    ${swatches}

    视频证据帧

    点击查看原尺寸参考图。图片已内嵌,断网也可查看。

    白箭头与深色箭头

    代表底色 + 不透明前景色的计算值。3:1 是项目必要图形的诊断目标,不代表整页已合规。

    ${contrast}
    色族白色 #FFFFFF深色 #273B50
    ${rendered.join('\n')}
    由 designv2.md 与 samples.json 同步生成 · npm --prefix labs/hex-video run design
    视频参考`; writeFileSync(resolve(root,'designv2.html'),html); console.log(`designv2.html: ${samples.samples.length} colors, ${seconds.length} frames, ${toc.length} chapters, standalone/offline, ${Math.round(Buffer.byteLength(html)/1024)} KiB.`);