默认序列帧修改

This commit is contained in:
tianmo committed 2026-10-30 19:27:27 +08:00
1 parent 5efee6d918
commit a11d57f27f
69 files changed
+164 -22

No files matched your search

+5 -3
View File
@@ -1,6 +1,7 @@
import { Texture } from 'pixi.js'
import type { EmitMode, EmitShape, Blend, AttributeMode, RotationMode, ScaleAxisMode, CurvePoint, ColorGradientStop, ColliderShape, CollisionResponse, SceneCollider, ParticleImageResource, ParticleSequenceFrame, TrailImageResource, EmitterConfig, ParticleState, TrailBoneState, TrailState } from './particleTypes'
import { BLEND_MAP } from './particleConstants'
import { DEFAULT_SEQUENCE_FPS, ensureDefaultSequenceFrames } from './particleResources'
export function defaultConfig(): EmitterConfig {
return {
@@ -13,7 +14,7 @@ export function defaultConfig(): EmitterConfig {
previewUrl: '/images/star.png',
imageMode: 'fixed',
sequencePlayback: 'loop-forward',
sequenceFps: 30,
sequenceFps: DEFAULT_SEQUENCE_FPS,
sequenceFrames: [],
weight: 100,
locked: false,
@@ -380,14 +381,15 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
else if (!['once-hide', 'once-hold', 'loop-forward', 'loop-reverse'].includes(resource.sequencePlayback)) {
resource.sequencePlayback = 'loop-forward'
}
if (!Number.isFinite(resource.sequenceFps)) resource.sequenceFps = 30
resource.sequenceFps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || 30))
if (!Number.isFinite(resource.sequenceFps)) resource.sequenceFps = DEFAULT_SEQUENCE_FPS
resource.sequenceFps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || DEFAULT_SEQUENCE_FPS))
if (!Array.isArray(resource.sequenceFrames)) resource.sequenceFrames = []
for (const frame of resource.sequenceFrames) {
if (typeof frame.name !== 'string') frame.name = ''
if (typeof frame.previewUrl !== 'string') frame.previewUrl = ''
if (frame.texture === undefined) frame.texture = null
}
ensureDefaultSequenceFrames(resource)
if (!Number.isFinite(resource.weight)) resource.weight = 100
if (typeof resource.locked !== 'boolean') resource.locked = false
if (!Number.isFinite(resource.scale)) resource.scale = 1
+35 -2
View File
@@ -1,11 +1,44 @@
import type { Texture } from 'pixi.js'
import type { EmitterConfig, ParticleImageResource, TrailImageResource } from './particleTypes'
export const DEFAULT_SEQUENCE_FRAME_DIRECTORY = '/images/frame-sequence/yanhua'
export const DEFAULT_SEQUENCE_FPS = 15
export const DEFAULT_SEQUENCE_FRAME_NAMES = Array.from(
{ length: 15 },
(_, index) => `yanhua_${String(index + 1).padStart(2, '0')}.png`,
)
type SequenceResourceTarget = {
imageMode: ParticleImageResource['imageMode']
sequenceFrames: Array<Pick<ParticleImageResource['sequenceFrames'][number], 'name' | 'previewUrl'>>
textureName: string
textureFolder: string
previewUrl: string
}
export function applyDefaultSequenceFrames(resource: SequenceResourceTarget) {
resource.sequenceFrames = DEFAULT_SEQUENCE_FRAME_NAMES.map((name) => ({
name,
previewUrl: `${DEFAULT_SEQUENCE_FRAME_DIRECTORY}/${name}`,
texture: null,
}))
resource.textureName = 'yanhua'
resource.textureFolder = ''
resource.previewUrl = resource.sequenceFrames[0].previewUrl
}
/** 序列帧模式未指定图片时使用随项目发布的默认序列。 */
export function ensureDefaultSequenceFrames(resource: SequenceResourceTarget) {
if (resource.imageMode !== 'sequence' || resource.sequenceFrames.length) return false
applyDefaultSequenceFrames(resource)
return true
}
/** 粒子图片资源的出厂默认值(无可用资源时的兜底)。 */
export function defaultImageResource(texture: Texture<any> | null): ParticleImageResource {
return {
id: 1, texture, textureName: 'star', textureFolder: '',
previewUrl: '/images/star.png', imageMode: 'fixed', sequencePlayback: 'loop-forward', sequenceFps: 30, sequenceFrames: [],
previewUrl: '/images/star.png', imageMode: 'fixed', sequencePlayback: 'loop-forward', sequenceFps: DEFAULT_SEQUENCE_FPS, sequenceFrames: [],
weight: 100, locked: false, scale: 1, anchorX: 0.5, anchorY: 0.5,
colorMode: 'fixed', colorStart: '#ffffff', colorEnd: '#ff4d4d', blend: 'normal',
colorGradient: [{ position: 0, color: '#ffffff' }, { position: 1, color: '#ff4d4d' }],
@@ -64,7 +97,7 @@ export function pickTrailResource(cfg: EmitterConfig, sample: number): TrailImag
/** 序列帧在粒子年龄处的帧号与可见性。 */
export function imageFrameState(resource: ParticleImageResource, age: number) {
if (resource.imageMode !== 'sequence' || resource.sequenceFrames.length <= 1) return { index: 0, visible: true }
const fps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || 30))
const fps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || DEFAULT_SEQUENCE_FPS))
const rawIndex = Math.max(0, Math.floor(Math.max(0, age) * fps))
const lastIndex = resource.sequenceFrames.length - 1
if (resource.sequencePlayback === 'once-hide') {
+7 -1
View File
@@ -16,7 +16,7 @@
</div>
<div class="thumb-note">{{ displayFileName }}</div>
<label v-if="supportsSequence" class="row"><span>{{ $t('image.imageMode') }}</span>
<select v-model="resource.imageMode" class="inp">
<select :value="resource.imageMode || 'fixed'" class="inp" @change="changeImageMode">
<option value="fixed">{{ $t('image.fixed') }}</option>
<option value="sequence">{{ $t('image.sequence') }}</option>
</select>
@@ -151,6 +151,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
pick: [id: number]
pickSequence: [id: number]
modeChange: [id: number, mode: 'fixed' | 'sequence']
reset: [id: number]
remove: [id: number]
updateWeight: [id: number, value: number]
@@ -258,6 +259,11 @@ function pickImage() {
if (props.supportsSequence && props.resource.imageMode === 'sequence') emit('pickSequence', props.resource.id)
else emit('pick', props.resource.id)
}
function changeImageMode(event: Event) {
const mode = (event.target as HTMLSelectElement).value as 'fixed' | 'sequence'
emit('modeChange', props.resource.id, mode)
}
</script>
<style scoped>
@@ -95,6 +95,7 @@
default-file="star.png"
@pick="openTexturePicker"
@pick-sequence="openSequencePicker"
@mode-change="setImageMode"
@reset="resetTexture"
@remove="removeImageResource"
@update-weight="setResourceWeight"
@@ -158,7 +159,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, sectionOpen, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, sectionOpen, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
</script>
<style scoped>
+2 -2
View File
@@ -57,7 +57,7 @@ const activeIdx = computed(() => {
return i >= 0 ? i : 0
})
const sys = computed(() => store.systems[activeIdx.value] || null)
const { textureFileInput, sequenceFileInput, pendingResourceId, trailTextureFileInput, pendingTrailResourceId, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, rebalanceTrailWeights, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, readFileAsDataUrl, resetTexture, normalizeHex, setResourceFixedColor, commitResourceFixedColor, removeImageResource, roundWeight, distributeImageWeights, rebalanceImageWeights, setResourceWeight, toggleResourceLock } = useParticleResources(sys)
const { textureFileInput, sequenceFileInput, pendingResourceId, trailTextureFileInput, pendingTrailResourceId, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, rebalanceTrailWeights, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, readFileAsDataUrl, resetTexture, normalizeHex, setResourceFixedColor, commitResourceFixedColor, removeImageResource, roundWeight, distributeImageWeights, rebalanceImageWeights, setResourceWeight, toggleResourceLock } = useParticleResources(sys)
const automaticLoopDuration = computed(() => sys.value
? automaticLoopTransitionFrames(sys.value.config, store.timeline.fps)
: 30)
@@ -657,5 +657,5 @@ function onSpawnPathChange() {
// 重置/删除图片 → 回到默认 star.png
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
}
@@ -4,6 +4,7 @@ import { Texture } from 'pixi.js'
import { useParticleStore } from '../../store/particleStore'
import type { ParticleSystem } from '../../store/particleStore'
import { defaultConfig, ensureEmitterConfig } from '../../core/particleEmitter'
import { applyDefaultSequenceFrames, DEFAULT_SEQUENCE_FPS } from '../../core/particleResources'
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
@@ -227,7 +228,7 @@ function addImageResource() {
previewUrl: '/images/star.png',
imageMode: 'fixed',
sequencePlayback: 'loop-forward',
sequenceFps: 30,
sequenceFps: DEFAULT_SEQUENCE_FPS,
sequenceFrames: [],
weight: 100,
locked: false,
@@ -292,6 +293,27 @@ async function onSequenceFiles(event: Event) {
pendingResourceId.value = null
}
async function setImageMode(resourceId: number, mode: 'fixed' | 'sequence') {
const resource = sys.value?.config.imageResources.find((item) => item.id === resourceId)
if (!resource) return
if (mode === 'fixed') {
resource.imageMode = mode
return
}
if (!resource.sequenceFrames.length) applyDefaultSequenceFrames(resource)
try {
await Promise.all(resource.sequenceFrames.map(async (frame) => {
if (!frame.texture) frame.texture = markRaw(await Texture.fromURL(frame.previewUrl)) as any
}))
} catch (error) {
console.error('[sequence-default]', error)
return
}
resource.texture = resource.sequenceFrames[0]?.texture || resource.texture
resource.previewUrl = resource.sequenceFrames[0]?.previewUrl || resource.previewUrl
resource.imageMode = mode
}
function readFileAsDataUrl(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
@@ -311,7 +333,7 @@ function resetTexture(resourceId: number) {
resource.previewUrl = '/images/star.png'
resource.imageMode = 'fixed'
resource.sequencePlayback = 'loop-forward'
resource.sequenceFps = 30
resource.sequenceFps = DEFAULT_SEQUENCE_FPS
resource.sequenceFrames = []
resource.scale = 1
resource.anchorX = 0.5
@@ -428,6 +450,6 @@ function toggleResourceLock(resourceId: number) {
}
return { textureFileInput, sequenceFileInput, pendingResourceId, trailTextureFileInput, pendingTrailResourceId, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, rebalanceTrailWeights, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, readFileAsDataUrl, resetTexture, normalizeHex, setResourceFixedColor, commitResourceFixedColor, removeImageResource, roundWeight, distributeImageWeights, rebalanceImageWeights, setResourceWeight, toggleResourceLock }
return { textureFileInput, sequenceFileInput, pendingResourceId, trailTextureFileInput, pendingTrailResourceId, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, rebalanceTrailWeights, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, readFileAsDataUrl, resetTexture, normalizeHex, setResourceFixedColor, commitResourceFixedColor, removeImageResource, roundWeight, distributeImageWeights, rebalanceImageWeights, setResourceWeight, toggleResourceLock }
}
import { LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } from './particlePanelCurves'