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 范围与开始/循环/结束三段边界校验通过')