187 lines
13 KiB
Vue
187 lines
13 KiB
Vue
<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>
|