feat: version campaigns and document original puzzle design

Restore v0.1 and v0.2 archives with separate progress, publish the curved v0.3 campaign, and document the original 24-level design with reproducible evidence. Refine board presentation and movement, and keep video lab tooling within its own directory.
This commit is contained in:
cabinw
2026-09-09 13:17:30 +08:00
parent 70b021d4cc
commit e92cccba82
52 changed files with 76449 additions and 15076 deletions
@@ -0,0 +1,52 @@
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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(`<code>${esc(value)}</code>`));
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, path) => hold(`<img class="palette" alt="${esc(alt)}" src="${asset(path)}">`));
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, path) => {
if (/^https?:\/\//.test(path)) return hold(`<a href="${esc(path)}" target="_blank" rel="noopener noreferrer">${esc(label)}</a>`);
const image = /\.(png|svg|jpg)$/.test(path);
return hold(`<a href="${asset(path)}" ${image ? 'data-image="true"' : `download="${esc(path.split('/').at(-1))}"`}>${esc(label)}</a>`);
});
return esc(text).replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/\u0000(\d+)\u0000/g, (_, n) => held[n]);
}
const lines = markdown.split('\n'), rendered = [], toc = [];
for (let i=0; i<lines.length;) {
const line=lines[i];
if (!line.trim()) { i++; continue; }
const h=line.match(/^(#{1,6})\s+(.+)$/);
if(h){const level=h[1].length,id=`section-${i}`;rendered.push(`<h${level} id="${id}">${inline(h[2])}</h${level}>`);if(level===2)toc.push(`<a href="#${id}">${esc(h[2])}</a>`);i++;continue;}
if(line.startsWith('|')){const rows=[];while(i<lines.length&&lines[i].startsWith('|'))rows.push(lines[i++].split('|').slice(1,-1).map(s=>s.trim()));const header=rows.shift();if(rows[0]?.every(s=>/^:?-+:?$/.test(s)))rows.shift();rendered.push(`<div class="table-wrap"><table><thead><tr>${header.map(s=>`<th>${inline(s)}</th>`).join('')}</tr></thead><tbody>${rows.map(row=>`<tr>${row.map(s=>`<td>${inline(s)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`);continue;}
if(/^(- |\d+\. )/.test(line)){const ordered=/^\d/.test(line),items=[];while(i<lines.length&&(ordered?/^\d+\. /:/^- /).test(lines[i]))items.push(lines[i++].replace(/^(- |\d+\. )/,''));const tag=ordered?'ol':'ul';rendered.push(`<${tag}>${items.map(s=>`<li>${inline(s)}</li>`).join('')}</${tag}>`);continue;}
const p=[line];i++;while(i<lines.length&&lines[i].trim()&&!/^(#|\||- |\d+\. )/.test(lines[i]))p.push(lines[i++]);rendered.push(`<p>${inline(p.join(' '))}</p>`);
}
const swatches=samples.samples.map(s=>`<button class="swatch" data-color="${s.hex}" data-search="${s.role} ${s.hex}" aria-label="复制 ${s.role} ${s.hex}"><span class="chip" style="background:${s.hex}"></span><b>${s.role}</b><code>${s.hex}</code><small>${s.second}s · ROI ${s.roi.join(', ')}</small></button>`).join('');
const seconds=[0,14,42,55,85,110,114,130,170,187];
const gallery=seconds.map(t=>`<button class="frame" data-frame="${t}"><img loading="lazy" src="${asset(`reference/color-study/${String(t).padStart(3,'0')}.png`)}" alt="视频参考帧 ${Math.floor(t/60)}:${String(t%60).padStart(2,'0')}"><span>${Math.floor(t/60)}:${String(t%60).padStart(2,'0')}</span></button>`).join('');
const contrast=samples.samples.filter(s=>/^tile\./.test(s.role)&&!s.role.includes('Side')&&s.role!=='tile.arrow').map(s=>`<tr><td>${s.role}</td><td><span class="arrow" style="background:${s.hex};color:white">↑</span>${s.whiteContrast}:1 ${s.whiteContrast<3?'· 需适配':''}</td><td><span class="arrow" style="background:${s.hex};color:#273B50">↑</span>${s.darkContrast}:1</td></tr>`).join('');
const html=`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>HEX TRAILS · Design v2 视频用色手册</title><style>
:root{font-family:Inter,"PingFang SC","Microsoft YaHei",sans-serif;color:#273b50;background:#f4f6f9;line-height:1.8}*{box-sizing:border-box}body{margin:0}a{color:#1763ac;text-underline-offset:3px}button,input{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid #007ac5;outline-offset:3px}[hidden]{display:none!important}header{padding:48px max(24px,calc((100vw - 1240px)/2));background:linear-gradient(120deg,#0c61e8,#158fcc);color:white}header p{opacity:.9;margin:8px 0}header h1{font-size:34px;line-height:1.4;margin:12px 0}header a{color:white}.eyebrow{font-size:12px;letter-spacing:3px}main{display:grid;grid-template-columns:210px minmax(0,1fr);gap:40px;max-width:1290px;margin:36px auto;padding:0 24px}aside{position:sticky;top:24px;align-self:start}aside a{display:block;font-size:13px;text-decoration:none;padding:5px 8px;border-radius:6px}aside a:hover{background:#e3edf8}article{min-width:0}section,.spec{background:white;border:1px solid #e0e7ee;border-radius:18px;padding:28px;margin-bottom:28px}h2{font-size:24px;margin-top:8px}h3{font-size:19px;margin-top:32px}h4{font-size:16px}h1,h2,h3,h4{scroll-margin-top:24px}p,li{font-size:14px}code{font-family:ui-monospace,monospace;font-size:.9em;overflow-wrap:anywhere;background:#eff3f7;border-radius:4px;padding:2px 4px}.tools{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.tools a,.tools button{border:1px solid #b9cce0;border-radius:8px;padding:7px 14px;text-decoration:none;background:white;color:#285578;font-size:13px}input{padding:10px 12px;width:100%;border:1px solid #bccbd8;border-radius:9px;margin-bottom:20px}.swatches{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.swatch{border:1px solid #dce3eb;border-radius:10px;padding:0 0 10px;overflow:hidden;text-align:left;background:white;color:#273b50;min-width:0}.chip{display:block;height:76px;border-bottom:1px solid #0001}.swatch b,.swatch code,.swatch small{display:block;margin:6px 10px 0;overflow-wrap:anywhere}.swatch b{font-size:11px}.swatch small{font-size:10px;color:#5e7182}.gallery{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}.frame{padding:0;border:1px solid #d9e2ed;border-radius:9px;overflow:hidden;background:#eef3f8;color:#334e64}.frame img{width:100%;display:block}.frame span{font-size:12px}.table-wrap{overflow:auto;margin:18px 0}table{border-collapse:collapse;width:100%;font-size:13px}td,th{padding:12px;text-align:left;border-bottom:1px solid #dfe6ee;vertical-align:top;min-width:90px}th{background:#edf3f8;color:#314d67}td code{white-space:normal}.arrow{display:inline-grid;place-items:center;width:44px;height:44px;clip-path:polygon(25% 0,75% 0,100% 50%,75% 100%,25% 100%,0 50%);font-size:27px;font-weight:bold;margin-right:12px;vertical-align:middle}.palette{width:100%;height:auto}.note{color:#617489;font-size:12px}.spec>h1{font-size:28px}.spec>h2{border-top:1px solid #dfe6ee;padding-top:28px;margin-top:40px}dialog{border:0;border-radius:14px;padding:14px;max-width:95vw;max-height:95vh;background:#f6f8fb}dialog::backdrop{background:#111c2ae0}dialog img{display:block;max-width:85vw;max-height:80vh;object-fit:contain;margin:auto}dialog button{display:block;margin:10px auto 0;border:0;border-radius:6px;background:#214f79;color:white;padding:6px 20px}#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#233f58;color:white;border-radius:9px;padding:10px 18px;z-index:9;font-size:13px}footer{text-align:center;padding:20px;color:#627487;font-size:12px}@media(max-width:900px){main{grid-template-columns:1fr}aside{position:static;display:flex;flex-wrap:wrap;gap:5px}.swatches{grid-template-columns:repeat(3,minmax(0,1fr))}.gallery{grid-template-columns:repeat(5,1fr)}}@media(max-width:520px){header{padding:30px 20px}header h1{font-size:26px}main{padding:0 12px;gap:16px}section,.spec{padding:18px}.swatches{grid-template-columns:repeat(2,minmax(0,1fr))}.gallery{grid-template-columns:repeat(3,1fr)}}@media print{aside,.tools,input,#toast,dialog{display:none!important}main{display:block;padding:0;margin:0}header{background:white;color:#273b50;padding:12px}section,.spec{border:0;padding:0}table{font-size:10px}tr{break-inside:avoid}*{print-color-adjust:exact}}
</style></head><body><header><div class="eyebrow">HEX TRAILS / VIDEO COLOR STUDY</div><h1>Design v2 · 视频用色手册</h1><p>七个棋子色族 · 三套场景节奏 · 单片薄倒角适配</p><p>10 张证据帧 / 36 个区域采样 / 完整规范 · 独立离线 HTML</p><div class="tools"><a download="designv2.md" href="${asset('designv2.md')}">下载 Markdown</a><a download="samples.json" href="${asset('reference/color-study/samples.json')}">下载采样 JSON</a><button id="print">打印 / 保存 PDF</button></div></header><main><aside><a href="#colors">采样色板</a><a href="#frames">视频证据帧</a><a href="#contrast">箭头对比</a>${toc.join('')}</aside><article><section id="colors"><h2>采样色板</h2><p class="note">点击复制色值。HEX 是解码帧区域中位数,不是原游戏材质常量;本页未改动 Lab 配色。</p><label for="search">搜索角色或 HEX</label><input id="search" type="search" placeholder="例如 tile、map、#4787ED"><div class="swatches">${swatches}</div><p id="empty" hidden>没有匹配的颜色。</p></section><section id="frames"><h2>视频证据帧</h2><p class="note">点击查看原尺寸参考图。图片已内嵌,断网也可查看。</p><div class="gallery">${gallery}</div></section><section id="contrast"><h2>白箭头与深色箭头</h2><p class="note">代表底色 + 不透明前景色的计算值。3:1 是项目必要图形的诊断目标,不代表整页已合规。</p><div class="table-wrap"><table><thead><tr><th>色族</th><th>白色 #FFFFFF</th><th>深色 #273B50</th></tr></thead><tbody>${contrast}</tbody></table></div></section><div class="spec">${rendered.join('\n')}</div></article></main><footer>由 designv2.md 与 samples.json 同步生成 · npm --prefix labs/hex-video run design</footer><dialog id="viewer" aria-label="视频原尺寸参考帧"><img id="viewer-image" alt="视频参考"><button id="close">关闭</button></dialog><div id="toast" role="status" hidden></div><script>
const toast=document.getElementById('toast');let timer;
function notify(message){toast.textContent=message;toast.hidden=false;clearTimeout(timer);timer=setTimeout(()=>toast.hidden=true,2300);}
document.querySelectorAll('[data-color]').forEach(button=>button.onclick=async()=>{const color=button.dataset.color;try{await navigator.clipboard.writeText(color);notify('已复制 '+color);}catch{notify('请手动复制 '+color);const range=document.createRange();range.selectNodeContents(button.querySelector('code'));const selection=getSelection();selection.removeAllRanges();selection.addRange(range);}});
document.getElementById('search').oninput=e=>{const query=e.target.value.trim().toLowerCase();let count=0;document.querySelectorAll('[data-search]').forEach(el=>{el.hidden=!el.dataset.search.toLowerCase().includes(query);if(!el.hidden)count++;});document.getElementById('empty').hidden=count>0;};
const viewer=document.getElementById('viewer'),picture=document.getElementById('viewer-image');function openImage(src,alt){picture.src=src;picture.alt=alt;viewer.showModal();}
document.querySelectorAll('[data-frame]').forEach(b=>b.onclick=()=>openImage(b.querySelector('img').src,b.querySelector('img').alt));document.querySelectorAll('[data-image]').forEach(a=>a.onclick=e=>{e.preventDefault();openImage(a.href,a.textContent);});document.getElementById('close').onclick=()=>viewer.close();viewer.onclick=e=>{if(e.target===viewer)viewer.close();};document.getElementById('print').onclick=()=>print();
</script></body></html>`;
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.`);
+15
View File
@@ -0,0 +1,15 @@
import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
const root = new URL('../', import.meta.url);
const out = new URL('dist/', root);
await rm(out, { recursive: true, force: true });
await mkdir(new URL('labs/hex-video/', out), { recursive: true });
await mkdir(new URL('src/', out), { recursive: true });
for (const name of ['index.html','lab.js','lab.css','fixture.js','designv2.md','designv2.html','README.md','reference']) {
await cp(new URL(name, root), new URL('labs/hex-video/'+name, out), { recursive: true });
}
for (const name of ['core.js','geometry.js','path-math.js']) {
await cp(new URL('../../src/'+name, root), new URL('src/'+name, out));
}
await cp(new URL('../../Design.md', root), new URL('Design.md', out));
await writeFile(new URL('index.html', out), '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=labs/hex-video/index.html"><title>HEX Video Lab</title><a href="labs/hex-video/index.html">打开 HEX Video Lab</a></html>');
console.log('Hex Video build ready in labs/hex-video/dist/');