开发中途存档
This commit is contained in:
1 parent
a2de23a789
commit
a80575ef5e
19 files changed
+1998
-91
No files matched your search
@@ -4,6 +4,17 @@
|
||||
<span>粒子系统 · 编辑器</span>
|
||||
</div>
|
||||
|
||||
<div class="config-toolbar">
|
||||
<button title="保存全部编辑器配置 (Ctrl+S)" @click="saveConfig">保存配置</button>
|
||||
<button title="加载本地编辑器配置" @click="configFileInput?.click()">加载配置</button>
|
||||
<select v-model="selectedPreset" aria-label="预设列表" :disabled="!presetOptions.length || presetLoading" @pointerdown="refreshPresets" @focus="refreshPresets" @change="loadSelectedPreset">
|
||||
<option value="">{{ presetOptions.length ? '选择预设' : '无预设文件' }}</option>
|
||||
<option v-for="preset in presetOptions" :key="preset.file" :value="preset.file">{{ preset.name }}</option>
|
||||
</select>
|
||||
<input ref="configFileInput" hidden type="file" accept=".json,application/json" @change="onConfigFileChange" />
|
||||
</div>
|
||||
<div v-if="configMessage" class="config-message" :class="{ error: configError }">{{ configMessage }}</div>
|
||||
|
||||
<!-- 1. 场景对象:创建入口始终可用,即使当前没有粒子系统。 -->
|
||||
<div class="group">
|
||||
<div class="group-head scene-head" @click="toggle('scene')">
|
||||
@@ -768,7 +779,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, reactive, ref, watchEffect } from 'vue'
|
||||
import { computed, markRaw, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||
import { Texture } from 'pixi.js'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import { defaultConfig, ensureEmitterConfig } from '../core/particleEmitter'
|
||||
@@ -778,11 +789,76 @@ import ParticleAttributeControl from './ParticleAttributeControl.vue'
|
||||
import ColorGradientEditor from './ColorGradientEditor.vue'
|
||||
import ImageResourceCard from './ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../spine/SpineObjectPanel.vue'
|
||||
import { downloadEditorConfig, loadEditorConfigFile, loadEditorConfigText } from '../editor/editorConfig'
|
||||
|
||||
const store = useParticleStore()
|
||||
const configFileInput = ref<HTMLInputElement | null>(null)
|
||||
const selectedPreset = ref('')
|
||||
const presetOptions = ref<Array<{ name: string; file: string }>>([])
|
||||
const presetLoading = ref(false)
|
||||
const configMessage = ref('')
|
||||
const configError = ref(false)
|
||||
// 默认场景始终有一个可编辑的粒子系统;热更新保留现有系统时不重复创建。
|
||||
if (!store.systems.length) store.addSystem()
|
||||
|
||||
function saveConfig() {
|
||||
downloadEditorConfig(store)
|
||||
configError.value = false
|
||||
configMessage.value = '配置已保存到本地'
|
||||
}
|
||||
|
||||
async function onConfigFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
configMessage.value = '正在加载配置…'
|
||||
configError.value = false
|
||||
try {
|
||||
await loadEditorConfigFile(store, file)
|
||||
configMessage.value = `已加载:${file.name}`
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPresets() {
|
||||
try {
|
||||
const response = await fetch(`/Preset/index.json?t=${Date.now()}`, { cache: 'no-store' })
|
||||
if (!response.ok) throw new Error('无法读取预设列表')
|
||||
const presets = await response.json()
|
||||
presetOptions.value = Array.isArray(presets)
|
||||
? presets.filter((item): item is { name: string; file: string } => !!item && typeof item.name === 'string' && typeof item.file === 'string')
|
||||
: []
|
||||
} catch {
|
||||
presetOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSelectedPreset() {
|
||||
const file = selectedPreset.value
|
||||
if (!file) return
|
||||
presetLoading.value = true
|
||||
configError.value = false
|
||||
configMessage.value = '正在加载预设…'
|
||||
try {
|
||||
const response = await fetch(`/Preset/${encodeURIComponent(file)}`, { cache: 'no-store' })
|
||||
if (!response.ok) throw new Error(`无法读取预设:${file}`)
|
||||
await loadEditorConfigText(store, await response.text())
|
||||
configMessage.value = `已加载预设:${file.replace(/\.json$/i, '')}`
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
presetLoading.value = false
|
||||
selectedPreset.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refreshPresets)
|
||||
|
||||
const activeIdx = computed(() => {
|
||||
const i = store.systems.findIndex((s) => s.id === store.activeId)
|
||||
return i >= 0 ? i : 0
|
||||
@@ -1434,6 +1510,17 @@ function toggleResourceLock(resourceId: number) {
|
||||
.panel::-webkit-scrollbar-thumb:hover { background: #3a3a55; }
|
||||
.panel { scrollbar-width: thin; scrollbar-color: #2e2e44 transparent; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-weight: 700; color: #ccd; }
|
||||
.config-toolbar { display: grid; grid-template-columns: auto auto minmax(0,1fr); gap: 6px; margin-bottom: 10px; }
|
||||
.config-toolbar button,.config-toolbar select {
|
||||
min-width: 0; height: 30px; box-sizing: border-box; border: 1px solid #3b4864; border-radius: 6px;
|
||||
background: #263149; color: #d6def0; font-size: 11px;
|
||||
}
|
||||
.config-toolbar button { padding: 0 9px; cursor: pointer; }
|
||||
.config-toolbar button:hover { border-color: #7169dc; background: #343d68; color: #fff; }
|
||||
.config-toolbar select { padding: 0 7px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.config-toolbar select:disabled { color: #68738a; background: #1b2130; }
|
||||
.config-message { margin: -4px 0 9px; color: #83b9a2; font-size: 10px; }
|
||||
.config-message.error { color: #e8959e; }
|
||||
.empty, .panel-hint { color: #667; font-size: 12px; padding: 8px 0; }
|
||||
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
||||
.group-head {
|
||||
|
||||
+124
-57
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<div class="stage-wrap">
|
||||
<div ref="holder" class="stage-holder" :class="{ grid: showGrid }" @wheel.prevent="onWheel" @pointerdown="onPanDown" @dblclick.prevent="onStageDoubleClick"></div>
|
||||
<div ref="holder" class="stage-holder" :class="{ grid: store.settings.showGrid }" tabindex="0" @wheel.prevent="onWheel" @pointerdown="onPanDown" @dblclick.prevent="onStageDoubleClick"></div>
|
||||
|
||||
<!-- 顶栏 -->
|
||||
<div class="topbar">
|
||||
<button class="settings-btn" @click="toggleSettings" title="系统设置">系统设置</button>
|
||||
<button class="undo-btn" :disabled="editorHistoryState.undoCount === 0" @click="undoLastOperation" title="撤回上一步 (Ctrl+Z)">↶</button>
|
||||
|
||||
<!-- 根变换工具:拖动(默认) / 位移 / 旋转 / 缩放 -->
|
||||
<div class="tool-group">
|
||||
<button class="tool-btn" :class="{ on: activeTool === '' }" @click="setTool('')" title="拖动画布">✋</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'translate' }" @click="setTool('translate')" title="位移">✥</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'rotate' }" @click="setTool('rotate')" title="旋转">⟳</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'scale' }" @click="setTool('scale')" title="缩放">⤢</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === '' }" @click="toggleTool('')" title="拖动画布 (Q)">✋</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'translate' }" @click="toggleTool('translate')" title="位移 (W)">✥</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'rotate' }" @click="toggleTool('rotate')" title="旋转 (E)">⟳</button>
|
||||
<button class="tool-btn" :class="{ on: activeTool === 'scale' }" @click="toggleTool('scale')" title="缩放 (R)">⤢</button>
|
||||
</div>
|
||||
|
||||
<!-- 工具按钮组(参考画布工具栏) -->
|
||||
@@ -19,7 +20,7 @@
|
||||
<button class="tool-btn" @click="zoomIn" title="放大">+</button>
|
||||
<button class="tool-btn" @click="zoomOut" title="缩小">-</button>
|
||||
<button class="tool-btn" @click="fitView" title="适配画布">⤢</button>
|
||||
<button class="tool-btn" :class="{ on: showGrid }" @click="toggleGrid" title="切换网格">▦</button>
|
||||
<button class="tool-btn" :class="{ on: store.settings.showGrid }" @click="toggleGrid" title="切换网格">▦</button>
|
||||
<button class="tool-btn" @click="resetView" title="重置视图">⟳</button>
|
||||
</div>
|
||||
|
||||
@@ -44,13 +45,13 @@
|
||||
/>
|
||||
<div class="background-file-name" :title="backgroundFileName">{{ backgroundFileName || '未上传图像' }}</div>
|
||||
<div class="background-position-row">
|
||||
<label><span>X</span><input v-model.number="backgroundX" type="number" step="1" aria-label="背景图 X 偏移" /></label>
|
||||
<label><span>Y</span><input v-model.number="backgroundY" type="number" step="1" aria-label="背景图 Y 偏移" /></label>
|
||||
<label><span>X</span><input v-model.number="store.settings.backgroundX" type="number" step="1" aria-label="背景图 X 偏移" /></label>
|
||||
<label><span>Y</span><input v-model.number="store.settings.backgroundY" type="number" step="1" aria-label="背景图 Y 偏移" /></label>
|
||||
</div>
|
||||
<label class="background-opacity-row">
|
||||
<span>透明度</span>
|
||||
<input v-model.number="backgroundOpacity" type="range" min="0" max="1" step="0.01" aria-label="背景图透明度" />
|
||||
<b>{{ Math.round(backgroundOpacity * 100) }}%</b>
|
||||
<input v-model.number="store.settings.backgroundOpacity" type="range" min="0" max="1" step="0.01" aria-label="背景图透明度" />
|
||||
<b>{{ Math.round(store.settings.backgroundOpacity * 100) }}%</b>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,10 +63,23 @@
|
||||
<!-- 系统设置面板(左上角弹出) -->
|
||||
<div class="settings-panel" v-if="showSettings">
|
||||
<div class="sp-head">系统设置 <button class="sp-close" @click="showSettings = false">✕</button></div>
|
||||
<div class="sp-section-title">操作历史</div>
|
||||
<label class="sp-row"><span>最大撤回步数</span>
|
||||
<input
|
||||
class="sp-num sp-undo-limit"
|
||||
type="number"
|
||||
min="1"
|
||||
max="200"
|
||||
v-model.number="store.settings.undoLimit"
|
||||
aria-label="最大撤回步数"
|
||||
@change="onUndoLimitChange"
|
||||
/>
|
||||
</label>
|
||||
<div class="sp-divider"></div>
|
||||
<div class="sp-section-title">画布辅助显示</div>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showEmitter" />发射点</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showBones" />骨骼连线</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="showSkinMesh" />蒙皮网格</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showEmitter" />发射点</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showBones" />骨骼连线</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showSkinMesh" />蒙皮网格</label>
|
||||
<div class="sp-divider"></div>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.tickEnabled" />启用刻度</label>
|
||||
<template v-if="store.settings.tickEnabled">
|
||||
@@ -98,30 +112,49 @@ import { Application, Container, Graphics, Sprite, Text, Texture } from 'pixi.js
|
||||
import { ParticleEmitter, ensureEmitterConfig, type ParticleState } from '../core/particleEmitter'
|
||||
import { useParticleStore, type CollisionBody, type ScenePath } from '../store/particleStore'
|
||||
import { SpineRuntimeLayer } from '../spine/SpineRuntimeLayer'
|
||||
import { editorHistoryState, installEditorHistory, setUndoLimit, undoLastOperation } from '../editor/editorHistory'
|
||||
import { useEditorShortcuts } from '../editor/useEditorShortcuts'
|
||||
import { fileToDataUrl } from '../spine/spineSourceBundle'
|
||||
import { downloadEditorConfig } from '../editor/editorConfig'
|
||||
|
||||
const store = useParticleStore()
|
||||
const holder = ref<HTMLElement | null>(null)
|
||||
const showEmitter = ref(true)
|
||||
const showBones = ref(false)
|
||||
const showSkinMesh = ref(false)
|
||||
const info = ref<{ systems: number; particles: number } | null>(null)
|
||||
const showGrid = ref(true)
|
||||
const showSettings = ref(false)
|
||||
const showBackgroundMenu = ref(false)
|
||||
const backgroundFileInput = ref<HTMLInputElement | null>(null)
|
||||
const backgroundFileName = ref('')
|
||||
const backgroundX = ref(0)
|
||||
const backgroundY = ref(0)
|
||||
const backgroundOpacity = ref(1)
|
||||
const backgroundFileName = computed(() => store.settings.backgroundImageName)
|
||||
// 根变换 gizmo 工具:'translate' | 'rotate' | 'scale' | ''(无)
|
||||
const activeTool = ref<'' | 'translate' | 'rotate' | 'scale'>('')
|
||||
function setTool(t: '' | 'translate' | 'rotate' | 'scale') { activeTool.value = activeTool.value === t ? '' : t }
|
||||
const canvasFocused = ref(false)
|
||||
function toggleTool(t: '' | 'translate' | 'rotate' | 'scale') { activeTool.value = activeTool.value === t ? '' : t }
|
||||
|
||||
if (!(store.settings.undoLimit > 0)) store.settings.undoLimit = 20
|
||||
if (!Number.isFinite(store.settings.backgroundX)) store.settings.backgroundX = 0
|
||||
if (!Number.isFinite(store.settings.backgroundY)) store.settings.backgroundY = 0
|
||||
if (!Number.isFinite(store.settings.backgroundOpacity)) store.settings.backgroundOpacity = 1
|
||||
if (store.settings.backgroundImageName == null) store.settings.backgroundImageName = ''
|
||||
if (store.settings.backgroundImageData == null) store.settings.backgroundImageData = ''
|
||||
if (store.settings.showEmitter == null) store.settings.showEmitter = true
|
||||
if (store.settings.showBones == null) store.settings.showBones = false
|
||||
if (store.settings.showSkinMesh == null) store.settings.showSkinMesh = false
|
||||
if (store.settings.showGrid == null) store.settings.showGrid = true
|
||||
|
||||
useEditorShortcuts({
|
||||
canvasElement: holder,
|
||||
canvasFocused,
|
||||
undo: undoLastOperation,
|
||||
save: () => downloadEditorConfig(store),
|
||||
togglePlay: () => store.togglePlay(),
|
||||
toStart: () => store.toStart(),
|
||||
setTool: (tool) => { activeTool.value = tool },
|
||||
})
|
||||
|
||||
let app: Application | null = null
|
||||
let world: Container | null = null
|
||||
let backgroundSprite: Sprite | null = null
|
||||
let backgroundTexture: Texture | null = null
|
||||
let backgroundObjectUrl = ''
|
||||
let backgroundLoadVersion = 0
|
||||
let originGfx: Graphics | null = null
|
||||
let boneFollowTargetGfx: Graphics | null = null
|
||||
let boneGfx: Graphics | null = null
|
||||
@@ -143,6 +176,7 @@ let dotTex: Texture<any> | null = null
|
||||
let trailTex: Texture<any> | null = null
|
||||
|
||||
const emitterMap = new Map<number, ParticleEmitter>()
|
||||
const emitterConfigMap = new Map<number, object>()
|
||||
let previewColliders: CollisionBody[] = []
|
||||
const emitterPreviewTransform = new Map<number, { x: number; y: number; rotation: number }>()
|
||||
const selectedPathNodes = new Set<number>()
|
||||
@@ -203,45 +237,59 @@ function toggleSettings() {
|
||||
if (showSettings.value) showBackgroundMenu.value = false
|
||||
}
|
||||
|
||||
function onUndoLimitChange(event: Event) {
|
||||
setUndoLimit(store, Number((event.target as HTMLInputElement).value))
|
||||
}
|
||||
|
||||
async function onBackgroundFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file || !backgroundSprite) return
|
||||
const nextUrl = URL.createObjectURL(file)
|
||||
if (!file) return
|
||||
try {
|
||||
const nextTexture = await Texture.fromURL(nextUrl)
|
||||
const previousTexture = backgroundTexture
|
||||
const previousUrl = backgroundObjectUrl
|
||||
backgroundTexture = nextTexture
|
||||
backgroundObjectUrl = nextUrl
|
||||
backgroundSprite.texture = nextTexture
|
||||
backgroundSprite.visible = true
|
||||
backgroundFileName.value = file.name
|
||||
if (previousTexture) previousTexture.destroy(true)
|
||||
if (previousUrl) URL.revokeObjectURL(previousUrl)
|
||||
store.settings.backgroundImageData = await fileToDataUrl(file)
|
||||
store.settings.backgroundImageName = file.name
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(nextUrl)
|
||||
console.error('背景图加载失败', error)
|
||||
} finally {
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function clearBackground(resetControls = true) {
|
||||
if (backgroundSprite) {
|
||||
backgroundSprite.texture = Texture.EMPTY
|
||||
backgroundSprite.visible = false
|
||||
async function syncBackgroundTexture(dataUrl: string) {
|
||||
const version = ++backgroundLoadVersion
|
||||
if (!dataUrl) {
|
||||
if (backgroundSprite) {
|
||||
backgroundSprite.texture = Texture.EMPTY
|
||||
backgroundSprite.visible = false
|
||||
}
|
||||
if (backgroundTexture) backgroundTexture.destroy(true)
|
||||
backgroundTexture = null
|
||||
return
|
||||
}
|
||||
if (backgroundTexture) backgroundTexture.destroy(true)
|
||||
if (backgroundObjectUrl) URL.revokeObjectURL(backgroundObjectUrl)
|
||||
backgroundTexture = null
|
||||
backgroundObjectUrl = ''
|
||||
backgroundFileName.value = ''
|
||||
try {
|
||||
const texture = await Texture.fromURL(dataUrl)
|
||||
if (version !== backgroundLoadVersion || !backgroundSprite) {
|
||||
texture.destroy(true)
|
||||
return
|
||||
}
|
||||
const previous = backgroundTexture
|
||||
backgroundTexture = texture
|
||||
backgroundSprite.texture = texture
|
||||
backgroundSprite.visible = true
|
||||
if (previous) previous.destroy(true)
|
||||
} catch (error) {
|
||||
console.error('背景图恢复失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
function clearBackground(resetControls = true) {
|
||||
store.settings.backgroundImageData = ''
|
||||
store.settings.backgroundImageName = ''
|
||||
if (backgroundFileInput.value) backgroundFileInput.value.value = ''
|
||||
if (resetControls) {
|
||||
backgroundX.value = 0
|
||||
backgroundY.value = 0
|
||||
backgroundOpacity.value = 1
|
||||
store.settings.backgroundX = 0
|
||||
store.settings.backgroundY = 0
|
||||
store.settings.backgroundOpacity = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,12 +785,18 @@ function viewScaleBy(factor: number) {
|
||||
}
|
||||
function fitView() { store.editor.viewScale = 1.0; store.editor.viewX = 0; store.editor.viewY = 0 }
|
||||
function resetView() { store.editor.viewScale = 1.0; store.editor.viewX = 0; store.editor.viewY = 0 }
|
||||
function toggleGrid() { showGrid.value = !showGrid.value }
|
||||
function toggleGrid() { store.settings.showGrid = !store.settings.showGrid }
|
||||
|
||||
function syncEmitters() {
|
||||
if (!world) return
|
||||
for (const [id, em] of emitterMap) {
|
||||
if (!store.systems.some((s) => s.id === id)) { world.removeChild(em); em.destroy({ children: true }); emitterMap.delete(id) }
|
||||
const system = store.systems.find((item) => item.id === id)
|
||||
if (!system || emitterConfigMap.get(id) !== system.config) {
|
||||
world.removeChild(em)
|
||||
em.destroy({ children: true })
|
||||
emitterMap.delete(id)
|
||||
emitterConfigMap.delete(id)
|
||||
}
|
||||
}
|
||||
for (const sys of store.systems) {
|
||||
// 新增系统若还没贴图(dotTex 已加载后),补上默认点纹理,否则无法渲染
|
||||
@@ -764,6 +818,7 @@ function syncEmitters() {
|
||||
em.setEmitterPos(() => ({ x: sys.config.centerX, y: sys.config.centerY }))
|
||||
world.addChild(em)
|
||||
emitterMap.set(sys.id, em)
|
||||
emitterConfigMap.set(sys.id, sys.config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,7 +826,7 @@ function drawOrigin() {
|
||||
if (!originGfx) return
|
||||
const g = originGfx
|
||||
g.clear()
|
||||
if (!showEmitter.value) return
|
||||
if (!store.settings.showEmitter) return
|
||||
// 每个系统对应一个发射点(坐标点);没有系统时不画
|
||||
if (!store.systems.length) return
|
||||
for (const sys of store.systems) {
|
||||
@@ -836,7 +891,7 @@ function drawShapeRange() {
|
||||
if (!shapeGfx) return
|
||||
const g = shapeGfx
|
||||
g.clear()
|
||||
if (!showEmitter.value) return
|
||||
if (!store.settings.showEmitter) return
|
||||
const sys = store.systems.find((s) => s.id === store.activeId)
|
||||
if (!sys || sys.visible === false) return
|
||||
const c = sys.config
|
||||
@@ -1022,7 +1077,7 @@ function drawBones(bones: BonePreview[]) {
|
||||
if (!boneGfx) return
|
||||
const g = boneGfx
|
||||
g.clear()
|
||||
if (!showBones.value) return
|
||||
if (!store.settings.showBones) return
|
||||
const cx = 0, cy = 0
|
||||
// root → 每颗粒子骨骼 的连线
|
||||
for (const b of bones) {
|
||||
@@ -1237,8 +1292,8 @@ function loop() {
|
||||
world.scale.set(store.editor.viewScale)
|
||||
world.position.set(app.screen.width / 2 + store.editor.viewX, app.screen.height * 0.5 + store.editor.viewY)
|
||||
if (backgroundSprite) {
|
||||
backgroundSprite.position.set(Number(backgroundX.value) || 0, -(Number(backgroundY.value) || 0))
|
||||
backgroundSprite.alpha = Math.min(1, Math.max(0, Number(backgroundOpacity.value) || 0))
|
||||
backgroundSprite.position.set(Number(store.settings.backgroundX) || 0, -(Number(store.settings.backgroundY) || 0))
|
||||
backgroundSprite.alpha = Math.min(1, Math.max(0, Number(store.settings.backgroundOpacity) || 0))
|
||||
}
|
||||
|
||||
drawAxis()
|
||||
@@ -1411,7 +1466,7 @@ function loop() {
|
||||
}
|
||||
timelineDisplayFrame = displayFrame
|
||||
spineRuntime?.sync(store.spines, displayFrame, fps)
|
||||
for (const [, emitter] of emitterMap) emitter.drawTrailMeshDebug(showSkinMesh.value)
|
||||
for (const [, emitter] of emitterMap) emitter.drawTrailMeshDebug(store.settings.showSkinMesh)
|
||||
drawCollisionBodies()
|
||||
drawPaths()
|
||||
drawBoneFollowTargets()
|
||||
@@ -1430,6 +1485,7 @@ onMounted(async () => {
|
||||
app = new Application({ width: el.clientWidth || 800, height: el.clientHeight || 600, backgroundAlpha: 0, antialias: true })
|
||||
el.appendChild(app.view as unknown as HTMLCanvasElement)
|
||||
createLayers()
|
||||
watch(() => store.settings.backgroundImageData, syncBackgroundTexture, { immediate: true })
|
||||
await loadTexture()
|
||||
const tex = dotTex as Texture<any>
|
||||
for (const sys of store.systems) {
|
||||
@@ -1474,13 +1530,16 @@ onMounted(async () => {
|
||||
store.timeline.loop = true
|
||||
},
|
||||
)
|
||||
installEditorHistory(store)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(raf)
|
||||
spineRuntime?.destroy()
|
||||
spineRuntime = null
|
||||
clearBackground(false)
|
||||
backgroundLoadVersion++
|
||||
if (backgroundTexture) backgroundTexture.destroy(true)
|
||||
backgroundTexture = null
|
||||
backgroundSprite = null
|
||||
if (app) { app.destroy(true, { children: true }); app = null }
|
||||
})
|
||||
@@ -1492,6 +1551,7 @@ onUnmounted(() => {
|
||||
.stage-holder {
|
||||
width: 100%; height: 100%; cursor: grab; touch-action: none;
|
||||
background-color: #0e1219;
|
||||
outline: none;
|
||||
}
|
||||
.stage-holder.grid {
|
||||
background: conic-gradient(#141a26 90deg, transparent 90deg 180deg, #141a26 180deg 270deg, transparent 270deg);
|
||||
@@ -1557,6 +1617,12 @@ b { color: #ffcc33; }
|
||||
background: #202a3b; color: #cdf; font-size: 12px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.settings-btn:hover { border-color: #7774ff; background: #2b3a5c; color: #fff; }
|
||||
.undo-btn {
|
||||
width: 30px; height: 28px; flex: 0 0 30px; border: 1px solid #3a4963; border-radius: 6px;
|
||||
background: #202a3b; color: #cdd8ed; font-size: 18px; line-height: 1; cursor: pointer;
|
||||
}
|
||||
.undo-btn:hover:not(:disabled) { border-color: #7774ff; background: #2b3a5c; color: #fff; }
|
||||
.undo-btn:disabled { opacity: .38; cursor: not-allowed; }
|
||||
.settings-panel {
|
||||
position: absolute; top: 52px; left: 10px; width: 240px; z-index: 10;
|
||||
background: rgba(20,20,32,0.97); border: 1px solid #2a2a3c; border-radius: 10px; padding: 10px 12px;
|
||||
@@ -1570,6 +1636,7 @@ b { color: #ffcc33; }
|
||||
.sp-row > span:first-child { width: 76px; flex-shrink: 0; color: #8a93bb; }
|
||||
.sp-row input[type=range] { flex: 1; background: #1a1a28; accent-color: #3a5a8c; }
|
||||
.sp-num { width: 52px; background: #1a1a28; border: 1px solid #2e2e44; border-radius: 5px; color: #dde; font-size: 12px; padding: 2px 4px; }
|
||||
.sp-undo-limit { margin-left: auto; width: 64px; height: 26px; box-sizing: border-box; }
|
||||
.sp-colr { width: 26px; height: 22px; border: none; background: none; cursor: pointer; }
|
||||
.sp-hex { font-size: 11px; color: #889; font-variant-numeric: tabular-nums; }
|
||||
.sp-chk { display: flex; align-items: center; gap: 6px; margin: 8px 0; color: #aab; }
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div class="timeline">
|
||||
<!-- 控制按钮 -->
|
||||
<div class="tl-controls">
|
||||
<button class="tl-btn" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? '暂停' : '播放'">
|
||||
<button class="tl-btn" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? '暂停 (D)' : '播放 (D)'">
|
||||
{{ store.timeline.playing ? '❚❚' : '▶' }}
|
||||
</button>
|
||||
<button class="tl-btn" @click="store.toStart()" title="回到第一帧">⏮</button>
|
||||
<button class="tl-btn" @click="store.toStart()" title="回到第一帧 (S)">⏮</button>
|
||||
<button class="tl-btn" @click="store.previousFrame()" title="前一帧">◀</button>
|
||||
<button class="tl-btn" @click="store.nextFrame()" title="后一帧">▶</button>
|
||||
<button class="tl-btn" @click="store.toEnd()" title="移动到最后一帧">⏭</button>
|
||||
|
||||
Reference in new issue
Block a user