解决粒子首位帧衔接问题
This commit is contained in:
1 parent
0656684a92
commit
9c2421110c
22 files changed
+2873
-1407
No files matched your search
+34
-19
@@ -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
|
||||
|
||||
Reference in new issue
Block a user