Files
drawlogo/audit-pixels.html

128 lines
5.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>