feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user