Files
SpineParticlesWeb/src/views/particle-panel/EmissionSection.vue
T

187 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 2. 发射模式 -->
<div class="group">
<div class="group-head" @click="toggle('emitMode')">
<span class="caret">{{ collapsed.emitMode ? '▸' : '▾' }}</span><span>发射模式</span>
<button class="group-reset" title="重置发射模式参数" @click.stop="resetModule('emitMode')">重置</button>
</div>
<div class="group-body" v-show="!collapsed.emitMode">
<div class="segs">
<button class="seg" :class="{ on: sys.config.mode === 'stream' }" @click="setEmitMode('stream')">持续</button>
<button class="seg" :class="{ on: sys.config.mode === 'burst' }" @click="setEmitMode('burst')">爆发</button>
</div>
<template v-if="sys.config.mode === 'stream'">
<div class="segs stream-behavior-segs">
<button class="seg" :class="{ on: sys.config.streamBehavior === 'normal' }" @click="setStreamBehavior('normal')">常规持续</button>
<button class="seg" :class="{ on: sys.config.streamBehavior === 'loop' }" @click="setStreamBehavior('loop')">循环持续</button>
</div>
<NumSlider label="发射速率" :min="1" :max="300" :step="1" v-model="sys.config.rate" />
<NumSlider v-if="sys.config.streamBehavior === 'normal'" label="发射时长" :min="1 / 30" :max="60" :step="1 / 30" v-model="sys.config.duration" />
<NumSlider v-else label="发射时长 (f)" :min="1" :max="1800" :step="1" :model-value="loopDurationFrames" @update:model-value="setLoopDurationFrames" />
<div v-if="sys.config.streamBehavior === 'loop'" class="stream-loop-options">
<div class="loop-section-row">
<label class="switch-row">
<input type="checkbox" :checked="sys.config.generateLoopStartAnimation" @change="setLoopSection('start', $event)" />
<span class="switch-ui"></span><span>生成开始循环动画</span>
</label>
<div v-if="sys.config.generateLoopStartAnimation" class="loop-duration-control">
<label class="switch-row loop-duration-toggle">
<input type="checkbox" :checked="sys.config.loopStartUseCustomDuration" @change="setLoopCustomDuration('start', $event)" />
<span class="switch-ui"></span><span>指定时长</span>
</label>
<label class="loop-section-duration">
<input v-if="sys.config.loopStartUseCustomDuration" :value="sys.config.loopStartDurationFrames" type="number" min="1" max="1800" step="1" aria-label="开始循环段时长" @change="setLoopSectionDuration('start', $event)" /><span v-if="sys.config.loopStartUseCustomDuration">f</span>
<span v-else class="loop-auto-duration">自动 {{ automaticLoopDuration }}f</span>
</label>
</div>
</div>
<div class="loop-section-row">
<label class="switch-row">
<input type="checkbox" :checked="sys.config.generateLoopEndAnimation" @change="setLoopSection('end', $event)" />
<span class="switch-ui"></span><span>生成结束循环动画</span>
</label>
<div v-if="sys.config.generateLoopEndAnimation" class="loop-duration-control">
<label class="switch-row loop-duration-toggle">
<input type="checkbox" :checked="sys.config.loopEndUseCustomDuration" @change="setLoopCustomDuration('end', $event)" />
<span class="switch-ui"></span><span>指定时长</span>
</label>
<label class="loop-section-duration">
<input v-if="sys.config.loopEndUseCustomDuration" :value="sys.config.loopEndDurationFrames" type="number" min="1" max="1800" step="1" aria-label="结束循环段时长" @change="setLoopSectionDuration('end', $event)" /><span v-if="sys.config.loopEndUseCustomDuration">f</span>
<span v-else class="loop-auto-duration">自动 {{ automaticLoopDuration }}f</span>
</label>
</div>
</div>
</div>
<div v-if="sys.config.streamBehavior === 'loop'" class="modifier-hint stream-loop-hint">循环持续模式不支持骨骼跟随和路径跟随;进入或退出时会将粒子跟随设置恢复默认值。</div>
</template>
<template v-else>
<NumSlider label="单次数量" :min="1" :max="1000" :step="1" v-model="sys.config.burstCount" />
</template>
<NumSlider label="延迟时间" :min="0" :max="5" :step="0.1" v-model="sys.config.delay" />
</div>
</div>
<!-- 3. 发射器形状 -->
<div class="group">
<div class="group-head" @click="toggle('shape')">
<span class="caret">{{ collapsed.shape ? '▸' : '▾' }}</span><span>发射器形状</span>
<button class="group-reset" title="重置发射器形状参数" @click.stop="resetModule('shape')">重置</button>
</div>
<div class="group-body" v-show="!collapsed.shape">
<label class="row"><span>形状</span>
<select v-model="sys.config.shape" class="inp" @change="onShapeChange">
<option value="point">点</option>
<option value="circle">圆形</option>
<option value="rect">矩形</option>
<option value="cone">锥形</option>
<option value="path">路径</option>
</select>
</label>
<template v-if="sys.config.shape === 'circle' || sys.config.shape === 'cone'">
<NumSlider :label="sys.config.shape === 'cone' ? '内径' : '内半径'" :min="0" :max="sys.config.outerRadius" :step="1" v-model="sys.config.innerRadius" />
<NumSlider :label="sys.config.shape === 'cone' ? '外径' : '外半径'" :min="sys.config.innerRadius" :max="300" :step="1" v-model="sys.config.outerRadius" />
</template>
<template v-if="sys.config.shape === 'cone'">
<NumSlider label="锥形朝向" :min="-360" :max="360" :step="1" v-model="sys.config.coneDirection" />
<NumSlider label="锥形开口角度" :min="1" :max="360" :step="1" v-model="sys.config.coneAngle" />
</template>
<template v-else-if="sys.config.shape === 'rect'">
<NumSlider label="内宽" :min="0" :max="sys.config.outerRectW" :step="1" v-model="sys.config.innerRectW" />
<NumSlider label="内高" :min="0" :max="sys.config.outerRectH" :step="1" v-model="sys.config.innerRectH" />
<NumSlider label="外宽" :min="sys.config.innerRectW" :max="400" :step="1" v-model="sys.config.outerRectW" />
<NumSlider label="外高" :min="sys.config.innerRectH" :max="400" :step="1" v-model="sys.config.outerRectH" />
</template>
<template v-else-if="sys.config.shape === 'path'">
<label class="row"><span>选择路径</span>
<select v-model.number="sys.config.spawnPathId" class="inp" @change="onSpawnPathChange">
<option :value="0">无路径(点发射)</option>
<option v-for="path in store.paths" :key="path.id" :value="path.id" :disabled="!path.enabled">{{ path.name }}</option>
</select>
</label>
<label v-if="sys.config.spawnPathId > 0" class="row"><span>出生分布</span>
<select v-model="sys.config.spawnPathDistribution" class="inp">
<option value="fixed">等间距固定分布</option>
<option value="random">随机分布</option>
</select>
</label>
</template>
<div class="divider"></div>
<label class="row"><span>发射角度</span>
<select v-model="sys.config.emissionAngleMode" class="inp">
<option value="fixed">固定</option>
<option value="random">随机</option>
<option v-if="sys.config.shape === 'circle' || sys.config.shape === 'rect' || sys.config.shape === 'cone'" value="radial">径向(中心向外)</option>
<option v-if="sys.config.shape === 'path' && sys.config.spawnPathId > 0" value="path-normal">垂直于路径</option>
<option v-if="sys.config.shape === 'path' && sys.config.spawnPathId > 0" value="path-tangent">沿路径方向</option>
</select>
</label>
<NumSlider v-if="sys.config.emissionAngleMode === 'fixed'" label="固定角度" :min="-360" :max="360" :step="1" v-model="sys.config.direction" />
<template v-else-if="sys.config.emissionAngleMode === 'random'">
<NumSlider label="基准角度" :min="-360" :max="360" :step="1" v-model="sys.config.direction" />
<NumSlider label="随机范围" :min="0" :max="360" :step="1" v-model="sys.config.spread" />
</template>
<label v-if="sys.config.shape !== 'point'" class="switch-row">
<input type="checkbox" v-model="sys.config.showShape" />
<span class="switch-ui"></span><span>显示形状</span>
</label>
</div>
</div>
</template>
<script setup lang="ts">
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { store, sys, automaticLoopDuration, loopDurationFrames, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, resetModule, select, onShapeChange, onSpawnPathChange } = useParticlePanelContext()
</script>
<style scoped>
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
.group-head {
display: flex; align-items: center; gap: 6px; padding: 7px 10px; cursor: pointer;
font-size: 12px; font-weight: 600; color: #ccd; background: #191924; user-select: none;
}
.group-head:hover { background: #20202e; }
.group-reset {
margin-left: auto; height: 22px; padding: 0 7px; border: 1px solid #3a465f; border-radius: 5px;
background: #252d3d; color: #9eabc3; font-size: 10px; line-height: 20px; cursor: pointer;
}
.group-reset:hover { border-color: #7774ff; background: #45426f; color: #fff; }
.group-body { padding: 8px 10px 12px; }
.caret { display: inline-block; width: 12px; color: #667; font-size: 11px; }
.divider { height: 1px; background: #2a2a3c; margin: 8px 0; }
.row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 12px; color: #aab; }
.row > span:first-child { width: 60px; flex-shrink: 0; color: #8a93bb; }
.inp { flex: 1; min-width: 0; padding: 3px 5px; background: #1a1a28; border: 1px solid #2e2e44; border-radius: 5px; color: #dde; font-size: 12px; }
.segs { display: flex; gap: 6px; margin: 6px 0; }
.stream-behavior-segs { margin-top: 10px; }
.stream-loop-options { display: grid; gap: 8px; margin: 8px 0 4px; padding: 9px 10px; border: 1px solid #32405a; border-radius: 6px; background: #171f2e; }
.loop-section-row { display: flex; min-height: 27px; align-items: center; justify-content: space-between; gap: 8px; }
.loop-duration-control { display: flex; flex: 0 0 auto; align-items: center; gap: 7px; }
.loop-duration-toggle { gap: 5px; font-size: 10px; white-space: nowrap; }
.loop-duration-toggle .switch-ui { width: 26px; height: 14px; }
.loop-duration-toggle .switch-ui::after { width: 10px; height: 10px; }
.loop-duration-toggle input:checked + .switch-ui::after { transform: translateX(12px); }
.loop-section-duration { display: flex; flex: 0 0 auto; align-items: center; gap: 4px; color: #7f8ba4; font-size: 11px; }
.loop-section-duration input { position: static; width: 54px; height: 25px; padding: 0 6px; border: 1px solid #3a465f; border-radius: 5px; outline: none; background: #101725; color: #e4e8f2; text-align: right; opacity: 1; pointer-events: auto; }
.loop-section-duration input:focus { border-color: #716eff; }
.loop-auto-duration { min-width: 54px; color: #77839a; text-align: right; white-space: nowrap; }
.seg { flex: 1; padding: 5px; background: #1e1e2e; border: 1px solid #2e2e44; border-radius: 6px; color: #aab; font-size: 12px; cursor: pointer; }
.seg.on { background: #3a5a8c; color: #fff; border-color: #5a7ab8; }
.seg { display: flex; gap: 6px; margin: 6px 0; }
.inp.tiny { width: 46px; flex: 0 0 46px; text-align: center; }
.switch-row { display: flex; align-items: center; gap: 9px; color: #b8c1d5; font-size: 12px; cursor: pointer; }
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
.switch-ui { position: relative; width: 32px; height: 18px; border-radius: 10px; background: #354258; transition: background .16s ease; }
.switch-ui::after { content: ''; position: absolute; left: 2px; top: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .16s ease; }
.switch-row input:checked + .switch-ui { background: #625ff1; }
.switch-row input:checked + .switch-ui::after { transform: translateX(14px); }
.modifier-hint { margin: 7px 0 0 2px; color: #647086; font-size: 9px; line-height: 1.5; }
.stream-loop-hint { margin: 9px 2px 3px; padding: 7px 8px; border: 1px solid #4a436e; border-radius: 5px; background: #211f35; color: #aaa4d7; }
.switch-row input:disabled + .switch-ui { opacity: 0.45; cursor: not-allowed; }
</style>