spine资源引入
This commit is contained in:
1 parent
166b5015c2
commit
5f4667f736
25 files changed
+31151
-9082
No files matched your search
+72
-3
@@ -5,10 +5,13 @@
|
||||
<button class="tl-btn" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? '暂停' : '播放'">
|
||||
{{ store.timeline.playing ? '❚❚' : '▶' }}
|
||||
</button>
|
||||
<button class="tl-btn" @click="store.toStart()" title="回到起点">⏮</button>
|
||||
<button class="tl-btn" @click="store.toStart()" title="回到第一帧">⏮</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>
|
||||
<button class="tl-btn" :class="{ on: store.timeline.loop }" @click="store.toggleLoop()" title="循环">⟳</button>
|
||||
<span class="tl-frame">帧 {{ store.timeline.frame }} / {{ store.timeline.totalFrames }}</span>
|
||||
<span class="tl-legend"><i class="legend-emission"></i>持续发射 <i class="legend-life"></i>粒子存在</span>
|
||||
<span class="tl-legend"><i class="legend-emission"></i>持续发射 <i class="legend-life"></i>粒子存在 <i class="legend-spine"></i>骨架动画</span>
|
||||
<label class="tl-anim">动画
|
||||
<select v-model="store.timeline.animation" class="tl-sel">
|
||||
<option value="animation">animation</option>
|
||||
@@ -60,7 +63,19 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!store.systems.length" class="tl-empty">暂无粒子系统</div>
|
||||
<!-- Spine 动画条:紫色长度由动画数据决定,只允许整体拖动开始位置。 -->
|
||||
<div v-for="spine in spineLanes" :key="`spine-${spine.id}`" class="tl-lane spine-lane">
|
||||
<span class="lane-ico spine-ico">◆</span>
|
||||
<div
|
||||
class="spine-bar"
|
||||
:style="{ left: spineLeftPx(spine) + 'px', width: spineWidthPx(spine) + 'px' }"
|
||||
title="拖动调整动画开始时间"
|
||||
@pointerdown.stop="onSpineBarDown($event, spine)"
|
||||
>
|
||||
<span class="lane-name">{{ spine.name }} · {{ spine.selectedAnimation }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!store.systems.length && !spineLanes.length" class="tl-empty">暂无可显示的动画数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,6 +84,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
|
||||
type SysLike = { id: number; config: any }
|
||||
|
||||
@@ -83,9 +99,13 @@ const viewFrames = ref(120)
|
||||
// 避免 pointermove 每一帧都触发 Stage 清缓存、重置粒子模拟。
|
||||
const draftDelays = ref<Record<number, number>>({})
|
||||
const draftDurations = ref<Record<number, number>>({})
|
||||
const draftSpineStarts = ref<Record<number, number>>({})
|
||||
|
||||
const totalFrames = computed(() => store.timeline.totalFrames)
|
||||
const zoomLabel = computed(() => (viewFrames.value <= 30 ? '放大' : viewFrames.value >= 300 ? '缩小' : '适中'))
|
||||
const spineLanes = computed(() => store.spines.filter((spine) =>
|
||||
spine.selectedAnimation && spine.animations.some((animation) => animation.name === spine.selectedAnimation && animation.duration > 0),
|
||||
))
|
||||
|
||||
// 每像素代表的帧数(视口宽度内放下 viewFrames 帧)
|
||||
function pxPerF() {
|
||||
@@ -141,6 +161,19 @@ function lifeWidthPx(sys: SysLike) {
|
||||
return emissionWidthPx(sys) + (maxParticleLife(sys) * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
function selectedSpineDuration(spine: SpineSceneObject) {
|
||||
return spine.animations.find((animation) => animation.name === spine.selectedAnimation)?.duration || 0
|
||||
}
|
||||
|
||||
function spineLeftPx(spine: SpineSceneObject) {
|
||||
const start = draftSpineStarts.value[spine.id] ?? Math.max(0, Number(spine.timeOffset) || 0)
|
||||
return (start * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
function spineWidthPx(spine: SpineSceneObject) {
|
||||
return (selectedSpineDuration(spine) * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
// 滚轮缩放:只改变视口显示的帧数(左侧恒为 0 帧),不改变播放时长/totalFrames
|
||||
function onWheel(e: WheelEvent) {
|
||||
const factor = e.deltaY < 0 ? 0.8 : 1.25 // 滚上放大(显示更少帧),滚下缩小(显示更多帧)
|
||||
@@ -218,6 +251,34 @@ function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
window.addEventListener('pointerup', up)
|
||||
window.addEventListener('pointercancel', cancel)
|
||||
}
|
||||
|
||||
// Spine 动画长度来自文件,不能缩放;拖动整条只修改延迟播放时间。
|
||||
function onSpineBarDown(e: PointerEvent, spine: SpineSceneObject) {
|
||||
e.stopPropagation()
|
||||
const startX = e.clientX
|
||||
const startTime = Math.max(0, Number(spine.timeOffset) || 0)
|
||||
const secPerPx = pxPerF() / fps.value
|
||||
const move = (ev: PointerEvent) => {
|
||||
const seconds = Math.max(0, Math.min(startTime + (ev.clientX - startX) * secPerPx, 600))
|
||||
draftSpineStarts.value[spine.id] = Math.round(seconds * fps.value) / fps.value
|
||||
}
|
||||
const finish = (commit: boolean) => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
window.removeEventListener('pointerup', up)
|
||||
window.removeEventListener('pointercancel', cancel)
|
||||
const value = draftSpineStarts.value[spine.id]
|
||||
delete draftSpineStarts.value[spine.id]
|
||||
if (commit && value != null && value !== startTime) {
|
||||
spine.timeOffset = value
|
||||
store.recalcTotalFrames()
|
||||
}
|
||||
}
|
||||
const up = () => finish(true)
|
||||
const cancel = () => finish(false)
|
||||
window.addEventListener('pointermove', move)
|
||||
window.addEventListener('pointerup', up)
|
||||
window.addEventListener('pointercancel', cancel)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -230,6 +291,7 @@ function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
.tl-legend i { display: inline-block; width: 10px; height: 7px; border-radius: 2px; }
|
||||
.legend-emission { margin-left: 4px; background: #2f8b62; }
|
||||
.legend-life { margin-left: 5px; background: #a54551; }
|
||||
.legend-spine { margin-left: 5px; background: #7448b8; }
|
||||
.tl-anim { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #aab; }
|
||||
.tl-sel { background: #1a1a28; border: 1px solid #2e2e44; color: #dde; border-radius: 5px; padding: 2px 6px; font-size: 12px; }
|
||||
.tl-zoom { margin-left: auto; font-size: 11px; color: #667; }
|
||||
@@ -248,5 +310,12 @@ function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
.bar-emission { position: absolute; inset: 0 auto 0 0; z-index: 1; min-width: 1px; border: 1px solid #45a97d; border-radius: 5px 0 0 5px; background: #2f8b62; box-sizing: border-box; }
|
||||
.bar-handle { position: absolute; z-index: 4; right: -4px; top: -1px; bottom: -1px; width: 8px; cursor: ew-resize; background: rgba(255,255,255,0.18); }
|
||||
.bar-handle:hover { background: rgba(255,255,255,0.35); }
|
||||
.spine-ico { color: #c08cff; }
|
||||
.spine-bar {
|
||||
position: absolute; top: 4px; height: 18px; min-width: 1px; box-sizing: border-box;
|
||||
border: 1px solid #a575ed; border-radius: 5px; background: #7448b8; cursor: grab;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.05);
|
||||
}
|
||||
.spine-bar:active { cursor: grabbing; }
|
||||
.tl-empty { color: #667; font-size: 11px; padding: 8px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user