Files
drawlogo/audit-emojis.html

148 lines
6.6 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>动态表情像素审计(逐帧非透明像素 + 帧间变化检测)</title>
<style>
body{font-family:monospace;font-size:12px;margin:8px;background:#111;color:#eee}
h1{font-size:16px}h2{font-size:14px;margin:14px 0 6px}
.src{margin:6px 0;padding:6px;border:1px solid #333;border-radius:4px}
.src b{color:#7ee787}.src .bad{color:#ff7b72}.src .warn{color:#d29922}
.ok{color:#7ee787}.fail{color:#ff7b72}
</style>
</head>
<body>
<h1>动态表情像素审计</h1>
<p>逐帧渲染表情方案:① 每帧非透明像素 &gt; 0(无空白帧)② 帧间确有变化(动画真实生效)③ GIF 编码头校验。</p>
<div id="out"><p>加载中…</p></div>
<script src="js/data/icons.js"></script>
<script src="js/data/industries.js"></script>
<script src="js/data/styles.js?v=20260813"></script>
<script src="js/data/palettes.js"></script>
<script src="js/data/fonts.js"></script>
<script src="js/data/decorations.js"></script>
<script src="js/core/store.js?v=20260816"></script>
<script src="js/core/ai.js?v=20260927a"></script>
<script src="js/core/exporter.js?v=20260812h"></script>
<script src="js/core/generator.js?v=20260815c"></script>
<script src="js/core/avatarGen.js?v=20260815c"></script>
<script src="js/core/motion.js?v=20260823h"></script>
<script src="js/core/gifEncoder.js?v=20260823e"></script>
<script src="js/core/emojiGen.js?v=20260824a"></script>
<script src="js/core/emojiAi.js?v=20260824a"></script>
<script src="js/sticker-skills/core.js?v=20260823e"></script>
<script src="js/sticker-skills/skills.js?v=20260824a"></script>
<script src="js/data/builtin-icons.js?v=20260823g"></script>
<script src="js/core/iconlib.js?v=20260823g"></script>
<script>
'use strict';
const out = document.getElementById('out');
let pass = 0, fail = 0;
function check(name, ok, detail) {
if (ok) { pass++; console.log(' ✅ ' + name); }
else { fail++; console.log(' ❌ ' + name + (detail ? ' — ' + detail : '')); }
}
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
/** 渲染一帧 SVG → canvas,返回 {canvas, alphaSum} */
async function renderFrame(svgStr, size) {
const canvas = await HGB_Exporter.renderCanvas(size, null, svgStr);
const ctx = canvas.getContext('2d');
const data = ctx.getImageData(0, 0, size, size).data;
let alphaSum = 0;
for (let i = 3; i < data.length; i += 4) alphaSum += data[i];
return { canvas, alphaSum };
}
/** 两帧差异像素比例 */
function diffRatio(a, b, size) {
const da = a.getContext('2d').getImageData(0, 0, size, size).data;
const db = b.getContext('2d').getImageData(0, 0, size, size).data;
let diff = 0;
for (let i = 0; i < da.length; i += 4) {
if (da[i] !== db[i] || da[i+1] !== db[i+1] || da[i+2] !== db[i+2] || da[i+3] !== db[i+3]) diff++;
}
return diff / (size * size);
}
async function run() {
const size = 120;
const qs = new URLSearchParams(location.search);
const types = qs.get('only') ? qs.get('only').split(',') : null;
// GIF 编码器自检(确定性)
let gifOk = false, gifHead = '';
try {
const c = document.createElement('canvas'); c.width = 32; c.height = 32;
const c1 = document.createElement('canvas'); c1.width = 32; c1.height = 32;
const b = HGB_GifEncoder.encode([c, c1], { delay: 83 });
gifHead = String.fromCharCode(b[0], b[1], b[2], b[3], b[4], b[5]);
gifOk = gifHead === 'GIF89a';
} catch (e) { gifHead = 'err:' + e.message; }
check('GIF 编码头 = GIF89a', gifOk, gifHead);
// 表情技能库完整性
const skillIds = HGB_StickerSkills.all().map(s => s.id);
check('表情技能 12 个注册', skillIds.length === 12, '实际 ' + skillIds.length);
check('全部技能 test 通过', skillIds.every(id => {
try { const r = HGB_StickerSkills.test(id); return !!r && r.error === undefined && r.ok !== false; }
catch (e) { return false; }
}));
// 生成整组表情包(同图 × 全部动画类型)
const plans = await HGB_EmojiGen.generateEmojiSet({ industry: HGB_Industries.find('food'), brand: '表情' });
const audited = plans.filter(p => !types || types.includes(p.anim.type));
const rows = [];
let totalFrames = 0, totalBlank = 0, totalStatic = 0;
for (const plan of audited) {
const a = plan.anim;
const t = HGB_Motion.animTypes().find(x => x.id === a.type);
const n = HGB_Motion.frameCount(a);
const step = Math.max(1, Math.ceil(n / 12));
const frameIdx = [];
for (let i = 0; i < n; i += step) frameIdx.push(i);
let blank = 0, err = 0;
const canvases = [];
for (const i of frameIdx) {
totalFrames++;
const tF = HGB_Motion.frameT(i, a);
try {
const svg = HGB_EmojiGen.buildFrameSvg(HGB_EmojiGen.markElements(plan), null, size, tF, a, {});
const { canvas, alphaSum } = await renderFrame(svg, size);
if (alphaSum === 0) blank++;
canvases.push(canvas);
} catch (e) { err++; console.log('帧失败 ' + t.id + '#' + i + ': ' + e.message); }
}
// 帧间差异:首帧 vs 各后续帧
let changed = 0;
if (canvases.length > 1) {
for (let k = 1; k < canvases.length; k++) {
const r = diffRatio(canvases[0], canvases[k], size);
if (r > 0.001) changed++;
}
}
if (blank) totalBlank += blank;
if (changed === 0) totalStatic++;
check('动画 ' + t.id + ':' + frameIdx.length + ' 帧无空白', blank === 0 && err === 0, '空白 ' + blank + ' 失败 ' + err);
check('动画 ' + t.id + ':帧间确有变化', changed > 0, '变化帧 ' + changed + '/' + (canvases.length - 1));
rows.push({ id: t.id, frames: frameIdx.length, blank, err, changed });
}
check('全动画类型零空白帧', totalBlank === 0, '空白 ' + totalBlank + '/' + totalFrames);
check('全动画类型均有真实运动', totalStatic === 0, '静态 ' + totalStatic + '/' + audited.length);
let html = '<h2>✅ 通过 ' + pass + ' / 失败 ' + fail + '(' + totalFrames + ' 帧采样)</h2>';
rows.forEach(r => {
const ok = r.blank === 0 && r.err === 0 && r.changed > 0;
html += '<div class="src"><b>' + r.id + '</b>:' + r.frames + ' 帧,空白 <span class="' + (r.blank ? 'bad' : 'ok') + '">' + r.blank + '</span>,失败 ' + r.err + ',变化帧 <span class="' + (r.changed ? 'ok' : 'warn') + '">' + r.changed + '</span> → <span class="' + (ok ? 'ok' : 'fail') + '">' + (ok ? '通过' : '不通过') + '</span></div>';
});
out.innerHTML = html;
document.title = '表情审计 ' + pass + ' 通过 / ' + fail + ' 失败';
}
run().catch(e => { out.innerHTML = '<p class="fail">运行异常:' + esc(e.message) + '</p>'; });
</script>
</body>
</html>