import { readFileSync, writeFileSync } from 'node:fs'; import { THEME, CONTRAST_PAIRS, contrast, TYPOGRAPHY, MOTION, RADII } from '../src/design-tokens.js'; import { ICONS, ICON_SIZE, ICON_ATLAS } from '../src/ui-skin.js'; import { TILE_ATLAS, TILE_FRAMES, TILE_ASPECT } from '../src/sprites.js'; import { tileSample, roundedHex } from '../src/tile-shape.js'; import { drawScene, drawTile } from '../src/renderer.js'; import { layout } from '../src/geometry.js'; import { LEVELS } from '../src/levels.js'; const read=name=>readFileSync(new URL('../'+name,import.meta.url),'utf8'); const dataImage=name=>'data:image/png;base64,'+readFileSync(new URL('../'+name,import.meta.url)).toString('base64'); const esc=value=>String(value).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const markdown=read('Design.md'); // This intentionally small renderer supports the headings, lists, links and tables // used by Design.md. Escape text before formatting; do not execute embedded HTML. function inline(value){return esc(value).replace(/`([^`]+)`/g,'$1').replace(/\*\*(.+?)\*\*/g,'$1').replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,'$1');} function renderMarkdown(source){ const lines=source.split('\n'),out=[];let i=0,heading=0; while(i${inline(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();out.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(' '))}

    `); } return out.join('\n'); } const iconNames={gear:'设置',grid:'选关',back:'返回',close:'关闭',left:'左转',right:'右转',undo:'撤销',restart:'重来',bulb:'提示',star:'星星',edit:'编辑',play:'试玩',arrow:'方向',erase:'擦除',track:'轨道',gem:'六角'}; const icon=name=>``; const iconCSS=Object.entries(ICONS).map(([name,f])=>`.icon-${name}{background-size:${ICON_SIZE.w/f.w*100}% ${ICON_SIZE.h/f.h*100}%;background-position:${f.x/(ICON_SIZE.w-f.w)*100}% ${f.y/(ICON_SIZE.h-f.h)*100}%}`).join('\n'); const meanings={background:'游戏背景',board:'棋盘底格',boardGrid:'六边形分格',track:'轨道中心线',trackHalo:'轨道浅色外缘',surface:'弹窗与编辑面板',textPrimary:'主要文字',textSecondary:'说明与辅助文字',primary:'主操作与当前选中',onPrimary:'主操作上的文字',secondary:'次级操作表面',utility:'工具图标底板',onUtility:'工具图标与文字',selection:'弱选中表面',onSelection:'弱选中上的文字',disabled:'原生禁用参考表面',onDisabled:'原生禁用参考文字',input:'输入框表面',reward:'奖励与星星强调',warning:'警告文字',warningSurface:'警告提示表面',scrim:'模态遮罩 · 约 48%',focus:'键盘焦点',tileBlue:'蓝棋子 · 代表采样值',tilePink:'粉棋子 · 代表采样值',tileYellow:'黄棋子 · 代表采样值',tilePurple:'紫棋子 · 代表采样值',arrowLight:'蓝 / 粉 / 紫棋子箭头',arrowDark:'黄色棋子箭头'}; const swatches=Object.entries(THEME).map(([name,hex])=>``).join(''); const reports=CONTRAST_PAIRS.map(([label,fg,bg,min])=>({label,fg,bg,min,ratio:contrast(THEME[fg],THEME[bg])})); const pairRows=reports.map(r=>`${r.min===3?'➜':'Aa'}${r.label}${r.fg}
    ${r.bg}
    ${r.ratio.toFixed(2)}:1≥ ${r.min}:1${r.ratio>=r.min?'通过':'需调整'}`).join(''); // Record the game's actual renderer commands as SVG for a static composition. // Original PNG pixels are embedded unchanged; shared sampling and clip geometry // match the game. This is a documentation adapter, not an alternative game UI. let serial=0; function sceneSVG(render,label){const nodes=[],defs=[];const points=pts=>pts.map(p=>`${p.x.toFixed(3)},${p.y.toFixed(3)}`).join(' '); const painter={ polygon(pts,fill){nodes.push(``);}, line(pts,color,width){nodes.push(``);}, circle(x,y,r,fill){nodes.push(``);}, sprite(index,x,y,r){const f=tileSample(TILE_FRAMES[index]),w=r*2,h=w*TILE_ASPECT,id=`tile-clip-${serial++}`;defs.push(``);nodes.push(``);return true;}, }; render(painter);return `${defs.join('')}${nodes.join('')}`; } const level=LEVELS[1]; const board=sceneSVG(p=>drawScene(p,level,level.tiles,layout(level.cells,360,360)),'第二关棋盘静态展示,使用游戏实际轨道、棋子图片与颜色'); const tiles=sceneSVG(p=>[0,1,2,3].forEach((color,i)=>drawTile(p,95+(i%2)*170,95+Math.floor(i/2)*170,75,{color,dir:0})),'蓝、粉、黄、紫四种实际棋子,黄色使用深色箭头'); const replacements={ TOKENS:Object.entries(THEME).map(([k,v])=>`--${k}:${v};`).join('')+`--pressMs:${MOTION.pressMs}ms;--modalMs:${MOTION.modalMs}ms;--starOne:${MOTION.starStartMs}ms;--starTwo:${MOTION.starStartMs+MOTION.starGapMs}ms;--starThree:${MOTION.starStartMs+MOTION.starGapMs*2}ms;--controlRadius:${RADII.control}px;--dialogRadius:${RADII.dialog}px;`,ICON_ATLAS:dataImage(ICON_ATLAS),ICON_CSS:iconCSS, TILE_IMAGE:``, BOARD:board,TILES:tiles,MOVES:level.moveLimit,SWATCHES:swatches,PAIR_ROWS:pairRows, ICONS:Object.keys(ICONS).map(name=>`
    ${icon(name)}${iconNames[name]}${name}
    `).join(''), SPEC:renderMarkdown(markdown),PAYLOAD:JSON.stringify({markdown,theme:THEME,typography:TYPOGRAPHY,motion:MOTION,radii:RADII}).replace(/{if(!(key in replacements))throw new Error(`Unknown guide placeholder ${key}`);return replacements[key];}); writeFileSync(new URL('../Design.html',import.meta.url),output); console.log(`Design.html built: ${Object.keys(THEME).length} colors, ${Object.keys(ICONS).length} original icons, ${reports.length} contrast pairs, complete specification. Standalone / offline.`);