中英双语
This commit is contained in:
1 parent
ea26af26ee
commit
b98c0333e6
50 files changed
+4082
-2049
No files matched your search
+35
-33
@@ -2,33 +2,33 @@
|
||||
<div class="timeline">
|
||||
<!-- 控制按钮 -->
|
||||
<div class="tl-controls">
|
||||
<button class="tl-btn" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? '暂停 (D)' : '播放 (D)'">
|
||||
<button class="tl-btn" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? $t('timeline.pause') : $t('timeline.play')">
|
||||
{{ store.timeline.playing ? '❚❚' : '▶' }}
|
||||
</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>
|
||||
<button class="tl-btn" :class="{ on: store.timeline.loop }" title="持续循环播放" aria-label="持续循环播放" @click="store.toggleLoop()">⟳</button>
|
||||
<button class="tl-btn" @click="store.toStart()" :title="$t('timeline.toStart')">⏮</button>
|
||||
<button class="tl-btn" @click="store.previousFrame()" :title="$t('timeline.previous')">◀</button>
|
||||
<button class="tl-btn" @click="store.nextFrame()" :title="$t('timeline.next')">▶</button>
|
||||
<button class="tl-btn" @click="store.toEnd()" :title="$t('timeline.toEnd')">⏭</button>
|
||||
<button class="tl-btn" :class="{ on: store.timeline.loop }" :title="$t('timeline.continuousLoop')" :aria-label="$t('timeline.continuousLoop')" @click="store.toggleLoop()">⟳</button>
|
||||
<div class="tl-ab-loop">
|
||||
<button class="tl-ab-toggle" :class="{ on: store.timeline.abLoop }" title="独立 A/B 区间循环" @click="store.toggleABLoop()">
|
||||
<span>AB 循环</span><i class="tl-switch"><b></b></i>
|
||||
<button class="tl-ab-toggle" :class="{ on: store.timeline.abLoop }" :title="$t('timeline.abLoop')" @click="store.toggleABLoop()">
|
||||
<span>{{ $t('timeline.abLoopShort') }}</span><i class="tl-switch"><b></b></i>
|
||||
</button>
|
||||
<div v-if="store.timeline.abLoop" class="tl-ab-popover" @pointerdown.stop>
|
||||
<div class="tl-ab-title">AB 循环区间</div>
|
||||
<label class="tl-range" title="自定义播放起始帧"><span>A</span><input v-model="playbackStartDraft" type="number" min="0" :max="Math.max(0, store.timeline.playbackEnd - 1)" aria-label="播放起始帧" @change="commitPlaybackStart" @keydown.enter.prevent="commitPlaybackStart" /></label>
|
||||
<label class="tl-range" title="自定义播放结束帧"><span>B</span><input v-model="playbackEndDraft" type="number" :min="store.timeline.playbackStart + 1" :max="Math.max(1, store.timeline.totalFrames - 1)" aria-label="播放结束帧" @change="commitPlaybackEnd" @keydown.enter.prevent="commitPlaybackEnd" /></label>
|
||||
<div class="tl-ab-title">{{ $t('timeline.abRange') }}</div>
|
||||
<label class="tl-range" :title="$t('timeline.playbackStart')"><span>A</span><input v-model="playbackStartDraft" type="number" min="0" :max="Math.max(0, store.timeline.playbackEnd - 1)" :aria-label="$t('timeline.startFrame')" @change="commitPlaybackStart" @keydown.enter.prevent="commitPlaybackStart" /></label>
|
||||
<label class="tl-range" :title="$t('timeline.playbackEnd')"><span>B</span><input v-model="playbackEndDraft" type="number" :min="store.timeline.playbackStart + 1" :max="Math.max(1, store.timeline.totalFrames - 1)" :aria-label="$t('timeline.endFrame')" @change="commitPlaybackEnd" @keydown.enter.prevent="commitPlaybackEnd" /></label>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tl-frame">帧 <b>{{ store.timeline.frame }}</b> / <b>{{ Math.max(0, store.timeline.totalFrames - 1) }}</b></span>
|
||||
<span class="tl-legend"><i class="legend-emission"></i>持续发射 <i class="legend-life"></i>粒子存在 <i class="legend-spine"></i>Spine 动画</span>
|
||||
<span class="tl-frame">{{ $t('timeline.frameStatus', { current: store.timeline.frame, total: Math.max(0, store.timeline.totalFrames - 1) }) }}</span>
|
||||
<span class="tl-legend"><i class="legend-emission"></i>{{ $t('timeline.emission') }} <i class="legend-life"></i>{{ $t('timeline.particleLife') }} <i class="legend-spine"></i>{{ $t('timeline.spineAnimation') }}</span>
|
||||
<div ref="animationMenuRef" class="tl-anim tl-owner-animation">
|
||||
<span>所属动画</span>
|
||||
<button class="tl-animation-trigger" :disabled="!activeParticleSystem" title="设置当前粒子系统所属的动画" @click.stop="toggleAnimationMenu">
|
||||
<span>{{ $t('timeline.ownerAnimation') }}</span>
|
||||
<button class="tl-animation-trigger" :disabled="!activeParticleSystem" :title="$t('timeline.setOwnerAnimation')" @click.stop="toggleAnimationMenu">
|
||||
<span>{{ activeParticleSystem?.animation || store.timeline.animation }}</span><i>⌄</i>
|
||||
</button>
|
||||
<div v-if="animationMenuOpen" class="tl-animation-menu" @pointerdown.stop>
|
||||
<div class="tl-animation-menu-title">选择所属动画</div>
|
||||
<div class="tl-animation-menu-title">{{ $t('timeline.chooseOwnerAnimation') }}</div>
|
||||
<button
|
||||
v-for="animation in store.timeline.animations"
|
||||
:key="animation"
|
||||
@@ -39,25 +39,25 @@
|
||||
<span>{{ animation }}</span><i v-if="activeParticleSystem?.animation === animation">✓</i>
|
||||
</button>
|
||||
<div class="tl-animation-editor">
|
||||
<input v-model="animationNameDraft" class="tl-animation-name" maxlength="64" aria-label="动画名称" @keydown.enter="renameCurrentAnimation" />
|
||||
<button title="重命名当前动画" @click="renameCurrentAnimation">重命名</button>
|
||||
<input v-model="animationNameDraft" class="tl-animation-name" maxlength="64" :aria-label="$t('timeline.animationName')" @keydown.enter="renameCurrentAnimation" />
|
||||
<button :title="$t('timeline.renameAnimation')" @click="renameCurrentAnimation">{{ $t('timeline.rename') }}</button>
|
||||
</div>
|
||||
<div v-if="animationError" class="tl-animation-error">{{ animationError }}</div>
|
||||
<div class="tl-animation-actions">
|
||||
<button @click="addAnimation">+ 新增</button>
|
||||
<button :disabled="store.timeline.animations.length <= 1" @click="deleteCurrentAnimation">删除</button>
|
||||
<button @click="addAnimation">{{ $t('timeline.newAnimation') }}</button>
|
||||
<button :disabled="store.timeline.animations.length <= 1" @click="deleteCurrentAnimation">{{ $t('common.delete') }}</button>
|
||||
</div>
|
||||
<div v-if="store.timeline.animations.length <= 1" class="tl-animation-hint">至少需要保留一个动画</div>
|
||||
<div v-if="store.timeline.animations.length <= 1" class="tl-animation-hint">{{ $t('timeline.keepOneAnimation') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="tl-anim tl-spine-preview">Spine 预览动画
|
||||
<label class="tl-anim tl-spine-preview">{{ $t('timeline.spinePreview') }}
|
||||
<select
|
||||
class="tl-sel"
|
||||
:value="previewSpine?.selectedAnimation || ''"
|
||||
:disabled="!previewSpine"
|
||||
@change="onSpinePreviewAnimationChange"
|
||||
>
|
||||
<option value="">{{ previewSpine ? '无' : '无可用 Spine 动画' }}</option>
|
||||
<option value="">{{ previewSpine ? $t('timeline.none') : $t('timeline.noSpineAnimation') }}</option>
|
||||
<option v-for="animation in previewSpine?.animations || []" :key="animation.name" :value="animation.name">
|
||||
{{ animation.name }}({{ animation.duration.toFixed(2) }}s)
|
||||
</option>
|
||||
@@ -99,17 +99,17 @@
|
||||
: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="lane-name">{{ (sys.config as any).name || $t('timeline.systemName', { index: i + 1 }) }}</span>
|
||||
<span class="bar-life" :class="{ 'loop-life': !isSubEmitter(sys) && isLoopStream(sys) }" :style="{ width: lifeWidthPx(sys) + 'px' }"></span>
|
||||
<template v-if="!isSubEmitter(sys) && isLoopStream(sys)">
|
||||
<span v-if="loopStartWidthPx(sys)" class="bar-loop-transition bar-loop-start" :style="{ width: loopStartWidthPx(sys) + 'px' }"></span>
|
||||
<span class="bar-emission bar-loop" :style="{ left: loopStartWidthPx(sys) + 'px', width: emissionWidthPx(sys) + 'px' }">
|
||||
<span class="bar-handle" title="拖动循环段时长" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
<span class="bar-handle" :title="$t('timeline.dragLoopDuration')" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
</span>
|
||||
<span v-if="loopEndWidthPx(sys)" class="bar-loop-transition bar-loop-end" :style="{ left: (loopStartWidthPx(sys) + emissionWidthPx(sys)) + 'px', width: loopEndWidthPx(sys) + 'px' }"></span>
|
||||
</template>
|
||||
<span v-else-if="!isSubEmitter(sys) && sys.config.mode === 'stream'" class="bar-emission" :style="{ width: emissionWidthPx(sys) + 'px' }">
|
||||
<span class="bar-handle" title="拖动持续发射时长" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
<span class="bar-handle" :title="$t('timeline.dragEmissionDuration')" @pointerdown.stop="onBarResizeDown($event, sys)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,13 +119,13 @@
|
||||
<div
|
||||
class="spine-bar"
|
||||
:style="{ left: spineLeftPx(spine) + 'px', width: spineWidthPx(spine) + 'px' }"
|
||||
title="拖动调整动画开始时间"
|
||||
:title="$t('timeline.dragSpineStart')"
|
||||
@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 v-if="!store.systems.length && !spineLanes.length" class="tl-empty">{{ $t('timeline.noAnimationData') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +133,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { loopSegmentTiming } from '../core/loopSegmentTiming'
|
||||
@@ -142,6 +143,7 @@ import { subEmitterTriggerTimings } from '../core/subEmitterTiming'
|
||||
type SysLike = { id: number; config: any }
|
||||
|
||||
const store = useParticleStore()
|
||||
const { t } = useI18n()
|
||||
const vpRef = ref<HTMLElement | null>(null)
|
||||
const animationMenuRef = ref<HTMLElement | null>(null)
|
||||
const animationMenuOpen = ref(false)
|
||||
@@ -182,16 +184,16 @@ function addAnimation() {
|
||||
function renameCurrentAnimation() {
|
||||
const current = activeParticleSystem.value?.animation || store.timeline.animation
|
||||
const next = animationNameDraft.value.trim()
|
||||
if (!next) { animationError.value = '动画名称不能为空'; return }
|
||||
if (next !== current && store.timeline.animations.includes(next)) { animationError.value = '动画名称不能重复'; return }
|
||||
if (!store.renameAnimation(current, next)) { animationError.value = '无法修改动画名称'; return }
|
||||
if (!next) { animationError.value = t('timeline.animationNameEmpty'); return }
|
||||
if (next !== current && store.timeline.animations.includes(next)) { animationError.value = t('timeline.animationNameDuplicate'); return }
|
||||
if (!store.renameAnimation(current, next)) { animationError.value = t('timeline.animationRenameFailed'); return }
|
||||
animationNameDraft.value = next
|
||||
animationError.value = ''
|
||||
}
|
||||
|
||||
function deleteCurrentAnimation() {
|
||||
const current = activeParticleSystem.value?.animation || store.timeline.animation
|
||||
if (!store.removeAnimation(current)) { animationError.value = '至少需要保留一个动画'; return }
|
||||
if (!store.removeAnimation(current)) { animationError.value = t('timeline.keepOneAnimation'); return }
|
||||
const next = activeParticleSystem.value?.animation || store.timeline.animation
|
||||
animationNameDraft.value = next
|
||||
animationError.value = ''
|
||||
@@ -225,7 +227,7 @@ const totalFrames = computed(() => store.timeline.totalFrames)
|
||||
watch(totalFrames, (value) => {
|
||||
if (value > viewFrames.value) viewFrames.value = Math.min(2000, value)
|
||||
})
|
||||
const zoomLabel = computed(() => (viewFrames.value <= 30 ? '放大' : viewFrames.value >= 300 ? '缩小' : '适中'))
|
||||
const zoomLabel = computed(() => (viewFrames.value <= 30 ? t('timeline.zoomIn') : viewFrames.value >= 300 ? t('timeline.zoomOut') : t('timeline.zoomFit')))
|
||||
const spineLanes = computed(() => store.spines.filter((spine) =>
|
||||
spine.selectedAnimation && spine.animations.some((animation) => animation.name === spine.selectedAnimation && animation.duration > 0),
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user