功能修复
This commit is contained in:
1 parent
fed1015849
commit
bae179eb93
13 files changed
+274
-81
No files matched your search
+78
-42
@@ -3,7 +3,7 @@ import { pathDistributionProgress, sampleEmitterShape, sampleEmissionVelocityAng
|
||||
import { TrailController, createTrailState, resetTrailState, type ParticleTrailState } from './particleTrail'
|
||||
import { pickImageResource, pickTrailResource, imageFrameState, imageFrameTexture } from './particleResources'
|
||||
|
||||
import type { SceneCollider, EmitterConfig, ParticleState, ParticleInheritAttribute } from './particleTypes'
|
||||
import type { SceneCollider, EmitterConfig, EmitMode, ParticleState, ParticleInheritAttribute } from './particleTypes'
|
||||
import { defaultConfig, ensureEmitterConfig } from './particleConfig'
|
||||
import { BLEND_MAP, MAX_PARTICLES } from './particleConstants'
|
||||
import { lerp, finite, localToWorldMath, worldMathToLocal, localVelocityToWorldMath, worldVelocityToLocalMath, resolveColliderPoint, tFromLife, seededUnit, initialValue, overLifeValue, curveAt, mulberry32, hexToNumber, gradientColorAt, fieldFalloff, vortexAcceleration, addRgbColors } from './particleMath'
|
||||
@@ -90,6 +90,19 @@ export interface ParticleInheritOptions {
|
||||
data: ParticleInheritData
|
||||
}
|
||||
|
||||
interface EventEmission {
|
||||
mode: EmitMode
|
||||
worldX: number
|
||||
worldY: number
|
||||
depth: number
|
||||
inherit?: ParticleInheritOptions
|
||||
count: number
|
||||
rate: number
|
||||
remainingDelay: number
|
||||
remainingDuration: number
|
||||
accumulator: number
|
||||
}
|
||||
|
||||
export class ParticleEmitter extends Container {
|
||||
private pool: Particle[] = []
|
||||
private cfg: EmitterConfig
|
||||
@@ -103,15 +116,8 @@ export class ParticleEmitter extends Container {
|
||||
private _attractionWasEnabled = false
|
||||
/** 子发射器关闭自动发射,只接收父粒子事件生成的粒子。 */
|
||||
private autoEmission = true
|
||||
/** 事件粒子延迟队列:子发射器 delay 表示在父事件之后再延迟多久发射。 */
|
||||
private pendingEventSpawns: Array<{
|
||||
worldX: number
|
||||
worldY: number
|
||||
depth: number
|
||||
inherit?: ParticleInheritOptions
|
||||
count: number
|
||||
remaining: number
|
||||
}> = []
|
||||
/** 父事件启动的独立子发射窗口;持续模式会按 rate/duration 运行。 */
|
||||
private eventEmissions: EventEmission[] = []
|
||||
/** 活动粒子状态快照(供骨架 / 导出) */
|
||||
states: ParticleState[] = []
|
||||
/** 粒子出生/死亡/碰撞事件回调(由 EmitterSync 接入跨系统事件)。 */
|
||||
@@ -287,23 +293,8 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子发射器的延迟发射队列;新粒子在本帧就会参与后续更新与录制。
|
||||
if (this.pendingEventSpawns.length && dt > 0) {
|
||||
const ready: typeof this.pendingEventSpawns = []
|
||||
const waiting: typeof this.pendingEventSpawns = []
|
||||
for (const pending of this.pendingEventSpawns) {
|
||||
pending.remaining -= dt
|
||||
if (pending.remaining <= 0) ready.push(pending)
|
||||
else waiting.push(pending)
|
||||
}
|
||||
this.pendingEventSpawns = waiting
|
||||
for (const pending of ready) {
|
||||
const local = worldMathToLocal(this.cfg, pending.worldX, pending.worldY, this.simulationTransform)
|
||||
for (let index = 0; index < pending.count; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, pending.depth, pending.inherit)) break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新粒子在本帧就会参与后续更新与录制。
|
||||
this.updateEventEmissions(dt)
|
||||
|
||||
const states: ParticleState[] = []
|
||||
for (let i = 0; i < this.pool.length; i++) {
|
||||
@@ -557,27 +548,72 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
/** 由跨系统事件触发:在给定的数学世界坐标处生成一颗目标系统的粒子。 */
|
||||
/** 由跨系统事件触发:爆发模式生成一次,持续模式启动一个独立发射窗口。 */
|
||||
spawnEventParticle(worldX: number, worldY: number, depth = 1, inherit?: ParticleInheritOptions, count?: number): boolean {
|
||||
if (depth > 2) return false
|
||||
// 子发射器按自己的发射模式决定每次事件生成多少颗:爆发=单次数量,持续=1。
|
||||
const requested = count ?? (this.cfg.mode === 'burst' ? this.cfg.burstCount : 1)
|
||||
const total = Math.max(1, Math.min(MAX_PARTICLES, Math.round(Number(requested) || 1)))
|
||||
// 子发射器 delay 表示在父事件之后再延迟多久发射。
|
||||
const delay = Math.max(0, Number(this.cfg.delay) || 0)
|
||||
if (delay > 0) {
|
||||
this.pendingEventSpawns.push({ worldX, worldY, depth, inherit, count: total, remaining: delay })
|
||||
return true
|
||||
const duration = this.cfg.streamBehavior === 'loop'
|
||||
? Math.max(1 / 30, Number(this.cfg.loopDurationFrames) / 30 || 1 / 30)
|
||||
: Math.max(1 / 30, Number(this.cfg.duration) || 0)
|
||||
const emission: EventEmission = {
|
||||
mode: this.cfg.mode,
|
||||
worldX,
|
||||
worldY,
|
||||
depth,
|
||||
inherit: inherit
|
||||
? { ...inherit, attributes: [...inherit.attributes], data: { ...inherit.data } }
|
||||
: undefined,
|
||||
count: Math.max(1, Math.min(MAX_PARTICLES, Math.round(Number(count ?? this.cfg.burstCount) || 1))),
|
||||
rate: Math.max(0, Number(this.cfg.rate) || 0),
|
||||
remainingDelay: Math.max(0, Number(this.cfg.delay) || 0),
|
||||
remainingDuration: duration,
|
||||
accumulator: 0,
|
||||
}
|
||||
const local = worldMathToLocal(this.cfg, worldX, worldY, this.simulationTransform)
|
||||
if (emission.mode === 'burst' && emission.remainingDelay <= 0) {
|
||||
return this.spawnEventBurst(emission)
|
||||
}
|
||||
this.eventEmissions.push(emission)
|
||||
return true
|
||||
}
|
||||
|
||||
private spawnEventBurst(emission: EventEmission) {
|
||||
const local = worldMathToLocal(this.cfg, emission.worldX, emission.worldY, this.simulationTransform)
|
||||
let spawned = false
|
||||
for (let index = 0; index < total; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, depth, inherit)) break
|
||||
for (let index = 0; index < emission.count; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, emission.depth, emission.inherit)) break
|
||||
spawned = true
|
||||
}
|
||||
return spawned
|
||||
}
|
||||
|
||||
private updateEventEmissions(dt: number) {
|
||||
if (!this.eventEmissions.length || dt <= 0) return
|
||||
const waiting: EventEmission[] = []
|
||||
for (const emission of this.eventEmissions) {
|
||||
if (emission.remainingDelay > 0) {
|
||||
if (emission.remainingDelay > dt) {
|
||||
emission.remainingDelay -= dt
|
||||
waiting.push(emission)
|
||||
continue
|
||||
}
|
||||
emission.remainingDelay = 0
|
||||
}
|
||||
if (emission.mode === 'burst') {
|
||||
this.spawnEventBurst(emission)
|
||||
continue
|
||||
}
|
||||
const activeTime = Math.min(dt, emission.remainingDuration)
|
||||
emission.remainingDuration -= activeTime
|
||||
emission.accumulator += activeTime * emission.rate
|
||||
const local = worldMathToLocal(this.cfg, emission.worldX, emission.worldY, this.simulationTransform)
|
||||
while (emission.accumulator >= 1) {
|
||||
emission.accumulator -= 1
|
||||
this.spawnOne({ x: local.x, y: local.y }, emission.depth, emission.inherit)
|
||||
}
|
||||
if (emission.remainingDuration > 1e-8) waiting.push(emission)
|
||||
}
|
||||
this.eventEmissions = waiting
|
||||
}
|
||||
|
||||
private emitWorldEvent(
|
||||
kind: ParticleEventKind,
|
||||
particle: Particle,
|
||||
@@ -738,7 +774,7 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
for (const p of this.pool) {
|
||||
p.active = false
|
||||
p.sprite.visible = false
|
||||
@@ -830,7 +866,7 @@ export class ParticleEmitter extends Container {
|
||||
|
||||
/** 清空所有粒子并重置发射计时 */
|
||||
reset() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
for (const p of this.pool) {
|
||||
p.active = false
|
||||
p.sprite.visible = false
|
||||
@@ -852,7 +888,7 @@ export class ParticleEmitter extends Container {
|
||||
* 发射计时与随机序列回到周期起点,使每一周期产生完全相同的发射事件。
|
||||
*/
|
||||
restartEmissionCycle() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
this.accumulator = 0
|
||||
this.burstTimer = 0
|
||||
this._spawnSerial = 0
|
||||
|
||||
@@ -34,7 +34,7 @@ const numberOrZero = (value: unknown) => Math.max(0, Number(value) || 0)
|
||||
|
||||
/**
|
||||
* 计算子发射器在时间轴上的实际活动区间:
|
||||
* 起点取第一次可能被触发的时刻,终点取最后一次触发 + 子粒子最大生命。
|
||||
* 起点取第一次可能被触发的时刻,终点取最后一次触发 + 子发射持续时长 + 子粒子最大生命。
|
||||
* 触发时机:出生=父粒子出生时;死亡=最早/最晚死亡时;碰撞=出生到死亡之间。
|
||||
*/
|
||||
export function subEmitterTriggerTimings(
|
||||
@@ -66,13 +66,15 @@ export function subEmitterTriggerTimings(
|
||||
|
||||
const emissionStart = (config: SubEmitterTimingSource['config']) => numberOrZero(config.delay)
|
||||
const delayOf = (config: SubEmitterTimingSource['config']) => numberOrZero(config.delay)
|
||||
const emissionDuration = (config: SubEmitterTimingSource['config']) => {
|
||||
if (config.mode !== 'stream') return 0
|
||||
return config.streamBehavior === 'loop'
|
||||
? loopSegmentTiming(config as any, safeFps).total / safeFps
|
||||
: Math.max(1 / safeFps, Number(config.duration) || 0)
|
||||
}
|
||||
const emissionEnd = (config: SubEmitterTimingSource['config']) => {
|
||||
const delay = emissionStart(config)
|
||||
if (config.mode !== 'stream') return delay
|
||||
if (config.streamBehavior === 'loop') {
|
||||
return delay + loopSegmentTiming(config as any, safeFps).total / safeFps
|
||||
}
|
||||
return delay + Math.max(1 / safeFps, Number(config.duration) || 0)
|
||||
return delay + emissionDuration(config)
|
||||
}
|
||||
const lifeMinOf = (config: SubEmitterTimingSource['config']) => {
|
||||
const min = numberOrZero(config.lifeMin)
|
||||
@@ -135,7 +137,9 @@ export function subEmitterTriggerTimings(
|
||||
result.set(id, {
|
||||
firstTrigger: first,
|
||||
lastTrigger: last,
|
||||
end: Math.max(first, last) + lifeMaxOf(system.config),
|
||||
end: Math.max(first, last) + numberOrZero(system.config.delay)
|
||||
+ emissionDuration(system.config)
|
||||
+ lifeMaxOf(system.config),
|
||||
})
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -10,7 +10,7 @@ import { collectSubEmitterIds } from '../core/subEmitterIds'
|
||||
import { translate } from '../i18n'
|
||||
|
||||
type Track = { key: string; name: string; states: Array<ParticleState | null> }
|
||||
type ExportInput = { systems: ParticleSystem[]; timeline: TimelineState; settings: SpineExportSettings }
|
||||
type ExportInput = { systems: ParticleSystem[]; timeline: TimelineState; settings: SpineExportSettings; playbackEnd?: number }
|
||||
|
||||
const BLENDS = { normal: 'normal', add: 'additive', multiply: 'multiply', screen: 'screen' } as const
|
||||
const PARTICLE_POSITION_TOLERANCE = 0.1
|
||||
@@ -343,9 +343,12 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
|
||||
for (const system of visibleSystems) {
|
||||
const frameLength = system.frames?.length || 0
|
||||
const playbackFrameCount = Number.isFinite(input.playbackEnd)
|
||||
? Math.min(frameLength, Math.max(1, Math.floor(Number(input.playbackEnd)) + 1))
|
||||
: frameLength
|
||||
const sourceCount = subEmitterIds.has(system.id)
|
||||
? frameLength
|
||||
: Math.min(frameLength, particleDurationFrames(system, sourceFps))
|
||||
? playbackFrameCount
|
||||
: Math.min(playbackFrameCount, particleDurationFrames(system, sourceFps))
|
||||
const frames = (system.frames || []).slice(0, sourceCount)
|
||||
if (!frames.length) warnings.push({ code: 'empty-cache', message: translate('errors.emptyCacheWarning', { name: system.config.name }) })
|
||||
const prefix = uniqueName(safeName(system.config.name, `system_${system.id}`), usedNames)
|
||||
|
||||
+76
-11
@@ -6,6 +6,7 @@ import { cloneEditorValue } from '../editor/editorClone'
|
||||
import { defaultSpineExportSettings, type SpineExportSettings } from '../export/spineExportSettings'
|
||||
import { loopSegmentTiming } from '../core/loopSegmentTiming'
|
||||
import { collectSubEmitterIds } from '../core/subEmitterIds'
|
||||
import { subEmitterTriggerTimings } from '../core/subEmitterTiming'
|
||||
import { lastFrameOfDuration, resizePlaybackRange, resolvePlaybackRange } from '../timeline/playbackRange'
|
||||
|
||||
export interface ParticleSystem {
|
||||
@@ -117,6 +118,52 @@ export interface TimelineState {
|
||||
animations: string[]
|
||||
}
|
||||
|
||||
function timelineContentEndFrame(
|
||||
systems: readonly any[],
|
||||
spines: readonly any[],
|
||||
colliders: readonly any[],
|
||||
fps: number,
|
||||
) {
|
||||
const safeFps = Math.max(1, fps || 30)
|
||||
const subEmitterIds = collectSubEmitterIds(systems)
|
||||
const subEmitterTiming = subEmitterTriggerTimings(systems as any, safeFps)
|
||||
let endFrame = 1
|
||||
for (const system of systems) {
|
||||
const config = system.config
|
||||
if (subEmitterIds.has(system.id)) {
|
||||
const timing = subEmitterTiming.get(system.id)
|
||||
if (timing) endFrame = Math.max(endFrame, lastFrameOfDuration(timing.end, safeFps))
|
||||
continue
|
||||
}
|
||||
const delay = Math.max(0, Number(config.delay) || 0)
|
||||
if (config.mode === 'stream' && config.streamBehavior === 'loop') {
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(delay + loopSegmentTiming(config as any, safeFps).total / safeFps, safeFps))
|
||||
continue
|
||||
}
|
||||
const emission = config.mode === 'stream' ? Math.max(1 / safeFps, Number(config.duration) || 0) : 0
|
||||
const life = config.lifeMode === 'fixed'
|
||||
? Math.max(0, Number(config.lifeMin) || 0)
|
||||
: Math.max(0, Number(config.lifeMin) || 0, Number(config.lifeMax) || 0)
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(delay + emission + life, safeFps))
|
||||
}
|
||||
for (const spine of spines) {
|
||||
const animation = spine.animations.find((item: any) => item.name === spine.selectedAnimation)
|
||||
const duration = Math.max(0, Number(animation?.duration) || 0)
|
||||
if (duration > 0) {
|
||||
endFrame = Math.max(
|
||||
endFrame,
|
||||
lastFrameOfDuration(Math.max(0, Number(spine.timeOffset) || 0) + duration, safeFps),
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const collider of colliders) {
|
||||
if (collider.followPath && collider.followPathId) {
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(collider.followDuration, safeFps))
|
||||
}
|
||||
}
|
||||
return endFrame
|
||||
}
|
||||
|
||||
let seq = 1
|
||||
let colliderSeq = 1
|
||||
let pathSeq = 1
|
||||
@@ -170,12 +217,17 @@ export const useParticleStore = defineStore('particle', {
|
||||
}),
|
||||
getters: {
|
||||
activeSystemCount: (s) => s.systems.length,
|
||||
playbackRange: (s) => resolvePlaybackRange(
|
||||
s.timeline.abLoop,
|
||||
s.timeline.playbackStart,
|
||||
s.timeline.playbackEnd,
|
||||
s.timeline.totalFrames,
|
||||
),
|
||||
playbackRange: (s) => {
|
||||
const range = resolvePlaybackRange(
|
||||
s.timeline.abLoop,
|
||||
s.timeline.playbackStart,
|
||||
s.timeline.playbackEnd,
|
||||
s.timeline.totalFrames,
|
||||
)
|
||||
const contentEnd = timelineContentEndFrame(s.systems, s.spines, s.colliders, s.timeline.fps)
|
||||
const end = Math.max(1, Math.min(range.end, contentEnd))
|
||||
return { start: Math.min(range.start, end - 1), end }
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
syncSceneSequences() {
|
||||
@@ -511,7 +563,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
/** 依据所有粒子系统的"最晚消失时间"重算总帧数。
|
||||
* 固定生命周期使用 lifeMin;随机生命周期使用上下限中的较大值。
|
||||
* 单系统总时长 = delay + 持续模式 duration(爆发为 0)+ 最大粒子生命,
|
||||
* 并递归计入子发射器链(出生立即触发、死亡/碰撞在最晚生命结束时触发)。
|
||||
* 并递归计入子发射器的持续窗口与粒子链(出生立即触发、死亡/碰撞在最晚生命结束时触发)。
|
||||
* 多系统取最大;转帧 = ceil(sec * fps)。 */
|
||||
recalcTotalFrames() {
|
||||
const fps = Math.max(1, this.timeline.fps || 30)
|
||||
@@ -523,6 +575,13 @@ export const useParticleStore = defineStore('particle', {
|
||||
const byId = new Map(this.systems.map((system) => [system.id, system]))
|
||||
const tailCache = new Map<number, number>()
|
||||
const visiting = new Set<number>()
|
||||
const emissionDuration = (system: any) => {
|
||||
const config = system.config
|
||||
if (config.mode !== 'stream') return 0
|
||||
return config.streamBehavior === 'loop'
|
||||
? loopSegmentTiming(config, fps).total / fps
|
||||
: Math.max(1 / fps, Number(config.duration) || 0)
|
||||
}
|
||||
/** 从一颗粒子出生算起,它及其子发射器链最晚持续多久。 */
|
||||
const effectTail = (system: any): number => {
|
||||
const cached = tailCache.get(system.id)
|
||||
@@ -536,7 +595,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
for (const rule of config.events || []) {
|
||||
const target = byId.get(rule.targetSystemId)
|
||||
if (!target || target.id === system.id) continue
|
||||
const childTail = effectTail(target)
|
||||
const childTail = emissionDuration(target) + effectTail(target)
|
||||
// 出生时立即触发;死亡/碰撞最晚发生在父粒子生命结束时。
|
||||
const offset = rule.event === 'birth' ? 0 : life
|
||||
// 子发射器自身的 delay 表示父事件之后再延迟多久发射。
|
||||
@@ -559,10 +618,10 @@ export const useParticleStore = defineStore('particle', {
|
||||
const isLoop = c.mode === 'stream' && c.streamBehavior === 'loop'
|
||||
let end: number
|
||||
if (c.mode === 'stream') {
|
||||
const emissionDuration = isLoop
|
||||
const rootEmissionDuration = isLoop
|
||||
? loopSegmentTiming(c, fps).total / fps
|
||||
: Math.max(1 / fps, Number(c.duration) || 0)
|
||||
end = delay + emissionDuration
|
||||
end = delay + rootEmissionDuration
|
||||
if (isLoop) {
|
||||
// 循环结束段已包含父系统自身消亡,只追加子发射器链的尾部时长。
|
||||
let childTail = 0
|
||||
@@ -572,7 +631,13 @@ export const useParticleStore = defineStore('particle', {
|
||||
const target = byId.get(rule.targetSystemId)
|
||||
if (!target || target.id === system.id) continue
|
||||
const targetDelay = Math.max(0, Number(target.config.delay) || 0)
|
||||
childTail = Math.max(childTail, (rule.event === 'birth' ? 0 : life) + targetDelay + effectTail(target))
|
||||
childTail = Math.max(
|
||||
childTail,
|
||||
(rule.event === 'birth' ? 0 : life)
|
||||
+ targetDelay
|
||||
+ emissionDuration(target)
|
||||
+ effectTail(target),
|
||||
)
|
||||
}
|
||||
}
|
||||
end += childTail
|
||||
|
||||
@@ -238,6 +238,9 @@ const playbackEndDraft = ref(String(store.timeline.playbackEnd))
|
||||
|
||||
watch(() => store.timeline.playbackStart, (value) => { playbackStartDraft.value = String(value) })
|
||||
watch(() => store.timeline.playbackEnd, (value) => { playbackEndDraft.value = String(value) })
|
||||
watch(() => store.playbackRange.end, (end) => {
|
||||
if (store.timeline.recorded && store.timeline.frame > end) store.timeline.frame = end
|
||||
}, { immediate: true })
|
||||
|
||||
const totalFrames = computed(() => store.timeline.totalFrames)
|
||||
// 子发射器延长时间轴后自动扩大预览范围;用户手动放大后保持其缩放。
|
||||
@@ -335,7 +338,8 @@ function barWidthPx(sys: SysLike): number {
|
||||
const timing = subEmitterTiming.value.get(sys.id)
|
||||
const start = timing?.firstTrigger ?? 0
|
||||
const end = timing?.end ?? start
|
||||
return (Math.max(0, end - start) * fps.value) / pxPerF()
|
||||
const delay = draftDelays.value[sys.id] ?? Math.max(0, Number((sys.config as any).delay) || 0)
|
||||
return (Math.max(0, end - start - delay) * fps.value) / pxPerF()
|
||||
}
|
||||
return lifeWidthPx(sys)
|
||||
}
|
||||
@@ -398,7 +402,7 @@ function onWheel(e: WheelEvent) {
|
||||
viewFrames.value = Math.round(nv)
|
||||
}
|
||||
|
||||
// 拖动时间轴滑动(scrub):更新 frame(0..totalFrames-1)
|
||||
// 拖动时间轴滑动(scrub):只允许进入实际播放范围。
|
||||
function onScrubDown(e: PointerEvent) {
|
||||
const vp = vpRef.value!
|
||||
// 用户接管时间轴时立即暂停播放,否则每帧推进会和拖动抢播放头。
|
||||
@@ -406,7 +410,7 @@ function onScrubDown(e: PointerEvent) {
|
||||
const move = (ev: PointerEvent) => {
|
||||
const rect = vp.getBoundingClientRect()
|
||||
const f = Math.floor(((ev.clientX - rect.left) / rect.width) * viewFrames.value)
|
||||
store.timeline.frame = Math.max(0, Math.min(f, totalFrames.value - 1))
|
||||
store.timeline.frame = Math.max(0, Math.min(f, store.playbackRange.end))
|
||||
}
|
||||
move(e)
|
||||
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
|
||||
|
||||
@@ -137,7 +137,12 @@ async function exportSpineJson() {
|
||||
exportMessage.value = t('export.recalculating')
|
||||
try {
|
||||
await prepareExportFrames()
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
const result = buildSpineJson({
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
timeline: store.timeline,
|
||||
settings: exportSettings.value,
|
||||
playbackEnd: store.playbackRange.end,
|
||||
})
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
downloadSpineJson(result, exportSettings.value.fileName)
|
||||
const warning = result.warnings.length ? t('export.resourceWarning', { count: result.warnings.length }) : ''
|
||||
@@ -170,7 +175,12 @@ async function exportSpineProject() {
|
||||
await transfer.paint()
|
||||
await prepareExportFrames((progress) => transfer.update(progress * 18, t('export.recalculating')))
|
||||
transfer.update(20, t('export.generatingJson'))
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
const result = buildSpineJson({
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
timeline: store.timeline,
|
||||
settings: exportSettings.value,
|
||||
playbackEnd: store.playbackRange.end,
|
||||
})
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
exportMessage.value = t('export.preparingProject')
|
||||
const projectName = exportSettings.value.fileName.replace(/\.json$/i, '') || 'SpineParticle'
|
||||
|
||||
Reference in new issue
Block a user