128 lines
5.7 KiB
HTML
128 lines
5.7 KiB
HTML
<!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}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>像素审计</h1>
|
||
<div id="out"><p>加载中…</p></div>
|
||
<script>
|
||
const STROKE_SOURCES = new Set(['feather','lucide','tabler']);
|
||
function isStroke(ic, src){
|
||
if (ic.type && ic.type !== 'icon') return false;
|
||
if (STROKE_SOURCES.has(src)) return true;
|
||
const svg = ic.svg || '';
|
||
if (/stroke\s*=|\bstroke-linecap\s*=|\bstroke-linejoin\s*=/.test(svg)) return true;
|
||
// 显式 stroke-width="0"(Carbon 等填充型源自带,防默认描边):实为填充型(与 panels.js 一致)
|
||
if (/stroke-width="0"/.test(svg)) return false;
|
||
// 与 panels.js isStrokeIcon 一致:全几何元素 fill=none 无 stroke → 描边型
|
||
const geoms = svg.match(/<(?:path|circle|rect|ellipse|line|polyline|polygon)\b[^>]*>/g);
|
||
if (geoms && geoms.length && !/fill\s*=\s*"(?!none")[^"]*"/.test(svg)) return true;
|
||
return false;
|
||
}
|
||
/** 与 panels.js prepareIconBody 一致:currentColor 替换为显式色值(fill/stroke 一律实色) */
|
||
function prepareBody(body, isSt){
|
||
if (!body) return '';
|
||
return body
|
||
.replace(/fill="currentColor"/g, 'fill="#374151"')
|
||
.replace(/stroke="currentColor"/g, 'stroke="#374151"')
|
||
.replace(/stop-color="currentColor"/g, 'stop-color="#374151"');
|
||
}
|
||
function applyAttrs(el, str){
|
||
const re = /([\w-]+)\s*=\s*"([^"]*)"/g;
|
||
let m;
|
||
while ((m = re.exec(str))) el.setAttribute(m[1], m[2]);
|
||
}
|
||
/** 渲染 svg 字符串 → 像素统计 {blank, alphaSum},async */
|
||
function svgToCanvas(svgStr, 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 });
|
||
};
|
||
img.onerror = () => resolve({ blank: false, err: 'SVG加载失败' });
|
||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
|
||
});
|
||
}
|
||
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') || '48', 10);
|
||
const files = await (await fetch('/audit-files.json')).json();
|
||
const holder = document.createElement('div');
|
||
holder.style.cssText = 'position:fixed;left:-9999px;top:0;width:24px;height:24px;';
|
||
document.body.appendChild(holder);
|
||
let totalIcons = 0, totalBlank = 0, totalErr = 0;
|
||
const rows = [];
|
||
for (let fi = 0; fi < files.length; fi++) {
|
||
const f = files[fi];
|
||
const sid = f.replace(/\.js$/,'');
|
||
if (sid.startsWith('dicebear-')) continue;
|
||
if (only && !only.includes(sid)) continue;
|
||
let data = null;
|
||
try {
|
||
const txt = await (await fetch('/js/data/sources/' + f)).text();
|
||
const obj = txt.replace(/^[^\n]*?window\[[^\]]+\]\s*=\s*/, '').replace(/;\s*$/, '');
|
||
data = new Function('return (' + obj + ');')();
|
||
} catch(e){ totalErr++; rows.push({sid, error: '加载失败 ' + e.message}); continue; }
|
||
if (!data || !Array.isArray(data.icons)) { totalErr++; rows.push({sid, error:'无 icons 数组'}); continue; }
|
||
const icons = data.icons.slice(0, max);
|
||
let blank = 0;
|
||
const samples = [];
|
||
for (const ic of icons) {
|
||
totalIcons++;
|
||
const preserve = ic.type && ic.type !== 'icon';
|
||
const isSt = preserve ? false : isStroke(ic, sid);
|
||
const attrs = isSt
|
||
? 'fill="none" stroke="#374151" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"'
|
||
: 'fill="#374151" stroke="none"';
|
||
const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
|
||
svg.setAttribute('viewBox','0 0 24 24');
|
||
applyAttrs(svg, attrs);
|
||
// 与 panels.js 渲染一致:多色素材(emoji/品牌)保留原 body 直接渲染,不做 currentColor 替换;
|
||
// 其余按描边/填充型处理
|
||
const body = preserve ? ic.svg : prepareBody(ic.svg, isSt);
|
||
svg.innerHTML = body;
|
||
const full = new XMLSerializer().serializeToString(svg);
|
||
const r = await svgToCanvas(full, size);
|
||
if (r.blank) {
|
||
blank++;
|
||
totalBlank++;
|
||
if (samples.length < 12) samples.push(ic.id + (r.err ? '[' + r.err + ']' : ''));
|
||
}
|
||
}
|
||
if (blank) rows.push({ sid, total: icons.length, blank, samples });
|
||
if (fi % 10 === 0) out.innerHTML = '<p>进度 ' + (fi+1) + '/' + files.length + ' …</p>';
|
||
}
|
||
let html = '<h2>扫描 ' + totalIcons.toLocaleString() + ' 个,空白 ' + totalBlank.toLocaleString() + ',失败 ' + totalErr + '</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="bad">' + r.blank + '/' + r.total + '</span>(' + (r.blank/r.total*100).toFixed(2) + '%)' +
|
||
(r.samples.length ? '<br>' + r.samples.join('<br>') : '') + '</div>';
|
||
});
|
||
out.innerHTML = html;
|
||
document.title = 'PIXEL-DONE ' + totalIcons + '/' + totalBlank + '/' + totalErr;
|
||
}
|
||
run();
|
||
</script>
|
||
</body>
|
||
</html>
|