解决粒子首位帧衔接问题

This commit is contained in:
tianmo committed 2026-09-02 22:55:38 +08:00
1 parent 0656684a92
commit 9c2421110c
22 files changed
+2873 -1407

No files matched your search

+34 -19
View File
@@ -4,6 +4,8 @@ import type { SpineSceneObject } from '../spine/spineTypes'
import { releaseSpineAsset, retainSpineAsset } from '../spine/spineAssetRegistry'
import { cloneEditorValue } from '../editor/editorClone'
import { defaultSpineExportSettings, type SpineExportSettings } from '../export/spineExportSettings'
import { loopSegmentTiming } from '../core/loopSegmentTiming'
import { normalizePlaybackRange, resizePlaybackRange } from '../timeline/playbackRange'
export interface ParticleSystem {
id: number
@@ -95,6 +97,9 @@ export interface TimelineState {
frame: number
/** 总帧数 */
totalFrames: number
/** 自定义播放范围,首尾帧均包含。 */
playbackStart: number
playbackEnd: number
/** 帧率 */
fps: number
/** 是否已录制 */
@@ -130,7 +135,7 @@ export const useParticleStore = defineStore('particle', {
activeObjectType: 'particle' as SceneObjectType,
activeObjectId: 0,
// 与 Spine 编辑器默认时间基准对齐:1 秒 = 30 帧。
timeline: { playing: true, loop: true, frame: 0, totalFrames: 60, fps: 30, recorded: false, animation: 'animation', animations: ['animation'] } as TimelineState,
timeline: { playing: true, loop: true, frame: 0, totalFrames: 60, playbackStart: 0, playbackEnd: 59, fps: 30, recorded: false, animation: 'animation', animations: ['animation'] } as TimelineState,
settings: {
parameterPanelOnRight: true,
tickEnabled: false,
@@ -439,8 +444,8 @@ export const useParticleStore = defineStore('particle', {
this.recalcTotalFrames()
},
play() {
// 非循环播放停在末帧后,再次播放应从头开始;中途暂停则继续当前位置。
if (this.timeline.frame >= Math.max(0, this.timeline.totalFrames - 1)) this.timeline.frame = 0
const range = normalizePlaybackRange(this.timeline.playbackStart, this.timeline.playbackEnd, this.timeline.totalFrames)
if (this.timeline.frame < range.start || this.timeline.frame >= range.end) this.timeline.frame = range.start
this.timeline.playing = true
},
pause() { this.timeline.playing = false },
@@ -448,23 +453,35 @@ export const useParticleStore = defineStore('particle', {
if (this.timeline.playing) this.pause()
else this.play()
},
toStart() { this.timeline.frame = 0 },
toStart() { this.timeline.frame = this.timeline.playbackStart },
previousFrame() {
this.timeline.playing = false
this.timeline.frame = Math.max(0, this.timeline.frame - 1)
this.timeline.frame = Math.max(this.timeline.playbackStart, this.timeline.frame - 1)
},
nextFrame() {
this.timeline.playing = false
this.timeline.frame = Math.min(Math.max(0, this.timeline.totalFrames - 1), this.timeline.frame + 1)
this.timeline.frame = Math.min(this.timeline.playbackEnd, this.timeline.frame + 1)
},
toEnd() {
this.timeline.playing = false
this.timeline.frame = Math.max(0, this.timeline.totalFrames - 1)
this.timeline.frame = this.timeline.playbackEnd
},
toggleLoop() { this.timeline.loop = !this.timeline.loop },
setFrame(f: number) {
this.timeline.frame = Math.max(0, Math.min(f, this.timeline.totalFrames - 1))
},
setPlaybackStart(value: number) {
const range = normalizePlaybackRange(value, this.timeline.playbackEnd, this.timeline.totalFrames)
this.timeline.playbackStart = range.start
this.timeline.playbackEnd = range.end
if (this.timeline.frame < range.start) this.timeline.frame = range.start
},
setPlaybackEnd(value: number) {
const range = normalizePlaybackRange(this.timeline.playbackStart, value, this.timeline.totalFrames)
this.timeline.playbackStart = range.start
this.timeline.playbackEnd = range.end
if (this.timeline.frame > range.end) this.timeline.frame = range.end
},
/** 依据所有粒子系统的"最晚消失时间"重算总帧数。
* 固定生命周期使用 lifeMin;随机生命周期使用上下限中的较大值。
* 单系统总时长 = delay + 持续模式 duration(爆发为 0)+ 最大粒子生命,
@@ -486,17 +503,7 @@ export const useParticleStore = defineStore('particle', {
: 0
let end = Math.max(0, Number(c.delay) || 0) + emissionDuration + particleLife
if (c.mode === 'stream' && c.streamBehavior === 'loop') {
if (c.generateLoopEndAnimation) {
// 结束段从循环衔接帧开始,立即停止生成;额外保留一帧用于明确记录“全部死亡”。
end = Math.max(0, Number(c.delay) || 0) + particleLife + 1 / fps
} else if (c.generateLoopStartAnimation) {
// 启动段从空状态自然预热到稳定循环衔接帧;寿命跨越多个周期时自动增加预热周期。
const warmupCycles = Math.max(1, Math.ceil(particleLife / emissionDuration))
end = Math.max(0, Number(c.delay) || 0) + emissionDuration * warmupCycles
} else {
// 纯循环段延迟期间保持衔接帧,之后播放一个完整周期,不追加常规生命周期尾段。
end = Math.max(0, Number(c.delay) || 0) + emissionDuration
}
end = Math.max(0, Number(c.delay) || 0) + loopSegmentTiming(c, fps).total / fps
}
if (end > maxSec) maxSec = end
}
@@ -512,8 +519,16 @@ export const useParticleStore = defineStore('particle', {
if (end > maxSec) maxSec = end
}
// totalFrames 表示快照数量;0 秒和末端点都可选,因此比末端帧编号多 1。
const frames = Math.max(1, Math.ceil(maxSec * fps) + 1)
const previousFrames = this.timeline.totalFrames
const frames = Math.max(2, Math.ceil(maxSec * fps) + 1)
this.timeline.totalFrames = frames
const range = resizePlaybackRange(
{ start: this.timeline.playbackStart, end: this.timeline.playbackEnd },
previousFrames,
frames,
)
this.timeline.playbackStart = range.start
this.timeline.playbackEnd = range.end
// clamp 当前帧
if (this.timeline.frame >= frames) this.timeline.frame = frames - 1
return frames