系统优化
This commit is contained in:
1 parent
b98d67fdab
commit
c37cf09273
5 files changed
+489
-32
No files matched your search
+43
-21
@@ -8,6 +8,7 @@
|
||||
<button class="tl-btn" @click="store.toStart()" 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>
|
||||
<label class="tl-anim">动画
|
||||
<select v-model="store.timeline.animation" class="tl-sel">
|
||||
<option value="animation">animation</option>
|
||||
@@ -42,14 +43,21 @@
|
||||
<div class="tl-lanes">
|
||||
<div v-for="(sys, i) in store.systems" :key="sys.id" class="tl-lane">
|
||||
<span class="lane-ico">●</span>
|
||||
<!-- 粒子条:left=start(delay), width=duration -->
|
||||
<!-- 粒子条:left=delay;红色底条=系统存在粒子的总时长;绿色覆盖层=持续发射时长 -->
|
||||
<div
|
||||
class="lane-bar"
|
||||
:style="{ left: barLeftPx(sys) + 'px', width: barWidthPx(sys) + 'px' }"
|
||||
@pointerdown.stop="onBarDown($event, sys)"
|
||||
>
|
||||
<span class="lane-name">{{ (sys.config as any).name || '系统 ' + (i + 1) }}</span>
|
||||
<span class="bar-handle" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
<span class="bar-life" :style="{ width: lifeWidthPx(sys) + 'px' }"></span>
|
||||
<span
|
||||
v-if="sys.config.mode === 'stream'"
|
||||
class="bar-emission"
|
||||
:style="{ width: emissionWidthPx(sys) + 'px' }"
|
||||
>
|
||||
<span class="bar-handle" title="拖动持续发射时长" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!store.systems.length" class="tl-empty">暂无粒子系统</div>
|
||||
@@ -67,7 +75,8 @@ type SysLike = { id: number; config: any }
|
||||
const store = useParticleStore()
|
||||
const vpRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const fps = 60
|
||||
// 时间轴与固定步长模拟统一读取 Store;默认 1 秒 = 30 帧,与 Spine 对齐。
|
||||
const fps = computed(() => Math.max(1, store.timeline.fps || 30))
|
||||
/** 视口当前能显示的帧数(滚轮缩放它)。视口起点恒为第 0 帧,不滚动 */
|
||||
const viewFrames = ref(51)
|
||||
// 拖动期间只更新本地预览值,松手后再一次性写回配置。
|
||||
@@ -106,7 +115,7 @@ function barLeftPx(sys: SysLike) {
|
||||
const s = (sys.config as any)
|
||||
// 左端 = 延迟发射时长(delay);左端之前是空白=延迟
|
||||
const delay = draftDelays.value[sys.id] ?? Math.max(0, Number(s.delay) || 0)
|
||||
return (delay * fps) / pxPerF()
|
||||
return (delay * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
function maxParticleLife(sys: SysLike) {
|
||||
@@ -117,11 +126,19 @@ function maxParticleLife(sys: SysLike) {
|
||||
}
|
||||
|
||||
function barWidthPx(sys: SysLike) {
|
||||
const s = (sys.config as any)
|
||||
// 宽度 = 粒子系统总时长 = 发射时长(duration) + 当前模式下的最大粒子生命
|
||||
// 即从 delay 开始,覆盖到最后一个粒子消失,右端与 totalFrames 对齐
|
||||
const duration = draftDurations.value[sys.id] ?? Math.max(0, Number(s.duration) || 0)
|
||||
return ((duration + maxParticleLife(sys)) * fps) / pxPerF()
|
||||
return lifeWidthPx(sys)
|
||||
}
|
||||
|
||||
function emissionWidthPx(sys: SysLike) {
|
||||
if ((sys.config as any).mode !== 'stream') return 0
|
||||
const duration = draftDurations.value[sys.id] ?? Math.max(1 / fps.value, Number((sys.config as any).duration) || 1)
|
||||
return (duration * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
function lifeWidthPx(sys: SysLike) {
|
||||
// 红色底条表示整个系统“仍有粒子存在”的时长:持续发射窗口 + 最长粒子生命。
|
||||
// 爆发模式的发射窗口为 0,因此只保留最长粒子生命。
|
||||
return emissionWidthPx(sys) + (maxParticleLife(sys) * fps.value) / pxPerF()
|
||||
}
|
||||
|
||||
// 滚轮缩放:只改变视口显示的帧数(左侧恒为 0 帧),不改变播放时长/totalFrames
|
||||
@@ -152,11 +169,11 @@ function onBarDown(e: PointerEvent, sys: SysLike) {
|
||||
e.stopPropagation()
|
||||
const startX = e.clientX
|
||||
const startDelay = Math.max(0, Number((sys.config as any).delay) || 0)
|
||||
const secPerPx = pxPerF() / fps
|
||||
const secPerPx = pxPerF() / fps.value
|
||||
const move = (ev: PointerEvent) => {
|
||||
const dSec = (ev.clientX - startX) * secPerPx
|
||||
const nd = Math.max(0, Math.min(startDelay + dSec, 600))
|
||||
draftDelays.value[sys.id] = Math.round(nd * fps) / fps
|
||||
draftDelays.value[sys.id] = Math.round(nd * fps.value) / fps.value
|
||||
}
|
||||
const finish = (commit: boolean) => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
@@ -176,17 +193,16 @@ function onBarDown(e: PointerEvent, sys: SysLike) {
|
||||
// 拖动粒子条右缘:调整发射时长 duration
|
||||
function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
e.stopPropagation()
|
||||
const startX = e.clientX
|
||||
const startDur = Math.max(0, Number((sys.config as any).duration) || 0)
|
||||
const secPerPx = pxPerF() / fps
|
||||
const secPerPx = pxPerF() / fps.value
|
||||
const barLeft = (e.currentTarget as HTMLElement).closest('.lane-bar')?.getBoundingClientRect().left ?? e.clientX
|
||||
let moved = false
|
||||
const move = (ev: PointerEvent) => {
|
||||
moved = true
|
||||
const dSec = (ev.clientX - startX) * secPerPx
|
||||
let nd = Math.max(0, Math.min(startDur + dSec, 600))
|
||||
// 有限发射最短为 1 帧;0 继续保留“无限发射”的既有语义。
|
||||
if (nd > 0) nd = Math.max(1 / fps, nd)
|
||||
draftDurations.value[sys.id] = Math.round(nd * fps) / fps
|
||||
// 使用右端相对粒子条左端的绝对距离换算发射时长。
|
||||
// 因此拖到 60f 就得到 60f,而不是减去粒子生命周期后的剩余帧数。
|
||||
const nd = Math.max(1 / fps.value, Math.min((ev.clientX - barLeft) * secPerPx, 600))
|
||||
draftDurations.value[sys.id] = Math.round(nd * fps.value) / fps.value
|
||||
}
|
||||
const finish = (commit: boolean) => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
@@ -210,6 +226,10 @@ function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
.tl-btn { width: 30px; height: 26px; background: #1e1e2e; border: 1px solid #2e2e44; border-radius: 6px; color: #aab; font-size: 12px; cursor: pointer; }
|
||||
.tl-btn.on { background: #3a5a8c; color: #fff; border-color: #5a7ab8; }
|
||||
.tl-frame { font-size: 12px; color: #aab; }
|
||||
.tl-legend { display: flex; align-items: center; gap: 4px; color: #778198; font-size: 10px; }
|
||||
.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; }
|
||||
.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; }
|
||||
@@ -221,10 +241,12 @@ function onBarResizeDown(e: PointerEvent, sys: SysLike) {
|
||||
.tl-lanes { position: absolute; top: 22px; left: 0; right: 0; bottom: 0; }
|
||||
.tl-lane { position: relative; height: 26px; margin-bottom: 4px; }
|
||||
.lane-ico { position: absolute; left: 4px; top: 6px; font-size: 11px; color: #dde; z-index: 2; pointer-events: none; }
|
||||
.lane-bar { position: absolute; top: 4px; height: 18px; background: #2b4a8c; border: 1px solid #4a6aa8; border-radius: 5px; display: flex; align-items: center; padding-left: 14px; cursor: grab; box-sizing: border-box; }
|
||||
.lane-bar { position: absolute; top: 4px; height: 18px; display: flex; align-items: stretch; cursor: grab; box-sizing: border-box; }
|
||||
.lane-bar:active { cursor: grabbing; }
|
||||
.lane-name { font-size: 11px; color: #dfe; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.bar-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 8px; cursor: ew-resize; background: rgba(255,255,255,0.15); border-radius: 0 4px 4px 0; }
|
||||
.lane-name { position: absolute; left: 14px; right: 5px; top: 2px; z-index: 2; pointer-events: none; font-size: 11px; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.bar-life { position: absolute; inset: 0 auto 0 0; z-index: 0; min-width: 1px; border: 1px solid #c05a66; border-radius: 5px; background: #a54551; box-sizing: border-box; }
|
||||
.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); }
|
||||
.tl-empty { color: #667; font-size: 11px; padding: 8px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user