Files
SpineParticlesWeb/scripts/verify-timeline.mjs
T
2026-09-05 16:20:23 +08:00

93 lines
3.9 KiB
JavaScript

import { build } from 'esbuild'
const bundled = await build({
bundle: true,
stdin: {
contents: [
"export { bakeLoopFrames, closeLoopFrameSeam } from './src/core/loopFrameBaker.ts'",
"export { loopPlaybackRange, loopSegmentTiming } from './src/core/loopSegmentTiming.ts'",
"export { normalizePlaybackRange, resizePlaybackRange } from './src/timeline/playbackRange.ts'",
].join('\n'),
resolveDir: process.cwd(),
sourcefile: 'timeline-test-entry.ts',
},
format: 'esm',
platform: 'node',
write: false,
})
const source = bundled.outputFiles[0].text
const api = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`)
const timing = api.loopSegmentTiming({
loopDurationFrames: 30,
generateLoopStartAnimation: true, loopStartUseCustomDuration: true,
loopStartDurationFrames: 20,
generateLoopEndAnimation: true, loopEndUseCustomDuration: true,
loopEndDurationFrames: 40,
})
if (JSON.stringify(timing) !== JSON.stringify({ start: 20, loop: 30, end: 40, total: 90 })) {
throw new Error('开始、循环、结束三段时长计算错误')
}
const automaticTiming = api.loopSegmentTiming({
lifeMode: 'random', lifeMin: 0.5, lifeMax: 1.5,
loopDurationFrames: 30,
generateLoopStartAnimation: true, loopStartUseCustomDuration: false, loopStartDurationFrames: 20,
generateLoopEndAnimation: true, loopEndUseCustomDuration: false, loopEndDurationFrames: 40,
}, 30)
if (automaticTiming.start !== 45 || automaticTiming.end !== 45) {
throw new Error('未指定时长时应按最大粒子生命周期自动计算')
}
const loopRange = api.loopPlaybackRange({
delay: 0, loopDurationFrames: 80,
generateLoopStartAnimation: true, loopStartUseCustomDuration: true, loopStartDurationFrames: 20,
generateLoopEndAnimation: true, loopEndUseCustomDuration: true, loopEndDurationFrames: 20,
}, 30)
if (loopRange.start !== 20 || loopRange.end !== 100) {
throw new Error('时间轴上的闭合自循环范围计算错误')
}
const normalized = api.normalizePlaybackRange(10, 10, 100)
if (normalized.start !== 10 || normalized.end !== 11) throw new Error('播放范围必须至少包含两个帧位置')
const resized = api.resizePlaybackRange({ start: 5, end: 59 }, 60, 90)
if (resized.start !== 5 || resized.end !== 89) throw new Error('全范围播放没有随总时长扩展')
function state(x) {
return [{
boneName: 'p_0', spawnId: 3, x, y: 0, rotation: 0,
scaleX: 1, scaleY: 1, alpha: 1, colorHex: 0xffffff,
resourceId: 1, imageFrameIndex: 0, imageVisible: true, active: true,
}]
}
const emitter = {
step: 0,
activeCount: 1,
reset() { this.step = 0; this.activeCount = 1 },
restartEmissionCycle() { this.step = 0 },
update(_dt, mode) { if (mode === 'off') this.activeCount = 0; else this.step++; return this.capture() },
capture() { return this.activeCount ? state(this.step) : [] },
apply() {},
}
const system = {
config: {
lifeMode: 'fixed', lifeMin: 1, lifeMax: 1, delay: 0,
loopDurationFrames: 3,
generateLoopStartAnimation: true, loopStartUseCustomDuration: true, loopStartDurationFrames: 2,
generateLoopEndAnimation: true, loopEndUseCustomDuration: true, loopEndDurationFrames: 4,
},
}
api.bakeLoopFrames(system, emitter, 3, 10)
const frames = system.frames
if (frames.length !== 10) throw new Error('组合动画总帧数错误')
if (JSON.stringify(frames[2]) !== JSON.stringify(frames[5])) throw new Error('启动末帧、循环首尾帧没有精确衔接')
if (frames[0].some((particle) => particle.imageVisible !== false || particle.alpha !== 0)) {
throw new Error('启动段首帧应把衔接帧粒子反推到未出生状态')
}
if (frames.slice(0, 2).some((frame) => frame.some((particle) => particle.x > frames[2][0].x))) {
throw new Error('启动段不应包含衔接帧之后的额外循环')
}
if (frames[9].length !== 0) throw new Error('结束段末帧应为空')
console.log('时间轴 A-B 范围与开始/循环/结束三段边界校验通过')