feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>海报素材像素审计(canvas 非透明像素 + 原始/下载逐像素对比)</title>
<style>
body{font-family:monospace;font-size:12px;margin:8px;background:#111;color:#eee}
.src{margin:6px 0;padding:6px;border:1px solid #333;border-radius:4px}
.src b{color:#7ee787}.src .bad{color:#ff7b72}.src .ok{color:#7ee787}
h1{font-size:16px}h2{font-size:14px}
</style>
</head>
<body>
<h1>海报素材像素审计</h1>
<p>支持参数:<code>?only=源1,源2</code> 定向审计;<code>?max=N</code> 每源最多素材数;<code>?size=N</code> 渲染尺寸;<code>?compare=1</code> 开启「原始 vs 下载」逐像素对比。</p>
<div id="out"><p>加载中…</p></div>
<script src="js/data/poster-sources.js?v=20260823e"></script>
<script src="js/data/poster-builtin.js?v=20260823e"></script>
<script src="js/core/posterlib.js?v=20260823e"></script>
<script>
/** 渲染 svg 字符串 → 像素统计 {blank, alphaSum, data, width, height} */
function svgToCanvas(svgStr, viewBox, size) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
const c = document.createElement('canvas');
c.width = size; c.height = size;
const ctx = c.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0,0,size,size);
ctx.drawImage(img, 0, 0, size, size);
let data;
try { data = ctx.getImageData(0,0,size,size).data; }
catch(e){ resolve({blank:false, err:'getImageData受限'}); return; }
let alphaSum = 0;
for (let i = 3; i < data.length; i += 4) alphaSum += data[i];
resolve({ blank: alphaSum === 0, alphaSum, data });
};
img.onerror = () => resolve({ blank: false, err: 'SVG加载失败' });
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
});
}
/** 位图(PNG/JPG)→ 像素统计 */
function imgToCanvas(src, size) {
return new Promise(resolve => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
c.width = size; c.height = size;
const ctx = c.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0,0,size,size);
ctx.drawImage(img, 0, 0, size, size);
let data;
try { data = ctx.getImageData(0,0,size,size).data; }
catch(e){ resolve({blank:false, err:'getImageData受限', width:img.naturalWidth, height:img.naturalHeight}); return; }
let alphaSum = 0;
for (let i = 3; i < data.length; i += 4) alphaSum += data[i];
resolve({ blank: alphaSum === 0, alphaSum, data, width: img.naturalWidth, height: img.naturalHeight });
};
img.onerror = () => resolve({ blank: false, err: '位图加载失败' });
img.src = src;
});
}
/** 两张像素数据逐像素对比,返回差异率(0~1) */
function pixelDiff(a, b) {
if (!a || !b || a.length !== b.length) return 1;
let diff = 0, total = a.length / 4;
for (let i = 0; i < a.length; i += 4) {
if (Math.abs(a[i]-b[i]) + Math.abs(a[i+1]-b[i+1]) + Math.abs(a[i+2]-b[i+2]) + Math.abs(a[i+3]-b[i+3]) > 8) diff++;
}
return total ? diff / total : 0;
}
/** 由源配置重建原始 CDN URL(用于「原始 vs 下载」对比) */
function originalUrl(src, item) {
const name = item.name || String(item.id).split(':')[1];
const ext = (item.mime === 'image/jpeg') ? '.jpg' : (item.mime === 'image/png' ? '.png' : '.svg');
if (src.gh) {
const repo = src.gh + '@' + (src.ghBranch || 'master');
const dir = (src.dirs || [])[0] || '';
return `https://cdn.jsdelivr.net/gh/${repo}/${dir}/${name}${ext}`;
}
if (src.npm) {
const dir = (src.dirs || [])[0] || '';
return `https://unpkg.com/${src.npm}/${dir}/${name}${ext}`;
}
return null;
}
function wrapSvg(body, viewBox) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox || '0 0 24 24'}">${body}</svg>`;
}
async function run() {
const out = document.getElementById('out');
const qs = new URLSearchParams(location.search);
const only = qs.get('only') ? qs.get('only').split(',').map(s => s.trim()) : null;
const max = parseInt(qs.get('max') || '300', 10);
const size = parseInt(qs.get('size') || '64', 10);
const compare = qs.get('compare') === '1';
const srcMap = {};
for (const s of (window.HGB_PosterSources || [])) srcMap[s.id] = s;
const builtin = window.HGB_PosterBuiltin || { sources: {} };
const ids = Object.keys(builtin.sources || {});
if (!ids.length) { out.innerHTML = '<p>⚠️ 暂无海报素材源(poster-builtin.js 为空),请先运行 node scripts/build-posters.js。</p>'; return; }
let totalItems = 0, totalBlank = 0, totalErr = 0, totalDiff = 0;
const rows = [];
for (const sid of ids) {
if (only && !only.includes(sid)) continue;
let data = null;
try {
data = await HGB_PosterLib.loadSource(sid);
} catch (e) { totalErr++; rows.push({ sid, error: '加载失败 ' + e.message }); continue; }
if (!data || !Array.isArray(data.items)) { totalErr++; rows.push({ sid, error: '无 items 数组' }); continue; }
const items = data.items.slice(0, max);
const src = srcMap[sid] || {};
let blank = 0, diffBad = 0;
const samples = [];
for (const it of items) {
totalItems++;
// 字体无像素可审(文件完整性已由构建端 sha256/bytes 校验),跳过渲染
if (src.type === 'font') continue;
const viewBox = it.viewBox || (it.width && it.height ? `0 0 ${it.width} ${it.height}` : '0 0 24 24');
let r;
if (it.svg) {
r = await svgToCanvas(wrapSvg(it.svg, viewBox), viewBox, size);
} else if (it.file) {
r = await imgToCanvas('js/data/' + it.file, size);
// 分辨率参数校验:渲染出的原始尺寸需与构建记录一致(防止下载损坏/错位)
if (!r.err && it.width && (r.width !== it.width || r.height !== it.height)) {
r.resMismatch = true;
}
} else {
// 配色 JSON / 字体:无像素,仅结构完整性校验
continue;
}
if (r.blank) { blank++; totalBlank++; if (samples.length < 12) samples.push(it.id + (r.err ? '[' + r.err + ']' : '')); }
if (r.resMismatch) { if (samples.length < 12) samples.push(it.id + '[分辨率不符 ' + r.width + 'x' + r.height + '≠' + it.width + 'x' + it.height + ']'); }
// 「原始 vs 下载」逐像素对比:重新拉取 CDN 原始文件渲染,与下载数据比差异率
// 注:transform 源(如 heropatterns JS 模板 → 静态 SVG)为有意格式转换,无 1:1 原始文件可对比,跳过
if (compare && it.svg && !src.transform) {
const orig = originalUrl(src, it);
if (orig) {
try {
const raw = await (await fetch(orig)).text();
if (!/<svg[\s>]/i.test(raw)) continue; // 404/错误页等非 SVG 响应,跳过
const body = raw.replace(/[\s\S]*?<svg[^>]*>/, '').replace(/<\/svg>[\s\S]*/, '');
const ro = await svgToCanvas(wrapSvg(body, viewBox), viewBox, size);
if (ro.data && r.data) {
const d = pixelDiff(r.data, ro.data);
if (d > 0.01) { diffBad++; totalDiff++; if (samples.length < 12) samples.push(it.id + '[差异率 ' + (d * 100).toFixed(2) + '%]'); }
}
} catch (e) { /* 原始拉取失败忽略(网络/CDN) */ }
}
}
}
rows.push({ sid, total: items.length, blank, diffBad, samples });
out.innerHTML = '<p>进度 ' + ids.indexOf(sid) + 1 + '/' + ids.length + ' …</p>';
}
let html = '<h2>扫描 ' + totalItems.toLocaleString() + ' 个,空白 ' + totalBlank.toLocaleString() + ',失败 ' + totalErr + (compare ? ',像素差异异常 ' + totalDiff : '') + '</h2>';
rows.sort((a, b) => b.blank - a.blank).forEach(r => {
if (r.error) html += '<div class="src">✗ ' + r.sid + ':' + r.error + '</div>';
else html += '<div class="src"><b>' + r.sid + '</b>:空白 <span class="' + (r.blank ? 'bad' : 'ok') + '">' + r.blank + '/' + r.total + '</span>' +
(r.diffBad ? ' · 像素差异异常 <span class="bad">' + r.diffBad + '</span>' : '') +
(r.samples.length ? '<br>' + r.samples.join('<br>') : '') + '</div>';
});
out.innerHTML = html;
document.title = 'POSTER-AUDIT-DONE ' + totalItems + '/' + totalBlank + '/' + totalErr;
}
run();
</script>
</body>
</html>