Files
2026-09-20 21:22:03 +08:00

117 lines
6.1 KiB
JavaScript
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.
'use strict';
// 动态缩略图的回归测试。渲染本身要 DOM canvas,这里锁住所有能脱离浏览器验证的部分:
// 档位配置、目标体积默认值、帧数/尺寸换算、可见范围算法、WebP 封装、自动定档策略。
const assert = require('node:assert/strict');
const profiles = require('../js/animated-profiles.js');
const encoder = require('../js/animated-webp-encoder.js');
const visibleBounds = require('../js/spine-visible-bounds.js');
const renderer = require('../js/renderer.js');
// —— 目标体积:每秒 25 KB,夹在 25–100 KB(跟主插件的默认值一致)——
assert.equal(profiles.SIZE_PER_SECOND, 25);
assert.equal(profiles.DEFAULT_OUTPUT_SIZE, 200);
assert.equal(profiles.DEFAULT_PRIORITY, 'resolution');
assert.equal(profiles.suggestSize(0.2), 25);
assert.equal(profiles.suggestSize(1), 25);
assert.equal(profiles.suggestSize(2), 50);
assert.equal(profiles.suggestSize(10), 100);
// —— 档位表:细节分辨率永远不超过输出画布边长 ——
const ladder = profiles.animatedProfiles('resolution', 200);
assert.equal(ladder.length, 10);
assert.ok(ladder.every((profile) => profile.size <= 200), '细节分辨率不能超过输出画布');
assert.ok(ladder.every((profile) => profile.fps >= 1 && profile.quality >= 0 && profile.quality <= 1));
assert.deepEqual(profiles.animatedProfiles('resolution', 400).slice(0, 5).map((p) => p.size), [400, 400, 400, 400, 400]);
// —— 搜索路径:首尾保留锚点,中间是插值 ——
const searchPath = profiles.expandAnimatedPath('resolution', 200);
assert.deepEqual(searchPath[0], { size: 200, fps: 18, quality: 1 });
assert.equal(searchPath[searchPath.length - 1].fps, 6);
assert.ok(searchPath.every((point) => point.size <= 200));
// —— 帧数换算:至少 2 帧、不超过采样帧数、按 ceil(range × fps) ——
assert.equal(renderer.profileFrameCount(100, 1, 12), 12);
assert.equal(renderer.profileFrameCount(100, 1, 200), 100, '帧率再高也不能超过采样帧数');
assert.equal(renderer.profileFrameCount(100, 0.001, 1), 2);
// —— 细节分辨率:等于画布时原样返回,低于画布时先降后升、尺寸不变 ——
const pixels = new Uint8Array(4 * 4 * 4).fill(7);
assert.equal(renderer.scaledFramePixels(pixels, 4, 4), pixels);
const scaled = renderer.scaledFramePixels(pixels, 4, 2);
assert.equal(scaled.length, 16 * 4);
assert.equal(scaled[0], 7);
// —— 可见范围:全透明不算范围,单个像素能定位 ——
assert.equal(visibleBounds.findAlphaBounds(new Uint8Array(4 * 4 * 4), 4, 4), null);
const box = new Uint8Array(4 * 4 * 4);
box[(1 * 4 + 1) * 4 + 3] = 255;
assert.deepEqual(visibleBounds.findAlphaBounds(box, 4, 4), { left: 1, top: 1, right: 2, bottom: 2 });
assert.deepEqual(visibleBounds.toWorldBounds({ left: 0, top: 0, right: 8, bottom: 8 }, 8, 8, 2, 0, 0), {
x: -2, y: -2, width: 4, height: 4,
});
// —— 取骨骼里的第一个有效动画;时长为 0 的动画要跳过 ——
assert.equal(renderer.animationDurationOf({ duration: 1.5 }), 1.5);
assert.equal(renderer.animationDurationOf({ duration: 0, timelines: [{ frames: [[0.5], [1]] }] }), 1);
assert.equal(
renderer.firstAnimation({ animations: [{ name: 'empty', duration: 0 }, { name: 'idle', duration: 1 }] }).name,
'idle',
'时长为 0 的动画不能当成动态缩略图的素材',
);
assert.equal(renderer.firstAnimation({ animations: [] }), null);
// —— 自动定档:和主插件画廊默认打勾的一致,取实际体积最小的那一档 ——
(async () => {
const candidates = [60, 10, 120, 30].map((kb) => ({ buffer: Buffer.alloc(kb * 1024), kb }));
assert.equal(renderer.smallestCandidate(candidates).kb, 10, '要挑体积最小的那一档,跟顺序无关');
assert.equal(renderer.smallestCandidate([]), null);
// —— 候选插值:给定搜索路径应返回指定数量的合法档位 ——
const size = 8;
const frames = Array.from({ length: 3 }, () => new Uint8Array(size * size * 4).fill(9));
const fakeEncode = async (_slot, framePixels, quality) => Buffer.alloc(Math.max(1, Math.round(framePixels.length * quality / 16)));
const picked = await renderer.pickAnimatedCandidates(searchPath, frames, size, 1, 20 * 1024, 10, fakeEncode);
assert.equal(picked.length, 10);
// 最快策略只是少采样(第 8 个参数),候选数量不变,顺序仍是从预计体积最小到最大。
const fastPicked = await renderer.pickAnimatedCandidates(searchPath, frames, size, 1, 20 * 1024, 10, fakeEncode, 4);
assert.equal(fastPicked.length, 10, '最快策略只是少采样,候选数量不变');
for (const profile of picked) {
// 这条搜索路径是按 200 的画布展开的,细节分辨率必须落在 [1, 200]。
assert.ok(profile.size >= 1 && profile.size <= 200, '细节分辨率要落在画布范围内');
assert.ok(profile.fps >= 1);
assert.ok(profile.quality >= 0 && profile.quality <= 1);
}
// —— WebP 封装:两帧以上才合法,结构必须是 RIFF/WEBP + ANMF ——
const still = (() => {
const chunk = (type, payload) => {
const output = Buffer.alloc(8 + payload.length + (payload.length & 1));
output.write(type, 0, 4, 'ascii');
output.writeUInt32LE(payload.length, 4);
payload.copy(output, 8);
return output;
};
const body = Buffer.concat([Buffer.from('WEBP'), chunk('VP8 ', Buffer.from([1, 2, 3, 4]))]);
const output = Buffer.alloc(8 + body.length);
output.write('RIFF', 0, 4, 'ascii');
output.writeUInt32LE(body.length, 4);
body.copy(output, 8);
return output;
})();
const animated = encoder.encode({
width: 200,
height: 200,
frames: [{ webp: still, duration: 55 }, { webp: still, duration: 55 }],
});
assert.equal(animated.toString('ascii', 0, 4), 'RIFF');
assert.equal(animated.toString('ascii', 8, 12), 'WEBP');
assert.equal(animated.readUInt32LE(4), animated.length - 8);
assert.throws(() => encoder.encode({ width: 200, height: 200, frames: [{ webp: still, duration: 55 }] }), /at least two frames/);
console.log('PASS: 动态缩略图档位、目标体积、帧数换算、可见范围、自动定档、WebP 封装');
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});