feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<!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>逐帧渲染表情方案:① 每帧非透明像素 > 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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
/** 渲染一帧 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>
|
||||
Reference in New Issue
Block a user